Skip to content

Commit

Permalink
refactor: service dependencies (immich-app#13108)
Browse files Browse the repository at this point in the history
refactor(server): simplify service dependency management
  • Loading branch information
jrasm91 authored and Yosi Taguri committed Oct 3, 2024
1 parent e10312b commit a254a24
Show file tree
Hide file tree
Showing 77 changed files with 800 additions and 1,860 deletions.
10 changes: 4 additions & 6 deletions server/src/services/activity.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
30 changes: 12 additions & 18 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({
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,
Expand All @@ -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,
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 @@ -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);
}
}
22 changes: 5 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,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', () => {
Expand Down
43 changes: 15 additions & 28 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 Expand Up @@ -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 });
Expand All @@ -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 || [],
Expand All @@ -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 });

Expand All @@ -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 },
);

Expand All @@ -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 },
);

Expand All @@ -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 });

Expand Down Expand Up @@ -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 });
}

Expand Down
10 changes: 4 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,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', () => {
Expand Down
30 changes: 12 additions & 18 deletions server/src/services/api-key.service.ts
Original file line number Diff line number Diff line change
@@ -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,
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
Loading

0 comments on commit a254a24

Please sign in to comment.