Skip to content

Commit

Permalink
refactor(server): simplify service dependency management
Browse files Browse the repository at this point in the history
  • Loading branch information
jrasm91 committed Oct 2, 2024
1 parent 6c7d51d commit bb758d1
Show file tree
Hide file tree
Showing 77 changed files with 837 additions and 1,801 deletions.
11 changes: 6 additions & 5 deletions server/src/services/activity.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,21 @@ 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 { newMockRepositories } 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();
const { deps, mocks } = newMockRepositories();
({ accessMock, activityMock } = mocks);

sut = new ActivityService(accessMock, activityMock);
sut = new ActivityService(...deps);
});

it('should work', () => {
Expand Down
22 changes: 8 additions & 14 deletions server/src/services/activity.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Inject, Injectable } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import {
ActivityCreateDto,
ActivityDto,
Expand All @@ -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({
const activities = await this.activityRepository.search({
userId: dto.userId,
albumId: dto.albumId,
assetId: dto.level === ReactionLevel.ALBUM ? null : dto.assetId,
Expand All @@ -38,7 +32,7 @@ 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) };
return { comments: await this.activityRepository.getStatistics(dto.assetId, dto.albumId) };
}

async create(auth: AuthDto, dto: ActivityCreateDto): Promise<MaybeDuplicate<ActivityResponseDto>> {
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -77,6 +71,6 @@ 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 this.activityRepository.delete(id);
}
}
25 changes: 8 additions & 17 deletions server/src/services/album.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,30 @@ 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 { newMockRepositories } 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);
const { deps, mocks } = newMockRepositories();
({ accessMock, albumMock, albumUserMock, eventMock, userMock } = mocks);

sut = new AlbumService(...deps);
});

it('should work', () => {
Expand Down
21 changes: 4 additions & 17 deletions server/src/services/album.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { BadRequestException, Injectable } from '@nestjs/common';
import {
AddUsersDto,
AlbumInfoDto,
Expand All @@ -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),
Expand Down
13 changes: 7 additions & 6 deletions server/src/services/api-key.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,20 @@ 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 { newMockRepositories } 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);
const { deps, mocks } = newMockRepositories();
({ cryptoMock, keyMock } = mocks);

sut = new APIKeyService(...deps);
});

describe('create', () => {
Expand Down
26 changes: 10 additions & 16 deletions server/src/services/api-key.service.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,20 @@
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);

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({
const entity = await this.keyRepository.create({
key: this.crypto.hashSha256(secret),
name: dto.name || 'API Key',
userId: auth.user.id,
Expand All @@ -31,35 +25,35 @@ 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');
}
return this.map(key);
}

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));
}

Expand Down
27 changes: 7 additions & 20 deletions server/src/services/asset-media.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,16 @@ 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';
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 { newMockRepositories } from 'test/utils';
import { QueryFailedError } from 'typeorm';
import { Mocked } from 'vitest';

Expand Down Expand Up @@ -189,24 +182,18 @@ 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);
const { deps, mocks } = newMockRepositories();
({ accessMock, assetMock, jobMock, storageMock, userMock } = mocks);

sut = new AssetMediaService(...deps);
});

describe('getUploadAssetIdByChecksum', () => {
Expand Down
31 changes: 4 additions & 27 deletions server/src/services/asset-media.service.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand All @@ -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;
Expand Down
Loading

0 comments on commit bb758d1

Please sign in to comment.