Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: service dependencies #13108

Merged
merged 1 commit into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading