diff --git a/apps/server/src/modules/account/repo/account.repo.integration.spec.ts b/apps/server/src/modules/account/repo/account.repo.integration.spec.ts index 70778e590a3..c8a920bb435 100644 --- a/apps/server/src/modules/account/repo/account.repo.integration.spec.ts +++ b/apps/server/src/modules/account/repo/account.repo.integration.spec.ts @@ -196,14 +196,46 @@ describe('account repo', () => { }); describe('deleteByUserId', () => { - it('should delete an account by user id', async () => { + const setup = async () => { const user = userFactory.buildWithId(); + const userWithoutAccount = userFactory.buildWithId(); const account = accountFactory.build({ userId: user.id }); + await em.persistAndFlush([user, account]); - await expect(repo.deleteByUserId(user.id)).resolves.not.toThrow(); + return { + account, + user, + userWithoutAccount, + }; + }; - await expect(repo.findById(account.id)).rejects.toThrow(NotFoundError); + describe('when user has account', () => { + it('should delete an account by user id', async () => { + const { account, user } = await setup(); + + await expect(repo.deleteByUserId(user.id)).resolves.not.toThrow(); + + await expect(repo.findById(account.id)).rejects.toThrow(NotFoundError); + }); + + it('should return accoountId', async () => { + const { account, user } = await setup(); + + const result = await repo.deleteByUserId(user.id); + + expect(result).toEqual([account.id]); + }); + }); + + describe('when user has no account', () => { + it('should return null', async () => { + const { userWithoutAccount } = await setup(); + + const result = await repo.deleteByUserId(userWithoutAccount.id); + + expect(result).toEqual([]); + }); }); }); diff --git a/apps/server/src/modules/account/repo/account.repo.ts b/apps/server/src/modules/account/repo/account.repo.ts index d9c07297e8d..5529dc98391 100644 --- a/apps/server/src/modules/account/repo/account.repo.ts +++ b/apps/server/src/modules/account/repo/account.repo.ts @@ -61,11 +61,14 @@ export class AccountRepo extends BaseRepo { return this.delete(account); } - async deleteByUserId(userId: EntityId): Promise { + async deleteByUserId(userId: EntityId): Promise { const account = await this.findByUserId(userId); - if (account) { - await this._em.removeAndFlush(account); + if (account === null) { + return []; } + await this._em.removeAndFlush(account); + + return [account.id]; } /** diff --git a/apps/server/src/modules/account/services/account-db.service.ts b/apps/server/src/modules/account/services/account-db.service.ts index 7ace1cd0655..7b25821b5a6 100644 --- a/apps/server/src/modules/account/services/account-db.service.ts +++ b/apps/server/src/modules/account/services/account-db.service.ts @@ -111,7 +111,7 @@ export class AccountServiceDb extends AbstractAccountService { return this.accountRepo.deleteById(internalId); } - async deleteByUserId(userId: EntityId): Promise { + async deleteByUserId(userId: EntityId): Promise { return this.accountRepo.deleteByUserId(userId); } diff --git a/apps/server/src/modules/account/services/account-idm.service.ts b/apps/server/src/modules/account/services/account-idm.service.ts index 65368599019..83194d648f3 100644 --- a/apps/server/src/modules/account/services/account-idm.service.ts +++ b/apps/server/src/modules/account/services/account-idm.service.ts @@ -152,9 +152,11 @@ export class AccountServiceIdm extends AbstractAccountService { await this.identityManager.deleteAccountById(id); } - async deleteByUserId(userId: EntityId): Promise { + async deleteByUserId(userId: EntityId): Promise { const idmAccount = await this.identityManager.findAccountByDbcUserId(userId); - await this.identityManager.deleteAccountById(idmAccount.id); + const deletedAccountId = await this.identityManager.deleteAccountById(idmAccount.id); + + return [deletedAccountId]; } // eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars diff --git a/apps/server/src/modules/account/services/account.service.abstract.ts b/apps/server/src/modules/account/services/account.service.abstract.ts index d027c30e3da..1a313b04568 100644 --- a/apps/server/src/modules/account/services/account.service.abstract.ts +++ b/apps/server/src/modules/account/services/account.service.abstract.ts @@ -26,7 +26,7 @@ export abstract class AbstractAccountService { abstract delete(id: EntityId): Promise; - abstract deleteByUserId(userId: EntityId): Promise; + abstract deleteByUserId(userId: EntityId): Promise; abstract searchByUsernamePartialMatch(userName: string, skip: number, limit: number): Promise>; diff --git a/apps/server/src/modules/account/services/account.service.spec.ts b/apps/server/src/modules/account/services/account.service.spec.ts index 1d7c7d43398..5f35f1b6769 100644 --- a/apps/server/src/modules/account/services/account.service.spec.ts +++ b/apps/server/src/modules/account/services/account.service.spec.ts @@ -2,6 +2,9 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ServerConfig } from '@modules/server'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; +import { ObjectId } from 'bson'; +import { DomainOperationBuilder } from '@shared/domain/builder'; +import { DomainName, OperationType } from '@shared/domain/types'; import { LegacyLogger } from '../../../core/logger'; import { AccountServiceDb } from './account-db.service'; import { AccountServiceIdm } from './account-idm.service'; @@ -289,6 +292,37 @@ describe('AccountService', () => { }); }); + describe('deleteAccountByUserId', () => { + const setup = () => { + const userId = new ObjectId().toHexString(); + const accountId = new ObjectId().toHexString(); + const spy = jest.spyOn(accountService, 'deleteByUserId'); + + const expectedResult = DomainOperationBuilder.build(DomainName.ACCOUNT, OperationType.DELETE, 1, [accountId]); + + return { accountId, expectedResult, spy, userId }; + }; + + it('should call deleteByUserId in accountService', async () => { + const { spy, userId } = setup(); + + await accountService.deleteAccountByUserId(userId); + expect(spy).toHaveBeenCalledWith(userId); + spy.mockRestore(); + }); + + it('should call deleteByUserId in accountService', async () => { + const { accountId, expectedResult, spy, userId } = setup(); + + spy.mockResolvedValueOnce([accountId]); + + const result = await accountService.deleteAccountByUserId(userId); + expect(spy).toHaveBeenCalledWith(userId); + expect(result).toEqual(expectedResult); + spy.mockRestore(); + }); + }); + describe('findMany', () => { it('should call findMany in accountServiceDb', async () => { await expect(accountService.findMany()).resolves.not.toThrow(); diff --git a/apps/server/src/modules/account/services/account.service.ts b/apps/server/src/modules/account/services/account.service.ts index 2a461c32540..1f11f7c84a6 100644 --- a/apps/server/src/modules/account/services/account.service.ts +++ b/apps/server/src/modules/account/services/account.service.ts @@ -2,8 +2,10 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { ValidationError } from '@shared/common'; -import { Counted } from '@shared/domain/types'; +import { Counted, DomainName, EntityId, OperationType } from '@shared/domain/types'; import { isEmail, validateOrReject } from 'class-validator'; +import { DomainOperation } from '@shared/domain/interface'; +import { DomainOperationBuilder } from '@shared/domain/builder'; import { LegacyLogger } from '../../../core/logger'; import { ServerConfig } from '../../server/server.config'; import { AccountServiceDb } from './account-db.service'; @@ -157,13 +159,29 @@ export class AccountService extends AbstractAccountService { }); } - async deleteByUserId(userId: string): Promise { - await this.accountDb.deleteByUserId(userId); + async deleteByUserId(userId: string): Promise { + const deletedAccounts = await this.accountDb.deleteByUserId(userId); await this.executeIdmMethod(async () => { this.logger.debug(`Deleting account with userId ${userId} ...`); - await this.accountIdm.deleteByUserId(userId); + const deletedAccountIdm = await this.accountIdm.deleteByUserId(userId); + deletedAccounts.push(...deletedAccountIdm); this.logger.debug(`Deleted account with userId ${userId}`); }); + + return deletedAccounts; + } + + async deleteAccountByUserId(userId: string): Promise { + const deletedAccounts = await this.deleteByUserId(userId); + + const result = DomainOperationBuilder.build( + DomainName.ACCOUNT, + OperationType.DELETE, + deletedAccounts.length, + deletedAccounts + ); + + return result; } /** diff --git a/apps/server/src/modules/class/service/class.service.spec.ts b/apps/server/src/modules/class/service/class.service.spec.ts index fca6bb9ee0d..3ee13d98d23 100644 --- a/apps/server/src/modules/class/service/class.service.spec.ts +++ b/apps/server/src/modules/class/service/class.service.spec.ts @@ -2,9 +2,10 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { EntityId } from '@shared/domain/types'; +import { DomainName, EntityId, OperationType } from '@shared/domain/types'; import { setupEntities } from '@shared/testing'; import { Logger } from '@src/core/logger'; +import { DomainOperationBuilder } from '@shared/domain/builder'; import { Class } from '../domain'; import { classFactory } from '../domain/testing'; import { classEntityFactory } from '../entity/testing'; @@ -134,7 +135,13 @@ describe(ClassService.name, () => { classesRepo.findAllByUserId.mockResolvedValue(mappedClasses); + const expectedResult = DomainOperationBuilder.build(DomainName.CLASS, OperationType.UPDATE, 2, [ + class1.id, + class2.id, + ]); + return { + expectedResult, userId1, }; }; @@ -147,11 +154,11 @@ describe(ClassService.name, () => { }); it('should update classes without updated user', async () => { - const { userId1 } = setup(); + const { expectedResult, userId1 } = setup(); const result = await service.deleteUserDataFromClasses(userId1.toHexString()); - expect(result).toEqual(2); + expect(result).toEqual(expectedResult); }); }); }); diff --git a/apps/server/src/modules/class/service/class.service.ts b/apps/server/src/modules/class/service/class.service.ts index 81d62253a79..4cdc9f95864 100644 --- a/apps/server/src/modules/class/service/class.service.ts +++ b/apps/server/src/modules/class/service/class.service.ts @@ -1,7 +1,9 @@ import { Injectable, InternalServerErrorException } from '@nestjs/common'; -import { DomainModel, EntityId, StatusModel } from '@shared/domain/types'; +import { DomainName, EntityId, OperationType, StatusModel } from '@shared/domain/types'; import { Logger } from '@src/core/logger'; import { DataDeletionDomainOperationLoggable } from '@shared/common/loggable'; +import { DomainOperationBuilder } from '@shared/domain/builder'; +import { DomainOperation } from '@shared/domain/interface'; import { Class } from '../domain'; import { ClassesRepo } from '../repo'; @@ -23,11 +25,11 @@ export class ClassService { return classes; } - public async deleteUserDataFromClasses(userId: EntityId): Promise { + public async deleteUserDataFromClasses(userId: EntityId): Promise { this.logger.info( new DataDeletionDomainOperationLoggable( 'Deleting data from Classes', - DomainModel.CLASS, + DomainName.CLASS, userId, StatusModel.PENDING ) @@ -49,10 +51,18 @@ export class ClassService { const numberOfUpdatedClasses = updatedClasses.length; await this.classesRepo.updateMany(updatedClasses); + + const result = DomainOperationBuilder.build( + DomainName.CLASS, + OperationType.UPDATE, + numberOfUpdatedClasses, + this.getClassesId(updatedClasses) + ); + this.logger.info( new DataDeletionDomainOperationLoggable( 'Successfully removed user data from Classes', - DomainModel.CLASS, + DomainName.CLASS, userId, StatusModel.FINISHED, numberOfUpdatedClasses, @@ -60,6 +70,10 @@ export class ClassService { ) ); - return numberOfUpdatedClasses; + return result; + } + + private getClassesId(classes: Class[]): EntityId[] { + return classes.map((item) => item.id); } } diff --git a/apps/server/src/modules/deletion/builder/deletion-log-statistic.builder.spec.ts b/apps/server/src/modules/deletion/builder/deletion-log-statistic.builder.spec.ts index a8a998e7a4e..54352ce9ad2 100644 --- a/apps/server/src/modules/deletion/builder/deletion-log-statistic.builder.spec.ts +++ b/apps/server/src/modules/deletion/builder/deletion-log-statistic.builder.spec.ts @@ -1,22 +1,28 @@ -import { DomainModel } from '@shared/domain/types'; +import { DomainName, OperationType } from '@shared/domain/types'; +import { ObjectId } from 'bson'; import { DeletionLogStatisticBuilder } from '.'; describe(DeletionLogStatisticBuilder.name, () => { afterAll(() => { jest.clearAllMocks(); }); + const setup = () => { + const domain = DomainName.PSEUDONYMS; + const operation = OperationType.DELETE; + const count = 2; + const refs = [new ObjectId().toHexString(), new ObjectId().toHexString()]; + + return { domain, operation, count, refs }; + }; it('should build generic deletionLogStatistic with all attributes', () => { - // Arrange - const domain = DomainModel.PSEUDONYMS; - const modifiedCount = 0; - const deletedCount = 2; + const { domain, operation, count, refs } = setup(); - const result = DeletionLogStatisticBuilder.build(domain, modifiedCount, deletedCount); + const result = DeletionLogStatisticBuilder.build(domain, operation, count, refs); - // Assert expect(result.domain).toEqual(domain); - expect(result.modifiedCount).toEqual(modifiedCount); - expect(result.deletedCount).toEqual(deletedCount); + expect(result.operation).toEqual(operation); + expect(result.count).toEqual(count); + expect(result.refs).toEqual(refs); }); }); diff --git a/apps/server/src/modules/deletion/builder/deletion-log-statistic.builder.ts b/apps/server/src/modules/deletion/builder/deletion-log-statistic.builder.ts index fa0680b8500..b8cf0af6a58 100644 --- a/apps/server/src/modules/deletion/builder/deletion-log-statistic.builder.ts +++ b/apps/server/src/modules/deletion/builder/deletion-log-statistic.builder.ts @@ -1,15 +1,9 @@ import { DomainOperation } from '@shared/domain/interface'; -import { DomainModel } from '@shared/domain/types'; +import { DomainName, EntityId, OperationType } from '@shared/domain/types'; export class DeletionLogStatisticBuilder { - static build( - domain: DomainModel, - modifiedCount: number, - deletedCount: number, - modifiedRef?: string[], - deletedRef?: string[] - ): DomainOperation { - const deletionLogStatistic = { domain, modifiedCount, deletedCount, modifiedRef, deletedRef }; + static build(domain: DomainName, operation: OperationType, count: number, refs: EntityId[]): DomainOperation { + const deletionLogStatistic = { domain, operation, count, refs }; return deletionLogStatistic; } diff --git a/apps/server/src/modules/deletion/builder/deletion-request-body-props.builder.spec.ts b/apps/server/src/modules/deletion/builder/deletion-request-body-props.builder.spec.ts index dcde2f6adb3..c82c82a5df2 100644 --- a/apps/server/src/modules/deletion/builder/deletion-request-body-props.builder.spec.ts +++ b/apps/server/src/modules/deletion/builder/deletion-request-body-props.builder.spec.ts @@ -1,5 +1,5 @@ import { ObjectId } from 'bson'; -import { DomainModel } from '@shared/domain/types'; +import { DomainName } from '@shared/domain/types'; import { DeletionRequestBodyPropsBuilder } from './deletion-request-body-props.builder'; describe(DeletionRequestBodyPropsBuilder.name, () => { @@ -8,7 +8,7 @@ describe(DeletionRequestBodyPropsBuilder.name, () => { }); describe('when create deletionRequestBodyParams', () => { const setup = () => { - const domain = DomainModel.PSEUDONYMS; + const domain = DomainName.PSEUDONYMS; const refId = new ObjectId().toHexString(); const deleteInMinutes = 1000; return { domain, refId, deleteInMinutes }; diff --git a/apps/server/src/modules/deletion/builder/deletion-request-body-props.builder.ts b/apps/server/src/modules/deletion/builder/deletion-request-body-props.builder.ts index 21e00fb7ab0..3452472f4bd 100644 --- a/apps/server/src/modules/deletion/builder/deletion-request-body-props.builder.ts +++ b/apps/server/src/modules/deletion/builder/deletion-request-body-props.builder.ts @@ -1,8 +1,8 @@ -import { DomainModel, EntityId } from '@shared/domain/types'; +import { DomainName, EntityId } from '@shared/domain/types'; import { DeletionRequestBodyProps } from '../controller/dto'; export class DeletionRequestBodyPropsBuilder { - static build(domain: DomainModel, id: EntityId, deleteInMinutes?: number): DeletionRequestBodyProps { + static build(domain: DomainName, id: EntityId, deleteInMinutes?: number): DeletionRequestBodyProps { const deletionRequestItem = { targetRef: { domain, id }, deleteInMinutes, diff --git a/apps/server/src/modules/deletion/builder/deletion-request-log-response.builder.spec.ts b/apps/server/src/modules/deletion/builder/deletion-request-log-response.builder.spec.ts index e057fb0d9e6..62426f890c0 100644 --- a/apps/server/src/modules/deletion/builder/deletion-request-log-response.builder.spec.ts +++ b/apps/server/src/modules/deletion/builder/deletion-request-log-response.builder.spec.ts @@ -1,4 +1,5 @@ -import { DomainModel } from '@shared/domain/types'; +import { DomainName, OperationType } from '@shared/domain/types'; +import { ObjectId } from 'bson'; import { DeletionLogStatisticBuilder, DeletionRequestLogResponseBuilder, DeletionTargetRefBuilder } from '.'; import { DeletionStatusModel } from '../domain/types'; @@ -6,21 +7,25 @@ describe(DeletionRequestLogResponseBuilder, () => { afterAll(() => { jest.clearAllMocks(); }); - - it('should build generic deletionRequestLog with all attributes', () => { - // Arrange - const targetRefDomain = DomainModel.PSEUDONYMS; + const setup = () => { + const targetRefDomain = DomainName.PSEUDONYMS; const targetRefId = '653e4833cc39e5907a1e18d2'; const targetRef = DeletionTargetRefBuilder.build(targetRefDomain, targetRefId); const deletionPlannedAt = new Date(); const status = DeletionStatusModel.SUCCESS; - const modifiedCount = 0; - const deletedCount = 2; - const statistics = [DeletionLogStatisticBuilder.build(targetRefDomain, modifiedCount, deletedCount)]; + const operation = OperationType.DELETE; + const count = 2; + const refs = [new ObjectId().toHexString(), new ObjectId().toHexString()]; + const statistics = [DeletionLogStatisticBuilder.build(targetRefDomain, operation, count, refs)]; + + return { deletionPlannedAt, statistics, status, targetRef }; + }; + + it('should build generic deletionRequestLog with all attributes', () => { + const { deletionPlannedAt, statistics, status, targetRef } = setup(); const result = DeletionRequestLogResponseBuilder.build(targetRef, deletionPlannedAt, status, statistics); - // Assert expect(result.targetRef).toEqual(targetRef); expect(result.deletionPlannedAt).toEqual(deletionPlannedAt); expect(result.status).toEqual(status); diff --git a/apps/server/src/modules/deletion/builder/deletion-target-ref.builder.spec.ts b/apps/server/src/modules/deletion/builder/deletion-target-ref.builder.spec.ts index 762518d17bd..533ef311200 100644 --- a/apps/server/src/modules/deletion/builder/deletion-target-ref.builder.spec.ts +++ b/apps/server/src/modules/deletion/builder/deletion-target-ref.builder.spec.ts @@ -1,4 +1,4 @@ -import { DomainModel } from '@shared/domain/types'; +import { DomainName } from '@shared/domain/types'; import { DeletionTargetRefBuilder } from './index'; describe(DeletionTargetRefBuilder.name, () => { @@ -8,7 +8,7 @@ describe(DeletionTargetRefBuilder.name, () => { it('should build generic deletionTargetRef with all attributes', () => { // Arrange - const domain = DomainModel.PSEUDONYMS; + const domain = DomainName.PSEUDONYMS; const refId = '653e4833cc39e5907a1e18d2'; const result = DeletionTargetRefBuilder.build(domain, refId); diff --git a/apps/server/src/modules/deletion/builder/deletion-target-ref.builder.ts b/apps/server/src/modules/deletion/builder/deletion-target-ref.builder.ts index 1d1cee14a04..cbad86bae11 100644 --- a/apps/server/src/modules/deletion/builder/deletion-target-ref.builder.ts +++ b/apps/server/src/modules/deletion/builder/deletion-target-ref.builder.ts @@ -1,8 +1,8 @@ -import { DomainModel, EntityId } from '@shared/domain/types'; +import { DomainName, EntityId } from '@shared/domain/types'; import { DeletionTargetRef } from '../interface'; export class DeletionTargetRefBuilder { - static build(domain: DomainModel, id: EntityId): DeletionTargetRef { + static build(domain: DomainName, id: EntityId): DeletionTargetRef { const deletionTargetRef = { domain, id }; return deletionTargetRef; diff --git a/apps/server/src/modules/deletion/controller/api-test/deletion-request-create.api.spec.ts b/apps/server/src/modules/deletion/controller/api-test/deletion-request-create.api.spec.ts index 22814bf8590..c6a74f0b11e 100644 --- a/apps/server/src/modules/deletion/controller/api-test/deletion-request-create.api.spec.ts +++ b/apps/server/src/modules/deletion/controller/api-test/deletion-request-create.api.spec.ts @@ -5,7 +5,7 @@ import { AuthGuard } from '@nestjs/passport'; import { EntityManager } from '@mikro-orm/mongodb'; import { TestXApiKeyClient } from '@shared/testing'; import { AdminApiServerTestModule } from '@modules/server/admin-api.server.module'; -import { DomainModel } from '@shared/domain/types'; +import { DomainName } from '@shared/domain/types'; import { DeletionRequestBodyProps, DeletionRequestResponse } from '../dto'; import { DeletionRequestEntity } from '../../entity'; @@ -86,14 +86,14 @@ describe(`deletionRequest create (api)`, () => { const setup = () => { const deletionRequestToCreate: DeletionRequestBodyProps = { targetRef: { - domain: DomainModel.USER, + domain: DomainName.USER, id: '653e4833cc39e5907a1e18d2', }, }; const deletionRequestToImmediateRemoval: DeletionRequestBodyProps = { targetRef: { - domain: DomainModel.USER, + domain: DomainName.USER, id: '653e4833cc39e5907a1e18d2', }, deleteInMinutes: 0, diff --git a/apps/server/src/modules/deletion/controller/dto/deletion-request-log.response.spec.ts b/apps/server/src/modules/deletion/controller/dto/deletion-request-log.response.spec.ts index 88a649fcf91..61fb0c52089 100644 --- a/apps/server/src/modules/deletion/controller/dto/deletion-request-log.response.spec.ts +++ b/apps/server/src/modules/deletion/controller/dto/deletion-request-log.response.spec.ts @@ -1,5 +1,5 @@ import { ObjectId } from 'bson'; -import { DomainModel } from '@shared/domain/types'; +import { DomainName, OperationType } from '@shared/domain/types'; import { DeletionLogStatisticBuilder, DeletionTargetRefBuilder } from '../../builder'; import { DeletionRequestLogResponse } from './index'; import { DeletionStatusModel } from '../../domain/types'; @@ -8,14 +8,15 @@ describe(DeletionRequestLogResponse.name, () => { describe('constructor', () => { describe('when passed properties', () => { const setup = () => { - const targetRefDomain = DomainModel.PSEUDONYMS; + const targetRefDomain = DomainName.PSEUDONYMS; const targetRefId = new ObjectId().toHexString(); const targetRef = DeletionTargetRefBuilder.build(targetRefDomain, targetRefId); const status = DeletionStatusModel.SUCCESS; const deletionPlannedAt = new Date(); - const modifiedCount = 0; - const deletedCount = 2; - const statistics = [DeletionLogStatisticBuilder.build(targetRefDomain, modifiedCount, deletedCount)]; + const operation = OperationType.DELETE; + const count = 2; + const refs = [new ObjectId().toHexString(), new ObjectId().toHexString()]; + const statistics = [DeletionLogStatisticBuilder.build(targetRefDomain, operation, count, refs)]; return { targetRef, deletionPlannedAt, status, statistics }; }; diff --git a/apps/server/src/modules/deletion/domain/deletion-log.do.spec.ts b/apps/server/src/modules/deletion/domain/deletion-log.do.spec.ts index 10040956627..37b3ea19bae 100644 --- a/apps/server/src/modules/deletion/domain/deletion-log.do.spec.ts +++ b/apps/server/src/modules/deletion/domain/deletion-log.do.spec.ts @@ -1,8 +1,7 @@ import { ObjectId } from '@mikro-orm/mongodb'; -import { DomainModel } from '@shared/domain/types'; +import { DomainName, OperationType } from '@shared/domain/types'; import { deletionLogFactory } from './testing/factory/deletion-log.factory'; import { DeletionLog } from './deletion-log.do'; -import { DeletionOperationModel } from './types'; describe(DeletionLog.name, () => { describe('constructor', () => { @@ -36,10 +35,10 @@ describe(DeletionLog.name, () => { const setup = () => { const props = { id: new ObjectId().toHexString(), - domain: DomainModel.USER, - operation: DeletionOperationModel.DELETE, - modifiedCount: 0, - deletedCount: 1, + domain: DomainName.USER, + operation: OperationType.DELETE, + count: 1, + refs: [new ObjectId().toHexString()], deletionRequestId: new ObjectId().toHexString(), performedAt: new Date(), createdAt: new Date(), @@ -57,8 +56,8 @@ describe(DeletionLog.name, () => { id: deletionLogDo.id, domain: deletionLogDo.domain, operation: deletionLogDo.operation, - modifiedCount: deletionLogDo.modifiedCount, - deletedCount: deletionLogDo.deletedCount, + count: deletionLogDo.count, + refs: deletionLogDo.refs, deletionRequestId: deletionLogDo.deletionRequestId, performedAt: deletionLogDo.performedAt, createdAt: deletionLogDo.createdAt, diff --git a/apps/server/src/modules/deletion/domain/deletion-log.do.ts b/apps/server/src/modules/deletion/domain/deletion-log.do.ts index 81a9c7f41cb..ca4460d2885 100644 --- a/apps/server/src/modules/deletion/domain/deletion-log.do.ts +++ b/apps/server/src/modules/deletion/domain/deletion-log.do.ts @@ -1,14 +1,13 @@ -import { DomainModel, EntityId } from '@shared/domain/types'; +import { DomainName, EntityId, OperationType } from '@shared/domain/types'; import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; -import { DeletionOperationModel } from './types'; export interface DeletionLogProps extends AuthorizableObject { createdAt?: Date; updatedAt?: Date; - domain: DomainModel; - operation?: DeletionOperationModel; - modifiedCount: number; - deletedCount: number; + domain: DomainName; + operation: OperationType; + count: number; + refs: string[]; deletionRequestId?: EntityId; performedAt?: Date; } @@ -22,20 +21,20 @@ export class DeletionLog extends DomainObject { return this.props.updatedAt; } - get domain(): DomainModel { + get domain(): DomainName { return this.props.domain; } - get operation(): DeletionOperationModel | undefined { + get operation(): OperationType { return this.props.operation; } - get modifiedCount(): number { - return this.props.modifiedCount; + get count(): number { + return this.props.count; } - get deletedCount(): number { - return this.props.deletedCount; + get refs(): string[] { + return this.props.refs; } get deletionRequestId(): EntityId | undefined { diff --git a/apps/server/src/modules/deletion/domain/deletion-request.do.spec.ts b/apps/server/src/modules/deletion/domain/deletion-request.do.spec.ts index 2650e894fdc..8655c897fe7 100644 --- a/apps/server/src/modules/deletion/domain/deletion-request.do.spec.ts +++ b/apps/server/src/modules/deletion/domain/deletion-request.do.spec.ts @@ -1,5 +1,5 @@ import { ObjectId } from '@mikro-orm/mongodb'; -import { DomainModel } from '@shared/domain/types'; +import { DomainName } from '@shared/domain/types'; import { DeletionRequest } from './deletion-request.do'; import { DeletionStatusModel } from './types'; import { deletionRequestFactory } from './testing/factory/deletion-request.factory'; @@ -36,7 +36,7 @@ describe(DeletionRequest.name, () => { const setup = () => { const props = { id: new ObjectId().toHexString(), - targetRefDomain: DomainModel.USER, + targetRefDomain: DomainName.USER, deleteAfter: new Date(), targetRefId: new ObjectId().toHexString(), status: DeletionStatusModel.REGISTERED, diff --git a/apps/server/src/modules/deletion/domain/deletion-request.do.ts b/apps/server/src/modules/deletion/domain/deletion-request.do.ts index 92010ef2570..d4aa7d0f657 100644 --- a/apps/server/src/modules/deletion/domain/deletion-request.do.ts +++ b/apps/server/src/modules/deletion/domain/deletion-request.do.ts @@ -1,11 +1,11 @@ -import { DomainModel, EntityId } from '@shared/domain/types'; +import { DomainName, EntityId } from '@shared/domain/types'; import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; import { DeletionStatusModel } from './types'; export interface DeletionRequestProps extends AuthorizableObject { createdAt?: Date; updatedAt?: Date; - targetRefDomain: DomainModel; + targetRefDomain: DomainName; deleteAfter: Date; targetRefId: EntityId; status: DeletionStatusModel; @@ -20,7 +20,7 @@ export class DeletionRequest extends DomainObject { return this.props.updatedAt; } - get targetRefDomain(): DomainModel { + get targetRefDomain(): DomainName { return this.props.targetRefDomain; } diff --git a/apps/server/src/modules/deletion/domain/testing/factory/deletion-log.factory.ts b/apps/server/src/modules/deletion/domain/testing/factory/deletion-log.factory.ts index 2593ba5c242..aa8a9981c9c 100644 --- a/apps/server/src/modules/deletion/domain/testing/factory/deletion-log.factory.ts +++ b/apps/server/src/modules/deletion/domain/testing/factory/deletion-log.factory.ts @@ -1,16 +1,15 @@ import { DoBaseFactory } from '@shared/testing'; import { ObjectId } from '@mikro-orm/mongodb'; -import { DomainModel } from '@shared/domain/types'; +import { DomainName, OperationType } from '@shared/domain/types'; import { DeletionLog, DeletionLogProps } from '../../deletion-log.do'; -import { DeletionOperationModel } from '../../types'; export const deletionLogFactory = DoBaseFactory.define(DeletionLog, () => { return { id: new ObjectId().toHexString(), - domain: DomainModel.USER, - operation: DeletionOperationModel.DELETE, - modifiedCount: 0, - deletedCount: 1, + domain: DomainName.USER, + operation: OperationType.DELETE, + count: 1, + refs: [new ObjectId().toHexString()], deletionRequestId: new ObjectId().toHexString(), performedAt: new Date(), createdAt: new Date(), diff --git a/apps/server/src/modules/deletion/domain/testing/factory/deletion-request.factory.ts b/apps/server/src/modules/deletion/domain/testing/factory/deletion-request.factory.ts index e0ae6e7e41b..544ece5ad2f 100644 --- a/apps/server/src/modules/deletion/domain/testing/factory/deletion-request.factory.ts +++ b/apps/server/src/modules/deletion/domain/testing/factory/deletion-request.factory.ts @@ -1,7 +1,7 @@ import { DoBaseFactory } from '@shared/testing'; import { ObjectId } from '@mikro-orm/mongodb'; import { DeepPartial } from 'fishery'; -import { DomainModel } from '@shared/domain/types'; +import { DomainName } from '@shared/domain/types'; import { DeletionRequest, DeletionRequestProps } from '../../deletion-request.do'; import { DeletionStatusModel } from '../../types'; @@ -18,7 +18,7 @@ class DeletionRequestFactory extends DoBaseFactory { return { id: new ObjectId().toHexString(), - targetRefDomain: DomainModel.USER, + targetRefDomain: DomainName.USER, deleteAfter: new Date(), targetRefId: new ObjectId().toHexString(), status: DeletionStatusModel.REGISTERED, diff --git a/apps/server/src/modules/deletion/entity/deletion-log.entity.spec.ts b/apps/server/src/modules/deletion/entity/deletion-log.entity.spec.ts index a0524a30a52..843a1f01535 100644 --- a/apps/server/src/modules/deletion/entity/deletion-log.entity.spec.ts +++ b/apps/server/src/modules/deletion/entity/deletion-log.entity.spec.ts @@ -1,8 +1,7 @@ import { setupEntities } from '@shared/testing'; import { ObjectId } from '@mikro-orm/mongodb'; -import { DomainModel } from '@shared/domain/types'; +import { DomainName, OperationType } from '@shared/domain/types'; import { DeletionLogEntity } from './deletion-log.entity'; -import { DeletionOperationModel } from '../domain/types'; describe(DeletionLogEntity.name, () => { beforeAll(async () => { @@ -14,10 +13,10 @@ describe(DeletionLogEntity.name, () => { const setup = () => { const props = { id: new ObjectId().toHexString(), - domain: DomainModel.USER, - operation: DeletionOperationModel.DELETE, - modifiedCount: 0, - deletedCount: 1, + domain: DomainName.USER, + operation: OperationType.DELETE, + count: 1, + refs: [new ObjectId().toHexString()], deletionRequestId: new ObjectId(), performedAt: new Date(), createdAt: new Date(), @@ -47,8 +46,8 @@ describe(DeletionLogEntity.name, () => { id: entity.id, domain: entity.domain, operation: entity.operation, - modifiedCount: entity.modifiedCount, - deletedCount: entity.deletedCount, + count: entity.count, + refs: entity.refs, deletionRequestId: entity.deletionRequestId, performedAt: entity.performedAt, createdAt: entity.createdAt, diff --git a/apps/server/src/modules/deletion/entity/deletion-log.entity.ts b/apps/server/src/modules/deletion/entity/deletion-log.entity.ts index 03dfaf5123e..bff666c9b07 100644 --- a/apps/server/src/modules/deletion/entity/deletion-log.entity.ts +++ b/apps/server/src/modules/deletion/entity/deletion-log.entity.ts @@ -1,15 +1,14 @@ import { Entity, Index, Property } from '@mikro-orm/core'; import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; -import { DomainModel, EntityId } from '@shared/domain/types'; +import { DomainName, EntityId, OperationType } from '@shared/domain/types'; import { ObjectId } from 'bson'; -import { DeletionOperationModel } from '../domain/types'; export interface DeletionLogEntityProps { id?: EntityId; - domain: DomainModel; - operation?: DeletionOperationModel; - modifiedCount: number; - deletedCount: number; + domain: DomainName; + operation: OperationType; + count: number; + refs: EntityId[]; deletionRequestId?: ObjectId; performedAt?: Date; createdAt?: Date; @@ -19,16 +18,16 @@ export interface DeletionLogEntityProps { @Entity({ tableName: 'deletionlogs' }) export class DeletionLogEntity extends BaseEntityWithTimestamps { @Property() - domain: DomainModel; + domain: DomainName; - @Property({ nullable: true }) - operation?: DeletionOperationModel; + @Property() + operation: OperationType; @Property() - modifiedCount: number; + count: number; @Property() - deletedCount: number; + refs: EntityId[]; @Property({ nullable: true }) deletionRequestId?: ObjectId; @@ -44,12 +43,9 @@ export class DeletionLogEntity extends BaseEntityWithTimestamps { } this.domain = props.domain; - - if (props.operation !== undefined) { - this.operation = props.operation; - } - this.modifiedCount = props.modifiedCount; - this.deletedCount = props.deletedCount; + this.operation = props.operation; + this.count = props.count; + this.refs = props.refs; if (props.deletionRequestId !== undefined) { this.deletionRequestId = props.deletionRequestId; diff --git a/apps/server/src/modules/deletion/entity/deletion-request.entity.spec.ts b/apps/server/src/modules/deletion/entity/deletion-request.entity.spec.ts index 273679ef671..ad6330e074f 100644 --- a/apps/server/src/modules/deletion/entity/deletion-request.entity.spec.ts +++ b/apps/server/src/modules/deletion/entity/deletion-request.entity.spec.ts @@ -1,6 +1,6 @@ import { setupEntities } from '@shared/testing'; import { ObjectId } from '@mikro-orm/mongodb'; -import { DomainModel } from '@shared/domain/types'; +import { DomainName } from '@shared/domain/types'; import { DeletionStatusModel } from '../domain/types'; import { DeletionRequestEntity } from '.'; @@ -16,7 +16,7 @@ describe(DeletionRequestEntity.name, () => { const setup = () => { const props = { id: new ObjectId().toHexString(), - targetRefDomain: DomainModel.USER, + targetRefDomain: DomainName.USER, deleteAfter: new Date(), targetRefId: new ObjectId().toHexString(), status: DeletionStatusModel.REGISTERED, diff --git a/apps/server/src/modules/deletion/entity/deletion-request.entity.ts b/apps/server/src/modules/deletion/entity/deletion-request.entity.ts index b81835641a9..0110fc64a20 100644 --- a/apps/server/src/modules/deletion/entity/deletion-request.entity.ts +++ b/apps/server/src/modules/deletion/entity/deletion-request.entity.ts @@ -1,12 +1,12 @@ import { Entity, Index, Property, Unique } from '@mikro-orm/core'; import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; -import { DomainModel, EntityId } from '@shared/domain/types'; +import { DomainName, EntityId } from '@shared/domain/types'; import { DeletionStatusModel } from '../domain/types'; const SECONDS_OF_90_DAYS = 90 * 24 * 60 * 60; export interface DeletionRequestEntityProps { id?: EntityId; - targetRefDomain: DomainModel; + targetRefDomain: DomainName; deleteAfter: Date; targetRefId: EntityId; status: DeletionStatusModel; @@ -25,7 +25,7 @@ export class DeletionRequestEntity extends BaseEntityWithTimestamps { targetRefId!: EntityId; @Property() - targetRefDomain: DomainModel; + targetRefDomain: DomainName; @Property() status: DeletionStatusModel; diff --git a/apps/server/src/modules/deletion/entity/testing/factory/deletion-log.entity.factory.ts b/apps/server/src/modules/deletion/entity/testing/factory/deletion-log.entity.factory.ts index 6090f14402d..52f0acba755 100644 --- a/apps/server/src/modules/deletion/entity/testing/factory/deletion-log.entity.factory.ts +++ b/apps/server/src/modules/deletion/entity/testing/factory/deletion-log.entity.factory.ts @@ -1,18 +1,17 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { BaseFactory } from '@shared/testing'; -import { DomainModel } from '@shared/domain/types'; +import { DomainName, OperationType } from '@shared/domain/types'; import { DeletionLogEntity, DeletionLogEntityProps } from '../../deletion-log.entity'; -import { DeletionOperationModel } from '../../../domain/types'; export const deletionLogEntityFactory = BaseFactory.define( DeletionLogEntity, () => { return { id: new ObjectId().toHexString(), - domain: DomainModel.USER, - operation: DeletionOperationModel.DELETE, - modifiedCount: 0, - deletedCount: 1, + domain: DomainName.USER, + operation: OperationType.DELETE, + count: 1, + refs: [new ObjectId().toHexString()], deletionRequestId: new ObjectId(), createdAt: new Date(), updatedAt: new Date(), diff --git a/apps/server/src/modules/deletion/entity/testing/factory/deletion-request.entity.factory.ts b/apps/server/src/modules/deletion/entity/testing/factory/deletion-request.entity.factory.ts index 8f33e0d66d6..bab48f7c6b2 100644 --- a/apps/server/src/modules/deletion/entity/testing/factory/deletion-request.entity.factory.ts +++ b/apps/server/src/modules/deletion/entity/testing/factory/deletion-request.entity.factory.ts @@ -1,6 +1,6 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { BaseFactory } from '@shared/testing'; -import { DomainModel } from '@shared/domain/types'; +import { DomainName } from '@shared/domain/types'; import { DeletionStatusModel } from '../../../domain/types'; import { DeletionRequestEntity, DeletionRequestEntityProps } from '../../deletion-request.entity'; @@ -9,7 +9,7 @@ export const deletionRequestEntityFactory = BaseFactory.define { return { id: new ObjectId().toHexString(), - targetRefDomain: DomainModel.USER, + targetRefDomain: DomainName.USER, deleteAfter: new Date(), targetRefId: new ObjectId().toHexString(), status: DeletionStatusModel.REGISTERED, diff --git a/apps/server/src/modules/deletion/interface/interfaces.ts b/apps/server/src/modules/deletion/interface/interfaces.ts index 7a2621d87b7..e981638baa5 100644 --- a/apps/server/src/modules/deletion/interface/interfaces.ts +++ b/apps/server/src/modules/deletion/interface/interfaces.ts @@ -1,6 +1,6 @@ -import { DomainModel, EntityId } from '@shared/domain/types'; +import { DomainName, EntityId } from '@shared/domain/types'; export interface DeletionTargetRef { - domain: DomainModel; + domain: DomainName; id: EntityId; } diff --git a/apps/server/src/modules/deletion/repo/deletion-log.repo.spec.ts b/apps/server/src/modules/deletion/repo/deletion-log.repo.spec.ts index 1ab1e515acc..c5716f5e5c7 100644 --- a/apps/server/src/modules/deletion/repo/deletion-log.repo.spec.ts +++ b/apps/server/src/modules/deletion/repo/deletion-log.repo.spec.ts @@ -61,8 +61,8 @@ describe(DeletionLogRepo.name, () => { id: domainObject.id, domain: domainObject.domain, operation: domainObject.operation, - modifiedCount: domainObject.modifiedCount, - deletedCount: domainObject.deletedCount, + count: domainObject.count, + refs: domainObject.refs, deletionRequestId: domainObject.deletionRequestId, performedAt: domainObject.performedAt, createdAt: domainObject.createdAt, @@ -93,8 +93,8 @@ describe(DeletionLogRepo.name, () => { id: entity.id, domain: entity.domain, operation: entity.operation, - modifiedCount: entity.modifiedCount, - deletedCount: entity.deletedCount, + count: entity.count, + refs: entity.refs, deletionRequestId: entity.deletionRequestId?.toHexString(), performedAt: entity.performedAt, createdAt: entity.createdAt, @@ -152,8 +152,8 @@ describe(DeletionLogRepo.name, () => { operation: deletionLogEntity1.operation, deletionRequestId: deletionLogEntity1.deletionRequestId?.toHexString(), performedAt: deletionLogEntity1.performedAt, - modifiedCount: deletionLogEntity1.modifiedCount, - deletedCount: deletionLogEntity1.deletedCount, + count: deletionLogEntity1.count, + refs: deletionLogEntity1.refs, createdAt: deletionLogEntity1.createdAt, updatedAt: deletionLogEntity1.updatedAt, }, @@ -163,8 +163,8 @@ describe(DeletionLogRepo.name, () => { operation: deletionLogEntity2.operation, deletionRequestId: deletionLogEntity2.deletionRequestId?.toHexString(), performedAt: deletionLogEntity2.performedAt, - modifiedCount: deletionLogEntity2.modifiedCount, - deletedCount: deletionLogEntity2.deletedCount, + count: deletionLogEntity2.count, + refs: deletionLogEntity2.refs, createdAt: deletionLogEntity2.createdAt, updatedAt: deletionLogEntity2.updatedAt, }, diff --git a/apps/server/src/modules/deletion/repo/mapper/deletion-log.mapper.spec.ts b/apps/server/src/modules/deletion/repo/mapper/deletion-log.mapper.spec.ts index 3937710336a..d48f2e63dcf 100644 --- a/apps/server/src/modules/deletion/repo/mapper/deletion-log.mapper.spec.ts +++ b/apps/server/src/modules/deletion/repo/mapper/deletion-log.mapper.spec.ts @@ -17,8 +17,8 @@ describe(DeletionLogMapper.name, () => { operation: entity.operation, deletionRequestId: entity.deletionRequestId?.toHexString(), performedAt: entity.performedAt, - modifiedCount: entity.modifiedCount, - deletedCount: entity.deletedCount, + count: entity.count, + refs: entity.refs, createdAt: entity.createdAt, updatedAt: entity.updatedAt, }); @@ -56,8 +56,8 @@ describe(DeletionLogMapper.name, () => { operation: entity.operation, deletionRequestId: entity.deletionRequestId?.toHexString(), performedAt: entity.performedAt, - modifiedCount: entity.modifiedCount, - deletedCount: entity.deletedCount, + count: entity.count, + refs: entity.refs, createdAt: entity.createdAt, updatedAt: entity.updatedAt, }) @@ -95,8 +95,8 @@ describe(DeletionLogMapper.name, () => { operation: domainObject.operation, deletionRequestId: new ObjectId(domainObject.deletionRequestId), performedAt: domainObject.performedAt, - modifiedCount: domainObject.modifiedCount, - deletedCount: domainObject.deletedCount, + count: domainObject.count, + refs: domainObject.refs, createdAt: domainObject.createdAt, updatedAt: domainObject.updatedAt, }); @@ -144,8 +144,8 @@ describe(DeletionLogMapper.name, () => { operation: domainObject.operation, deletionRequestId: new ObjectId(domainObject.deletionRequestId), performedAt: domainObject.performedAt, - modifiedCount: domainObject.modifiedCount, - deletedCount: domainObject.deletedCount, + count: domainObject.count, + refs: domainObject.refs, createdAt: domainObject.createdAt, updatedAt: domainObject.updatedAt, }) diff --git a/apps/server/src/modules/deletion/repo/mapper/deletion-log.mapper.ts b/apps/server/src/modules/deletion/repo/mapper/deletion-log.mapper.ts index bb10fe0ac93..f57cd2fbaeb 100644 --- a/apps/server/src/modules/deletion/repo/mapper/deletion-log.mapper.ts +++ b/apps/server/src/modules/deletion/repo/mapper/deletion-log.mapper.ts @@ -10,8 +10,8 @@ export class DeletionLogMapper { updatedAt: entity.updatedAt, domain: entity.domain, operation: entity.operation, - modifiedCount: entity.modifiedCount, - deletedCount: entity.deletedCount, + count: entity.count, + refs: entity.refs, deletionRequestId: entity.deletionRequestId?.toHexString(), performedAt: entity.performedAt, }); @@ -24,8 +24,8 @@ export class DeletionLogMapper { updatedAt: domainObject.updatedAt, domain: domainObject.domain, operation: domainObject.operation, - modifiedCount: domainObject.modifiedCount, - deletedCount: domainObject.deletedCount, + count: domainObject.count, + refs: domainObject.refs, deletionRequestId: new ObjectId(domainObject.deletionRequestId), performedAt: domainObject.performedAt, }); diff --git a/apps/server/src/modules/deletion/services/deletion-log.service.spec.ts b/apps/server/src/modules/deletion/services/deletion-log.service.spec.ts index e453ab24419..2716f4cd8f1 100644 --- a/apps/server/src/modules/deletion/services/deletion-log.service.spec.ts +++ b/apps/server/src/modules/deletion/services/deletion-log.service.spec.ts @@ -2,9 +2,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { setupEntities } from '@shared/testing'; import { ObjectId } from '@mikro-orm/mongodb'; -import { DomainModel } from '@shared/domain/types'; +import { DomainName, OperationType } from '@shared/domain/types'; import { DeletionLogRepo } from '../repo'; -import { DeletionOperationModel } from '../domain/types'; import { DeletionLogService } from './deletion-log.service'; import { deletionLogFactory } from '../domain/testing/factory/deletion-log.factory'; @@ -48,18 +47,18 @@ describe(DeletionLogService.name, () => { describe('when creating a deletionRequest', () => { const setup = () => { const deletionRequestId = '653e4833cc39e5907a1e18d2'; - const domain = DomainModel.USER; - const operation = DeletionOperationModel.DELETE; - const modifiedCount = 0; - const deletedCount = 1; + const domain = DomainName.USER; + const operation = OperationType.DELETE; + const count = 1; + const refs = [new ObjectId().toHexString()]; - return { deletionRequestId, domain, operation, modifiedCount, deletedCount }; + return { deletionRequestId, domain, operation, count, refs }; }; it('should call deletionRequestRepo.create', async () => { - const { deletionRequestId, domain, operation, modifiedCount, deletedCount } = setup(); + const { deletionRequestId, domain, operation, count, refs } = setup(); - await service.createDeletionLog(deletionRequestId, domain, operation, modifiedCount, deletedCount); + await service.createDeletionLog(deletionRequestId, domain, operation, count, refs); expect(deletionLogRepo.create).toHaveBeenCalledWith( expect.objectContaining({ @@ -68,8 +67,8 @@ describe(DeletionLogService.name, () => { deletionRequestId, domain, operation, - modifiedCount, - deletedCount, + count, + refs, }) ); }); @@ -83,7 +82,7 @@ describe(DeletionLogService.name, () => { const deletionLog1 = deletionLogFactory.build({ deletionRequestId }); const deletionLog2 = deletionLogFactory.build({ deletionRequestId, - domain: DomainModel.PSEUDONYMS, + domain: DomainName.PSEUDONYMS, }); const deletionLogs = [deletionLog1, deletionLog2]; diff --git a/apps/server/src/modules/deletion/services/deletion-log.service.ts b/apps/server/src/modules/deletion/services/deletion-log.service.ts index 577e76a864b..cf719c9e185 100644 --- a/apps/server/src/modules/deletion/services/deletion-log.service.ts +++ b/apps/server/src/modules/deletion/services/deletion-log.service.ts @@ -1,8 +1,7 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { Injectable } from '@nestjs/common'; -import { DomainModel, EntityId } from '@shared/domain/types'; +import { DomainName, EntityId, OperationType } from '@shared/domain/types'; import { DeletionLog } from '../domain/deletion-log.do'; -import { DeletionOperationModel } from '../domain/types'; import { DeletionLogRepo } from '../repo'; @Injectable() @@ -11,10 +10,10 @@ export class DeletionLogService { async createDeletionLog( deletionRequestId: EntityId, - domain: DomainModel, - operation: DeletionOperationModel, - modifiedCount: number, - deletedCount: number + domain: DomainName, + operation: OperationType, + count: number, + refs: string[] ): Promise { const newDeletionLog = new DeletionLog({ id: new ObjectId().toHexString(), @@ -22,8 +21,8 @@ export class DeletionLogService { domain, deletionRequestId, operation, - modifiedCount, - deletedCount, + count, + refs, }); await this.deletionLogRepo.create(newDeletionLog); diff --git a/apps/server/src/modules/deletion/services/deletion-request.service.spec.ts b/apps/server/src/modules/deletion/services/deletion-request.service.spec.ts index d4675d62861..e090a0b9f9f 100644 --- a/apps/server/src/modules/deletion/services/deletion-request.service.spec.ts +++ b/apps/server/src/modules/deletion/services/deletion-request.service.spec.ts @@ -2,7 +2,7 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { setupEntities } from '@shared/testing'; -import { DomainModel } from '@shared/domain/types'; +import { DomainName } from '@shared/domain/types'; import { DeletionRequestService } from './deletion-request.service'; import { DeletionRequestRepo } from '../repo'; import { deletionRequestFactory } from '../domain/testing/factory/deletion-request.factory'; @@ -48,7 +48,7 @@ describe(DeletionRequestService.name, () => { describe('when creating a deletionRequest', () => { const setup = () => { const targetRefId = '653e4833cc39e5907a1e18d2'; - const targetRefDomain = DomainModel.USER; + const targetRefDomain = DomainName.USER; return { targetRefId, targetRefDomain }; }; diff --git a/apps/server/src/modules/deletion/services/deletion-request.service.ts b/apps/server/src/modules/deletion/services/deletion-request.service.ts index 08c282a8693..ca0d4190901 100644 --- a/apps/server/src/modules/deletion/services/deletion-request.service.ts +++ b/apps/server/src/modules/deletion/services/deletion-request.service.ts @@ -1,6 +1,6 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { Injectable } from '@nestjs/common'; -import { DomainModel, EntityId } from '@shared/domain/types'; +import { DomainName, EntityId } from '@shared/domain/types'; import { DeletionRequest } from '../domain/deletion-request.do'; import { DeletionStatusModel } from '../domain/types'; import { DeletionRequestRepo } from '../repo/deletion-request.repo'; @@ -11,7 +11,7 @@ export class DeletionRequestService { async createDeletionRequest( targetRefId: EntityId, - targetRefDomain: DomainModel, + targetRefDomain: DomainName, deleteInMinutes = 43200 ): Promise<{ requestId: EntityId; deletionPlannedAt: Date }> { const dateOfDeletion = new Date(); diff --git a/apps/server/src/modules/deletion/uc/deletion-request.uc.spec.ts b/apps/server/src/modules/deletion/uc/deletion-request.uc.spec.ts index 1ceb0c86aaa..24a4998d33f 100644 --- a/apps/server/src/modules/deletion/uc/deletion-request.uc.spec.ts +++ b/apps/server/src/modules/deletion/uc/deletion-request.uc.spec.ts @@ -15,7 +15,7 @@ import { LegacyLogger } from '@src/core/logger'; import { ObjectId } from 'bson'; import { RegistrationPinService } from '@modules/registration-pin'; import { FilesStorageClientAdapterService } from '@src/modules/files-storage-client'; -import { DomainModel } from '@shared/domain/types'; +import { DomainName, OperationType } from '@shared/domain/types'; import { TaskService } from '@modules/task'; import { DomainOperationBuilder } from '@shared/domain/builder'; import { DeletionStatusModel } from '../domain/types'; @@ -157,7 +157,7 @@ describe(DeletionRequestUc.name, () => { const setup = () => { const deletionRequestToCreate: DeletionRequestBodyProps = { targetRef: { - domain: DomainModel.USER, + domain: DomainName.USER, id: new ObjectId().toHexString(), }, deleteInMinutes: 1440, @@ -203,31 +203,109 @@ describe(DeletionRequestUc.name, () => { describe('executeDeletionRequests', () => { describe('when executing deletionRequests', () => { const setup = () => { + const accountDeleted = DomainOperationBuilder.build(DomainName.ACCOUNT, OperationType.DELETE, 1, [ + new ObjectId().toHexString(), + ]); + + const classesUpdated = DomainOperationBuilder.build(DomainName.CLASS, OperationType.UPDATE, 1, [ + new ObjectId().toHexString(), + ]); + + const courseGroupUpdated = DomainOperationBuilder.build(DomainName.COURSEGROUP, OperationType.UPDATE, 1, [ + new ObjectId().toHexString(), + ]); + + const courseUpdated = DomainOperationBuilder.build(DomainName.COURSE, OperationType.UPDATE, 1, [ + new ObjectId().toHexString(), + ]); + const deletionRequestToExecute = deletionRequestFactory.build({ deleteAfter: new Date('2023-01-01') }); - const user = userDoFactory.buildWithId(); + + const dashboardDeleted = DomainOperationBuilder.build(DomainName.DASHBOARD, OperationType.DELETE, 1, [ + new ObjectId().toHexString(), + ]); + + const filesDeleted = DomainOperationBuilder.build(DomainName.FILE, OperationType.UPDATE, 1, [ + new ObjectId().toHexString(), + ]); + + const filesUpdated = DomainOperationBuilder.build(DomainName.FILE, OperationType.UPDATE, 1, [ + new ObjectId().toHexString(), + ]); + + const fileRecordsUpdated = DomainOperationBuilder.build(DomainName.FILERECORDS, OperationType.UPDATE, 1, [ + new ObjectId().toHexString(), + ]); + + const lessonsUpdated = DomainOperationBuilder.build(DomainName.LESSONS, OperationType.UPDATE, 1, [ + new ObjectId().toHexString(), + ]); + + const parentEmail = 'parent@parent.eu'; + + const pseudonymsDeleted = DomainOperationBuilder.build(DomainName.PSEUDONYMS, OperationType.DELETE, 1, [ + new ObjectId().toHexString(), + ]); + + const registrationPinDeleted = DomainOperationBuilder.build( + DomainName.REGISTRATIONPIN, + OperationType.DELETE, + 1, + [new ObjectId().toHexString()] + ); + const rocketChatUser: RocketChatUser = rocketChatUserFactory.build({ userId: deletionRequestToExecute.targetRefId, }); - const parentEmail = 'parent@parent.eu'; - const tasksModifiedByRemoveCreatorId = DomainOperationBuilder.build(DomainModel.TASK, 1, 0); - const tasksModifiedByRemoveUserFromFinished = DomainOperationBuilder.build(DomainModel.TASK, 1, 0); - const tasksDeleted = DomainOperationBuilder.build(DomainModel.TASK, 0, 1); - - registrationPinService.deleteRegistrationPinByEmail.mockResolvedValueOnce(2); - classService.deleteUserDataFromClasses.mockResolvedValueOnce(1); - courseGroupService.deleteUserDataFromCourseGroup.mockResolvedValueOnce(2); - courseService.deleteUserDataFromCourse.mockResolvedValueOnce(2); - filesService.markFilesOwnedByUserForDeletion.mockResolvedValueOnce(2); - filesService.removeUserPermissionsOrCreatorReferenceToAnyFiles.mockResolvedValueOnce(2); - lessonService.deleteUserDataFromLessons.mockResolvedValueOnce(2); - pseudonymService.deleteByUserId.mockResolvedValueOnce(2); - teamService.deleteUserDataFromTeams.mockResolvedValueOnce(2); - userService.deleteUser.mockResolvedValueOnce(1); - rocketChatUserService.deleteByUserId.mockResolvedValueOnce(1); - filesStorageClientAdapterService.removeCreatorIdFromFileRecords.mockResolvedValueOnce(5); - dashboardService.deleteDashboardByUserId.mockResolvedValueOnce(1); + + const rocketChatUserDeleted = DomainOperationBuilder.build(DomainName.ROCKETCHATUSER, OperationType.DELETE, 1, [ + new ObjectId().toHexString(), + ]); + + const rocketChatServiceDeleted = { success: true }; + + const tasksModifiedByRemoveCreatorId = DomainOperationBuilder.build(DomainName.TASK, OperationType.UPDATE, 1, [ + new ObjectId().toHexString(), + ]); + + const tasksModifiedByRemoveUserFromFinished = DomainOperationBuilder.build( + DomainName.TASK, + OperationType.UPDATE, + 1, + [new ObjectId().toHexString()] + ); + + const tasksDeleted = DomainOperationBuilder.build(DomainName.TASK, OperationType.DELETE, 1, [ + new ObjectId().toHexString(), + ]); + + const teamsUpdated = DomainOperationBuilder.build(DomainName.TEAMS, OperationType.UPDATE, 1, [ + new ObjectId().toHexString(), + ]); + + const userDeleted = DomainOperationBuilder.build(DomainName.USER, OperationType.DELETE, 1, [ + new ObjectId().toHexString(), + ]); + + const user = userDoFactory.buildWithId(); + + accountService.deleteAccountByUserId.mockResolvedValueOnce(accountDeleted); + registrationPinService.deleteRegistrationPinByEmail.mockResolvedValueOnce(registrationPinDeleted); + classService.deleteUserDataFromClasses.mockResolvedValueOnce(classesUpdated); + courseGroupService.deleteUserDataFromCourseGroup.mockResolvedValueOnce(courseGroupUpdated); + courseService.deleteUserDataFromCourse.mockResolvedValueOnce(courseUpdated); + filesService.markFilesOwnedByUserForDeletion.mockResolvedValueOnce(filesDeleted); + filesService.removeUserPermissionsOrCreatorReferenceToAnyFiles.mockResolvedValueOnce(filesUpdated); + lessonService.deleteUserDataFromLessons.mockResolvedValueOnce(lessonsUpdated); + pseudonymService.deleteByUserId.mockResolvedValueOnce(pseudonymsDeleted); + teamService.deleteUserDataFromTeams.mockResolvedValueOnce(teamsUpdated); + userService.deleteUser.mockResolvedValueOnce(userDeleted); + rocketChatUserService.deleteByUserId.mockResolvedValueOnce(rocketChatUserDeleted); + rocketChatService.deleteUser.mockResolvedValueOnce(rocketChatServiceDeleted); + filesStorageClientAdapterService.removeCreatorIdFromFileRecords.mockResolvedValueOnce(fileRecordsUpdated); + dashboardService.deleteDashboardByUserId.mockResolvedValueOnce(dashboardDeleted); taskService.removeCreatorIdFromTasks.mockResolvedValueOnce(tasksModifiedByRemoveCreatorId); - taskService.removeCreatorIdFromTasks.mockResolvedValueOnce(tasksModifiedByRemoveUserFromFinished); + taskService.removeUserFromFinished.mockResolvedValueOnce(tasksModifiedByRemoveUserFromFinished); taskService.deleteTasksByOnlyCreator.mockResolvedValueOnce(tasksDeleted); return { @@ -254,14 +332,14 @@ describe(DeletionRequestUc.name, () => { expect(deletionRequestService.markDeletionRequestAsExecuted).toHaveBeenCalledWith(deletionRequestToExecute.id); }); - it('should call accountService.deleteByUserId to delete user data in account module', async () => { + it('should call accountService.deleteAccountByUserId to delete user data in account module', async () => { const { deletionRequestToExecute } = setup(); deletionRequestService.findAllItemsToExecute.mockResolvedValueOnce([deletionRequestToExecute]); await uc.executeDeletionRequests(); - expect(accountService.deleteByUserId).toHaveBeenCalled(); + expect(accountService.deleteAccountByUserId).toHaveBeenCalledWith(deletionRequestToExecute.targetRefId); }); it('should call registrationPinService.deleteRegistrationPinByEmail to delete user data in registrationPin module', async () => { @@ -274,16 +352,17 @@ describe(DeletionRequestUc.name, () => { expect(registrationPinService.deleteRegistrationPinByEmail).toHaveBeenCalled(); }); - it('should call userService.getParentEmailsFromUser to get parentEmails', async () => { + it('should call userService.findById and userService.getParentEmailsFromUser to get own email and parentEmails', async () => { const { deletionRequestToExecute, user, parentEmail } = setup(); deletionRequestService.findAllItemsToExecute.mockResolvedValueOnce([deletionRequestToExecute]); - userService.findById.mockResolvedValueOnce(user); + userService.findByIdOrNull.mockResolvedValueOnce(user); userService.getParentEmailsFromUser.mockRejectedValue([parentEmail]); - registrationPinService.deleteRegistrationPinByEmail.mockRejectedValueOnce(2); + registrationPinService.deleteRegistrationPinByEmail.mockRejectedValueOnce(3); await uc.executeDeletionRequests(); + expect(userService.findByIdOrNull).toHaveBeenCalledWith(deletionRequestToExecute.targetRefId); expect(userService.getParentEmailsFromUser).toHaveBeenCalledWith(deletionRequestToExecute.targetRefId); }); @@ -470,14 +549,46 @@ describe(DeletionRequestUc.name, () => { const setup = () => { const deletionRequestToExecute = deletionRequestFactory.build({ deleteAfter: new Date('2023-01-01') }); - classService.deleteUserDataFromClasses.mockResolvedValueOnce(1); - courseGroupService.deleteUserDataFromCourseGroup.mockResolvedValueOnce(2); - courseService.deleteUserDataFromCourse.mockResolvedValueOnce(2); - filesService.markFilesOwnedByUserForDeletion.mockResolvedValueOnce(2); - filesService.removeUserPermissionsOrCreatorReferenceToAnyFiles.mockResolvedValueOnce(2); - lessonService.deleteUserDataFromLessons.mockResolvedValueOnce(2); - pseudonymService.deleteByUserId.mockResolvedValueOnce(2); - teamService.deleteUserDataFromTeams.mockResolvedValueOnce(2); + const classesUpdated = DomainOperationBuilder.build(DomainName.CLASS, OperationType.UPDATE, 1, [ + new ObjectId().toHexString(), + ]); + + const courseGroupUpdated = DomainOperationBuilder.build(DomainName.COURSEGROUP, OperationType.UPDATE, 1, [ + new ObjectId().toHexString(), + ]); + + const courseUpdated = DomainOperationBuilder.build(DomainName.COURSE, OperationType.UPDATE, 1, [ + new ObjectId().toHexString(), + ]); + + const filesDeleted = DomainOperationBuilder.build(DomainName.FILE, OperationType.UPDATE, 1, [ + new ObjectId().toHexString(), + ]); + + const filesUpdated = DomainOperationBuilder.build(DomainName.FILE, OperationType.UPDATE, 1, [ + new ObjectId().toHexString(), + ]); + + const lessonsUpdated = DomainOperationBuilder.build(DomainName.LESSONS, OperationType.UPDATE, 1, [ + new ObjectId().toHexString(), + ]); + + const pseudonymsDeleted = DomainOperationBuilder.build(DomainName.PSEUDONYMS, OperationType.DELETE, 1, [ + new ObjectId().toHexString(), + ]); + + const teamsUpdated = DomainOperationBuilder.build(DomainName.TEAMS, OperationType.UPDATE, 1, [ + new ObjectId().toHexString(), + ]); + + classService.deleteUserDataFromClasses.mockResolvedValueOnce(classesUpdated); + courseGroupService.deleteUserDataFromCourseGroup.mockResolvedValueOnce(courseGroupUpdated); + courseService.deleteUserDataFromCourse.mockResolvedValueOnce(courseUpdated); + filesService.markFilesOwnedByUserForDeletion.mockResolvedValueOnce(filesDeleted); + filesService.removeUserPermissionsOrCreatorReferenceToAnyFiles.mockResolvedValueOnce(filesUpdated); + lessonService.deleteUserDataFromLessons.mockResolvedValueOnce(lessonsUpdated); + pseudonymService.deleteByUserId.mockResolvedValueOnce(pseudonymsDeleted); + teamService.deleteUserDataFromTeams.mockResolvedValueOnce(teamsUpdated); userService.deleteUser.mockRejectedValueOnce(new Error()); return { @@ -509,8 +620,9 @@ describe(DeletionRequestUc.name, () => { ); const statistics = DomainOperationBuilder.build( deletionLogExecuted.domain, - deletionLogExecuted.modifiedCount, - deletionLogExecuted.deletedCount + deletionLogExecuted.operation, + deletionLogExecuted.count, + deletionLogExecuted.refs ); const executedDeletionRequestSummary = DeletionRequestLogResponseBuilder.build( @@ -562,8 +674,9 @@ describe(DeletionRequestUc.name, () => { ); const statistics = DeletionLogStatisticBuilder.build( deletionLogExecuted.domain, - deletionLogExecuted.modifiedCount, - deletionLogExecuted.deletedCount + deletionLogExecuted.operation, + deletionLogExecuted.count, + deletionLogExecuted.refs ); const executedDeletionRequestSummary = DeletionRequestLogResponseBuilder.build( diff --git a/apps/server/src/modules/deletion/uc/deletion-request.uc.ts b/apps/server/src/modules/deletion/uc/deletion-request.uc.ts index 84025a80f1e..84678afc6bb 100644 --- a/apps/server/src/modules/deletion/uc/deletion-request.uc.ts +++ b/apps/server/src/modules/deletion/uc/deletion-request.uc.ts @@ -10,7 +10,7 @@ import { RocketChatUserService } from '@modules/rocketchat-user'; import { TeamService } from '@modules/teams'; import { UserService } from '@modules/user'; import { Injectable } from '@nestjs/common'; -import { DomainModel, EntityId } from '@shared/domain/types'; +import { DomainName, EntityId, OperationType } from '@shared/domain/types'; import { LegacyLogger } from '@src/core/logger'; import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; import { TaskService } from '@modules/task'; @@ -19,7 +19,6 @@ import { DomainOperationBuilder } from '@shared/domain/builder/domain-operation. import { DeletionRequestLogResponseBuilder, DeletionTargetRefBuilder } from '../builder'; import { DeletionRequestBodyProps, DeletionRequestLogResponse, DeletionRequestResponse } from '../controller/dto'; import { DeletionRequest, DeletionLog } from '../domain'; -import { DeletionOperationModel } from '../domain/types'; import { DeletionRequestService, DeletionLogService } from '../services'; @Injectable() @@ -83,7 +82,7 @@ export class DeletionRequestUc { const deletionLog: DeletionLog[] = await this.deletionLogService.findByDeletionRequestId(deletionRequestId); const domainOperation: DomainOperation[] = deletionLog.map((log) => - DomainOperationBuilder.build(log.domain, log.modifiedCount, log.deletedCount) + DomainOperationBuilder.build(log.domain, log.operation, log.count, log.refs) ); response = { ...response, statistics: domainOperation }; @@ -122,25 +121,25 @@ export class DeletionRequestUc { private async logDeletion( deletionRequest: DeletionRequest, - domainModel: DomainModel, - operationModel: DeletionOperationModel, - updatedCount: number, - deletedCount: number + domainModel: DomainName, + operation: OperationType, + count: number, + refs: string[] ): Promise { - await this.deletionLogService.createDeletionLog( - deletionRequest.id, - domainModel, - operationModel, - updatedCount, - deletedCount - ); + await this.deletionLogService.createDeletionLog(deletionRequest.id, domainModel, operation, count, refs); } private async removeAccount(deletionRequest: DeletionRequest) { this.logger.debug({ action: 'removeAccount', deletionRequest }); - await this.accountService.deleteByUserId(deletionRequest.targetRefId); - await this.logDeletion(deletionRequest, DomainModel.ACCOUNT, DeletionOperationModel.DELETE, 0, 1); + const accountDeleted = await this.accountService.deleteAccountByUserId(deletionRequest.targetRefId); + await this.logDeletion( + deletionRequest, + accountDeleted.domain, + accountDeleted.operation, + accountDeleted.count, + accountDeleted.refs + ); } private async removeUserRegistrationPin(deletionRequest: DeletionRequest): Promise { @@ -151,72 +150,97 @@ export class DeletionRequestUc { emailsToDeletion = [userToDeletion.email, ...parentEmails]; } - const result = await Promise.all( + const results = await Promise.all( emailsToDeletion.map((email) => this.registrationPinService.deleteRegistrationPinByEmail(email)) ); - const deletedRegistrationPin = result.filter((res) => res !== 0).length; - await this.logDeletion( - deletionRequest, - DomainModel.REGISTRATIONPIN, - DeletionOperationModel.DELETE, - 0, - deletedRegistrationPin - ); + const result = this.getDomainOperation(results); + + await this.logDeletion(deletionRequest, result.domain, result.operation, result.count, result.refs); + + return result.count; + } - return deletedRegistrationPin; + private getDomainOperation(results: DomainOperation[]) { + return results.reduce( + (acc, current) => { + acc.count += current.count; + acc.refs = [...acc.refs, ...current.refs]; + + return acc; + }, + { + domain: results[0].domain, + operation: results[0].operation, + count: 0, + refs: [], + } + ); } private async removeUserFromClasses(deletionRequest: DeletionRequest) { this.logger.debug({ action: 'removeUserFromClasses', deletionRequest }); - const classesUpdated: number = await this.classService.deleteUserDataFromClasses(deletionRequest.targetRefId); - await this.logDeletion(deletionRequest, DomainModel.CLASS, DeletionOperationModel.UPDATE, classesUpdated, 0); + const classesUpdated = await this.classService.deleteUserDataFromClasses(deletionRequest.targetRefId); + await this.logDeletion( + deletionRequest, + classesUpdated.domain, + classesUpdated.operation, + classesUpdated.count, + classesUpdated.refs + ); } private async removeUserFromCourseGroup(deletionRequest: DeletionRequest) { this.logger.debug({ action: 'removeUserFromCourseGroup', deletionRequest }); - const courseGroupUpdated: number = await this.courseGroupService.deleteUserDataFromCourseGroup( - deletionRequest.targetRefId - ); + const courseGroupUpdated = await this.courseGroupService.deleteUserDataFromCourseGroup(deletionRequest.targetRefId); await this.logDeletion( deletionRequest, - DomainModel.COURSEGROUP, - DeletionOperationModel.UPDATE, - courseGroupUpdated, - 0 + courseGroupUpdated.domain, + courseGroupUpdated.operation, + courseGroupUpdated.count, + courseGroupUpdated.refs ); } private async removeUserFromCourse(deletionRequest: DeletionRequest) { this.logger.debug({ action: 'removeUserFromCourse', deletionRequest }); - const courseUpdated: number = await this.courseService.deleteUserDataFromCourse(deletionRequest.targetRefId); - await this.logDeletion(deletionRequest, DomainModel.COURSE, DeletionOperationModel.UPDATE, courseUpdated, 0); + const courseUpdated = await this.courseService.deleteUserDataFromCourse(deletionRequest.targetRefId); + await this.logDeletion( + deletionRequest, + courseUpdated.domain, + courseUpdated.operation, + courseUpdated.count, + courseUpdated.refs + ); } private async removeUsersDashboard(deletionRequest: DeletionRequest) { this.logger.debug({ action: 'removeUsersDashboard', deletionRequest }); - const dashboardDeleted: number = await this.dashboardService.deleteDashboardByUserId(deletionRequest.targetRefId); - await this.logDeletion(deletionRequest, DomainModel.DASHBOARD, DeletionOperationModel.DELETE, 0, dashboardDeleted); + const dashboardDeleted = await this.dashboardService.deleteDashboardByUserId(deletionRequest.targetRefId); + await this.logDeletion( + deletionRequest, + dashboardDeleted.domain, + dashboardDeleted.operation, + dashboardDeleted.count, + dashboardDeleted.refs + ); } private async removeUsersFilesAndPermissions(deletionRequest: DeletionRequest) { this.logger.debug({ action: 'removeUsersFilesAndPermissions', deletionRequest }); - const filesDeleted: number = await this.filesService.markFilesOwnedByUserForDeletion(deletionRequest.targetRefId); - const filesUpdated: number = await this.filesService.removeUserPermissionsOrCreatorReferenceToAnyFiles( + const filesDeleted = await this.filesService.markFilesOwnedByUserForDeletion(deletionRequest.targetRefId); + const filesUpdated = await this.filesService.removeUserPermissionsOrCreatorReferenceToAnyFiles( deletionRequest.targetRefId ); - await this.logDeletion( - deletionRequest, - DomainModel.FILE, - DeletionOperationModel.UPDATE, - filesDeleted + filesUpdated, - 0 - ); + + const result = this.getDomainOperation([filesDeleted, filesUpdated]); + + await this.logDeletion(deletionRequest, result.domain, result.operation, result.count, result.refs); } private async removeUsersDataFromFileRecords(deletionRequest: DeletionRequest) { @@ -228,32 +252,50 @@ export class DeletionRequestUc { await this.logDeletion( deletionRequest, - DomainModel.FILERECORDS, - DeletionOperationModel.UPDATE, - fileRecordsUpdated, - 0 + fileRecordsUpdated.domain, + fileRecordsUpdated.operation, + fileRecordsUpdated.count, + fileRecordsUpdated.refs ); } private async removeUserFromLessons(deletionRequest: DeletionRequest) { this.logger.debug({ action: 'removeUserFromLessons', deletionRequest }); - const lessonsUpdated: number = await this.lessonService.deleteUserDataFromLessons(deletionRequest.targetRefId); - await this.logDeletion(deletionRequest, DomainModel.LESSONS, DeletionOperationModel.UPDATE, lessonsUpdated, 0); + const lessonsUpdated = await this.lessonService.deleteUserDataFromLessons(deletionRequest.targetRefId); + await this.logDeletion( + deletionRequest, + lessonsUpdated.domain, + lessonsUpdated.operation, + lessonsUpdated.count, + lessonsUpdated.refs + ); } private async removeUsersPseudonyms(deletionRequest: DeletionRequest) { this.logger.debug({ action: 'removeUsersPseudonyms', deletionRequest }); - const pseudonymDeleted: number = await this.pseudonymService.deleteByUserId(deletionRequest.targetRefId); - await this.logDeletion(deletionRequest, DomainModel.PSEUDONYMS, DeletionOperationModel.DELETE, 0, pseudonymDeleted); + const pseudonymsDeleted = await this.pseudonymService.deleteByUserId(deletionRequest.targetRefId); + await this.logDeletion( + deletionRequest, + pseudonymsDeleted.domain, + pseudonymsDeleted.operation, + pseudonymsDeleted.count, + pseudonymsDeleted.refs + ); } private async removeUserFromTeams(deletionRequest: DeletionRequest) { this.logger.debug({ action: ' removeUserFromTeams', deletionRequest }); - const teamsUpdated: number = await this.teamService.deleteUserDataFromTeams(deletionRequest.targetRefId); - await this.logDeletion(deletionRequest, DomainModel.TEAMS, DeletionOperationModel.UPDATE, teamsUpdated, 0); + const teamsUpdated = await this.teamService.deleteUserDataFromTeams(deletionRequest.targetRefId); + await this.logDeletion( + deletionRequest, + teamsUpdated.domain, + teamsUpdated.operation, + teamsUpdated.count, + teamsUpdated.refs + ); } private async removeUser(deletionRequest: DeletionRequest) { @@ -261,9 +303,15 @@ export class DeletionRequestUc { const registrationPinDeleted = await this.removeUserRegistrationPin(deletionRequest); - if (registrationPinDeleted) { - const userDeleted: number = await this.userService.deleteUser(deletionRequest.targetRefId); - await this.logDeletion(deletionRequest, DomainModel.USER, DeletionOperationModel.DELETE, 0, userDeleted); + if (registrationPinDeleted >= 0) { + const userDeleted = await this.userService.deleteUser(deletionRequest.targetRefId); + await this.logDeletion( + deletionRequest, + userDeleted.domain, + userDeleted.operation, + userDeleted.count, + userDeleted.refs + ); } } @@ -271,38 +319,55 @@ export class DeletionRequestUc { this.logger.debug({ action: 'removeUserFromRocketChat', deletionRequest }); const rocketChatUser = await this.rocketChatUserService.findByUserId(deletionRequest.targetRefId); - let rocketChatUserDeleted = 0; if (rocketChatUser.length > 0) { - [, rocketChatUserDeleted] = await Promise.all([ + const [rocketChatDeleted, rocketChatUserDeleted] = await Promise.all([ this.rocketChatService.deleteUser(rocketChatUser[0].username), this.rocketChatUserService.deleteByUserId(rocketChatUser[0].userId), ]); + + await this.logDeletion( + deletionRequest, + rocketChatUserDeleted.domain, + rocketChatUserDeleted.operation, + rocketChatUserDeleted.count, + rocketChatUserDeleted.refs + ); + + if (rocketChatDeleted) { + await this.logDeletion(deletionRequest, DomainName.ROCKETCHATSERVICE, OperationType.DELETE, 1, [ + rocketChatUser[0].username, + ]); + } } - await this.logDeletion( - deletionRequest, - DomainModel.ROCKETCHATUSER, - DeletionOperationModel.DELETE, - 0, - rocketChatUserDeleted - ); } private async removeUserFromTasks(deletionRequest: DeletionRequest) { this.logger.debug({ action: 'removeUserFromTasks', deletionRequest }); - const tasksDeleted = await this.taskService.deleteTasksByOnlyCreator(deletionRequest.targetRefId); - const tasksModifiedByRemoveCreator = await this.taskService.removeCreatorIdFromTasks(deletionRequest.targetRefId); - const tasksModifiedByRemoveUserFromFinished = await this.taskService.removeUserFromFinished( - deletionRequest.targetRefId + const [tasksDeleted, tasksModifiedByRemoveCreator, tasksModifiedByRemoveUserFromFinished] = await Promise.all([ + this.taskService.deleteTasksByOnlyCreator(deletionRequest.targetRefId), + this.taskService.removeCreatorIdFromTasks(deletionRequest.targetRefId), + this.taskService.removeUserFromFinished(deletionRequest.targetRefId), + ]); + + const modifiedTasksCount = tasksModifiedByRemoveCreator.count + tasksModifiedByRemoveUserFromFinished.count; + const modifiedTasksRef = [...tasksModifiedByRemoveCreator.refs, ...tasksModifiedByRemoveUserFromFinished.refs]; + + await this.logDeletion( + deletionRequest, + tasksDeleted.domain, + tasksDeleted.operation, + tasksDeleted.count, + tasksDeleted.refs ); await this.logDeletion( deletionRequest, - DomainModel.TASK, - DeletionOperationModel.UPDATE, - tasksModifiedByRemoveCreator.modifiedCount + tasksModifiedByRemoveUserFromFinished.modifiedCount, - tasksDeleted.deletedCount + tasksModifiedByRemoveCreator.domain, + tasksModifiedByRemoveCreator.operation, + modifiedTasksCount, + modifiedTasksRef ); } } diff --git a/apps/server/src/modules/deletion/uc/interface/interfaces.ts b/apps/server/src/modules/deletion/uc/interface/interfaces.ts index 004484c20b2..c4bfb615d9e 100644 --- a/apps/server/src/modules/deletion/uc/interface/interfaces.ts +++ b/apps/server/src/modules/deletion/uc/interface/interfaces.ts @@ -1,8 +1,8 @@ import { EntityId } from '@shared/domain/types'; -import { DomainModel } from '@shared/domain/types/domain'; +import { DomainName } from '@shared/domain/types/domain-name.enum'; export interface DeletionTargetRef { - targetRefDomain: DomainModel; + targetRefDomain: DomainName; targetRefId: EntityId; } @@ -13,13 +13,13 @@ export interface DeletionRequestLog { } export interface DeletionLogStatistic { - domain: DomainModel; + domain: DomainName; modifiedCount?: number; deletedCount?: number; } export interface DeletionRequestProps { - targetRef: { targetRefDoamin: DomainModel; targetRefId: EntityId }; + targetRef: { targetRefDoamin: DomainName; targetRefId: EntityId }; deleteInMinutes?: number; } diff --git a/apps/server/src/modules/files-storage-client/service/files-storage-client.service.ts b/apps/server/src/modules/files-storage-client/service/files-storage-client.service.ts index a729f878587..d6c4ce94ccd 100644 --- a/apps/server/src/modules/files-storage-client/service/files-storage-client.service.ts +++ b/apps/server/src/modules/files-storage-client/service/files-storage-client.service.ts @@ -1,6 +1,9 @@ import { Injectable } from '@nestjs/common'; -import { EntityId } from '@shared/domain/types'; +import { DomainName, EntityId, OperationType } from '@shared/domain/types'; import { LegacyLogger } from '@src/core/logger'; +import { FileDO } from '@src/infra/rabbitmq'; +import { DomainOperation } from '@shared/domain/interface'; +import { DomainOperationBuilder } from '@shared/domain/builder'; import { CopyFileDto, FileDto } from '../dto'; import { FileRequestInfo } from '../interfaces'; import { CopyFilesRequestInfo } from '../interfaces/copy-file-request-info'; @@ -36,9 +39,20 @@ export class FilesStorageClientAdapterService { return fileInfos; } - async removeCreatorIdFromFileRecords(creatorId: EntityId): Promise { + async removeCreatorIdFromFileRecords(creatorId: EntityId): Promise { const response = await this.fileStorageMQProducer.removeCreatorIdFromFileRecords(creatorId); - return response.length; + const result = DomainOperationBuilder.build( + DomainName.FILERECORDS, + OperationType.UPDATE, + response.length, + this.getFileRecordsId(response) + ); + + return result; + } + + private getFileRecordsId(files: FileDO[]): EntityId[] { + return files.map((file) => file.id); } } diff --git a/apps/server/src/modules/files/service/files.service.spec.ts b/apps/server/src/modules/files/service/files.service.spec.ts index 702906ddd25..6cab718a7ab 100644 --- a/apps/server/src/modules/files/service/files.service.spec.ts +++ b/apps/server/src/modules/files/service/files.service.spec.ts @@ -3,6 +3,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { setupEntities } from '@shared/testing'; import { Logger } from '@src/core/logger'; +import { DomainOperationBuilder } from '@shared/domain/builder'; +import { DomainName, OperationType } from '@shared/domain/types'; import { FilesService } from './files.service'; import { FilesRepo } from '../repo'; import { fileEntityFactory, filePermissionEntityFactory } from '../entity/testing'; @@ -103,75 +105,119 @@ describe(FilesService.name, () => { }); describe('removeUserPermissionsOrCreatorReferenceToAnyFiles', () => { - it('should not modify any files if there are none that user has permission to access or is creator', async () => { + const setup = () => { const userId = new ObjectId().toHexString(); + const userPermission = filePermissionEntityFactory.build({ refId: userId }); + const anotherUserPermission = filePermissionEntityFactory.build(); + const yetAnotherUserPermission = filePermissionEntityFactory.build(); + + const entity1 = fileEntityFactory.buildWithId({ + permissions: [userPermission], + creatorId: userId, + }); + const entity2 = fileEntityFactory.buildWithId({ + permissions: [userPermission], + creatorId: userId, + }); + const entity3 = fileEntityFactory.buildWithId({ + permissions: [userPermission], + }); + const entity4 = fileEntityFactory.buildWithId({ + permissions: [anotherUserPermission, yetAnotherUserPermission, userPermission], + creatorId: userId, + }); + const entity5 = fileEntityFactory.buildWithId({ + permissions: [anotherUserPermission, yetAnotherUserPermission, userPermission], + creatorId: userId, + }); + const entity6 = fileEntityFactory.buildWithId({ + permissions: [yetAnotherUserPermission, userPermission, anotherUserPermission], + }); + + const entities = [entity4, entity5, entity6]; + + const expectedResultWhenFilesNotExists = DomainOperationBuilder.build( + DomainName.FILE, + OperationType.UPDATE, + 0, + [] + ); + + const expectedResultWhenFilesExistsWithOnlyUserId = DomainOperationBuilder.build( + DomainName.FILE, + OperationType.UPDATE, + 3, + [entity1.id, entity2.id, entity3.id] + ); + + const expectedResultWhenManyFilesExistsWithOtherUsers = DomainOperationBuilder.build( + DomainName.FILE, + OperationType.UPDATE, + 3, + [entity4.id, entity5.id, entity6.id] + ); + + return { + entity1, + entity2, + entity3, + entities, + expectedResultWhenFilesExistsWithOnlyUserId, + expectedResultWhenManyFilesExistsWithOtherUsers, + expectedResultWhenFilesNotExists, + userId, + userPermission, + anotherUserPermission, + yetAnotherUserPermission, + }; + }; + + it('should not modify any files if there are none that user has permission to access or is creator', async () => { + const { expectedResultWhenFilesNotExists, userId } = setup(); + repo.findByPermissionRefIdOrCreatorId.mockResolvedValueOnce([]); const result = await service.removeUserPermissionsOrCreatorReferenceToAnyFiles(userId); - expect(result).toEqual(0); + expect(result).toEqual(expectedResultWhenFilesNotExists); expect(repo.findByPermissionRefIdOrCreatorId).toBeCalledWith(userId); - expect(repo.save).not.toBeCalled(); }); describe('should properly remove user permissions, creatorId reference', () => { - const setup = () => { - const userId = new ObjectId().toHexString(); - const userPermission = filePermissionEntityFactory.build({ refId: userId }); - - const entity = fileEntityFactory.buildWithId({ permissions: [userPermission], creatorId: userId }); - const entity2 = fileEntityFactory.buildWithId({ creatorId: userId }); - const entity3 = fileEntityFactory.buildWithId({ permissions: [userPermission] }); - - repo.findByPermissionRefIdOrCreatorId.mockResolvedValueOnce([entity, entity2, entity3]); - return { userId, userPermission, entity, entity2, entity3 }; - }; it('in case of just a single file (permission) accessible by given user and couple of files created', async () => { - const { userId, userPermission, entity, entity2, entity3 } = setup(); + const { entity1, entity2, entity3, expectedResultWhenFilesExistsWithOnlyUserId, userId, userPermission } = + setup(); + + repo.findByPermissionRefIdOrCreatorId.mockResolvedValueOnce([entity1, entity2, entity3]); const result = await service.removeUserPermissionsOrCreatorReferenceToAnyFiles(userId); - expect(result).toEqual(3); + expect(result).toEqual(expectedResultWhenFilesExistsWithOnlyUserId); expect(entity3.permissions).not.toContain(userPermission); - expect(entity._creatorId).toBe(undefined); + expect(entity1._creatorId).toBe(undefined); expect(entity3.permissions).not.toContain(userPermission); expect(entity2._creatorId).toBe(undefined); expect(repo.findByPermissionRefIdOrCreatorId).toBeCalledWith(userId); - expect(repo.save).toBeCalledWith([entity, entity2, entity3]); + expect(repo.save).toBeCalledWith([entity1, entity2, entity3]); }); it('in case of many files accessible or created by given user', async () => { - const userId = new ObjectId().toHexString(); - const userPermission = filePermissionEntityFactory.build({ refId: userId }); - const anotherUserPermission = filePermissionEntityFactory.build(); - const yetAnotherUserPermission = filePermissionEntityFactory.build(); - const entities = [ - fileEntityFactory.buildWithId({ - permissions: [userPermission, anotherUserPermission, yetAnotherUserPermission], - }), - fileEntityFactory.buildWithId({ - permissions: [yetAnotherUserPermission, userPermission, anotherUserPermission], - creatorId: userId, - }), - fileEntityFactory.buildWithId({ - permissions: [anotherUserPermission, yetAnotherUserPermission, userPermission], - }), - fileEntityFactory.buildWithId({ - permissions: [userPermission, yetAnotherUserPermission, anotherUserPermission], - creatorId: userId, - }), - fileEntityFactory.buildWithId({ - permissions: [yetAnotherUserPermission, anotherUserPermission, userPermission], - }), - ]; + const { + entities, + expectedResultWhenManyFilesExistsWithOtherUsers, + userId, + userPermission, + anotherUserPermission, + yetAnotherUserPermission, + } = setup(); repo.findByPermissionRefIdOrCreatorId.mockResolvedValueOnce(entities); const result = await service.removeUserPermissionsOrCreatorReferenceToAnyFiles(userId); - expect(result).toEqual(5); + expect(result).toEqual(expectedResultWhenManyFilesExistsWithOtherUsers); for (let i = 0; i < entities.length; i += 1) { expect(entities[i].permissions).not.toContain(userPermission); @@ -236,6 +282,40 @@ describe(FilesService.name, () => { }); describe('markFilesOwnedByUserForDeletion', () => { + const setup = () => { + const userId = new ObjectId().toHexString(); + const entity1 = fileEntityFactory.buildWithId({ ownerId: userId }); + const entity2 = fileEntityFactory.buildWithId({ ownerId: userId }); + const entity3 = fileEntityFactory.buildWithId({ ownerId: userId }); + const entities = [entity1, entity2, entity3]; + + const expectedResultWhenFilesNotExists = DomainOperationBuilder.build( + DomainName.FILE, + OperationType.UPDATE, + 0, + [] + ); + + const expectedResultWhenOneFileExists = DomainOperationBuilder.build(DomainName.FILE, OperationType.UPDATE, 1, [ + entity1.id, + ]); + + const expectedResultWhenManyFilesExists = DomainOperationBuilder.build(DomainName.FILE, OperationType.UPDATE, 3, [ + entity1.id, + entity2.id, + entity3.id, + ]); + + return { + entities, + entity1, + expectedResultWhenOneFileExists, + expectedResultWhenManyFilesExists, + expectedResultWhenFilesNotExists, + userId, + }; + }; + const verifyEntityChanges = (entity: FileEntity) => { expect(entity.deleted).toEqual(true); @@ -246,46 +326,37 @@ describe(FilesService.name, () => { }; it('should not mark any files for deletion if there are none owned by given user', async () => { - const userId = new ObjectId().toHexString(); - repo.findByOwnerUserId.mockResolvedValueOnce([]); + const { expectedResultWhenFilesNotExists, userId } = setup(); + repo.findByOwnerUserId.mockResolvedValueOnce([]); const result = await service.markFilesOwnedByUserForDeletion(userId); - expect(result).toEqual(0); + expect(result).toEqual(expectedResultWhenFilesNotExists); expect(repo.findByOwnerUserId).toBeCalledWith(userId); - expect(repo.save).not.toBeCalled(); }); describe('should properly mark files for deletion', () => { it('in case of just a single file owned by given user', async () => { - const entity = fileEntityFactory.buildWithId(); - const userId = entity.ownerId; - repo.findByOwnerUserId.mockResolvedValueOnce([entity]); + const { entity1, expectedResultWhenOneFileExists, userId } = setup(); + repo.findByOwnerUserId.mockResolvedValueOnce([entity1]); const result = await service.markFilesOwnedByUserForDeletion(userId); - expect(result).toEqual(1); - verifyEntityChanges(entity); + expect(result).toEqual(expectedResultWhenOneFileExists); + verifyEntityChanges(entity1); expect(repo.findByOwnerUserId).toBeCalledWith(userId); - expect(repo.save).toBeCalledWith([entity]); + expect(repo.save).toBeCalledWith([entity1]); }); it('in case of many files owned by the user', async () => { - const userId = new ObjectId().toHexString(); - const entities = [ - fileEntityFactory.buildWithId({ ownerId: userId }), - fileEntityFactory.buildWithId({ ownerId: userId }), - fileEntityFactory.buildWithId({ ownerId: userId }), - fileEntityFactory.buildWithId({ ownerId: userId }), - fileEntityFactory.buildWithId({ ownerId: userId }), - ]; + const { entities, expectedResultWhenManyFilesExists, userId } = setup(); repo.findByOwnerUserId.mockResolvedValueOnce(entities); const result = await service.markFilesOwnedByUserForDeletion(userId); - expect(result).toEqual(5); + expect(result).toEqual(expectedResultWhenManyFilesExists); entities.forEach((entity) => verifyEntityChanges(entity)); expect(repo.findByOwnerUserId).toBeCalledWith(userId); diff --git a/apps/server/src/modules/files/service/files.service.ts b/apps/server/src/modules/files/service/files.service.ts index 1b3941aaffc..7e39e5c236a 100644 --- a/apps/server/src/modules/files/service/files.service.ts +++ b/apps/server/src/modules/files/service/files.service.ts @@ -1,7 +1,9 @@ import { Injectable } from '@nestjs/common'; -import { DomainModel, EntityId, StatusModel } from '@shared/domain/types'; +import { DomainName, EntityId, OperationType, StatusModel } from '@shared/domain/types'; import { Logger } from '@src/core/logger'; import { DataDeletionDomainOperationLoggable } from '@shared/common/loggable'; +import { DomainOperation } from '@shared/domain/interface'; +import { DomainOperationBuilder } from '@shared/domain/builder'; import { FileEntity } from '../entity'; import { FilesRepo } from '../repo'; @@ -15,21 +17,17 @@ export class FilesService { return this.repo.findByPermissionRefIdOrCreatorId(userId); } - async removeUserPermissionsOrCreatorReferenceToAnyFiles(userId: EntityId): Promise { + async removeUserPermissionsOrCreatorReferenceToAnyFiles(userId: EntityId): Promise { this.logger.info( new DataDeletionDomainOperationLoggable( 'Deleting user data from Files', - DomainModel.FILE, + DomainName.FILE, userId, StatusModel.PENDING ) ); const entities = await this.repo.findByPermissionRefIdOrCreatorId(userId); - if (entities.length === 0) { - return 0; - } - entities.forEach((entity) => { entity.removePermissionsByRefId(userId); entity.removeCreatorId(userId); @@ -39,10 +37,17 @@ export class FilesService { const numberOfUpdatedFiles = entities.length; + const result = DomainOperationBuilder.build( + DomainName.FILE, + OperationType.UPDATE, + numberOfUpdatedFiles, + this.getFilesId(entities) + ); + this.logger.info( new DataDeletionDomainOperationLoggable( 'Successfully removed user data from Files', - DomainModel.FILE, + DomainName.FILE, userId, StatusModel.FINISHED, numberOfUpdatedFiles, @@ -50,38 +55,41 @@ export class FilesService { ) ); - return numberOfUpdatedFiles; + return result; } async findFilesOwnedByUser(userId: EntityId): Promise { return this.repo.findByOwnerUserId(userId); } - async markFilesOwnedByUserForDeletion(userId: EntityId): Promise { + async markFilesOwnedByUserForDeletion(userId: EntityId): Promise { this.logger.info( new DataDeletionDomainOperationLoggable( 'Marking user files to deletion', - DomainModel.FILE, + DomainName.FILE, userId, StatusModel.PENDING ) ); const entities = await this.repo.findByOwnerUserId(userId); - if (entities.length === 0) { - return 0; - } - entities.forEach((entity) => entity.markForDeletion()); await this.repo.save(entities); const numberOfMarkedForDeletionFiles = entities.length; + const result = DomainOperationBuilder.build( + DomainName.FILE, + OperationType.UPDATE, + numberOfMarkedForDeletionFiles, + this.getFilesId(entities) + ); + this.logger.info( new DataDeletionDomainOperationLoggable( 'Successfully marked user files for deletion', - DomainModel.FILE, + DomainName.FILE, userId, StatusModel.FINISHED, numberOfMarkedForDeletionFiles, @@ -89,6 +97,10 @@ export class FilesService { ) ); - return numberOfMarkedForDeletionFiles; + return result; + } + + private getFilesId(files: FileEntity[]): EntityId[] { + return files.map((file) => file.id); } } diff --git a/apps/server/src/modules/learnroom/service/course.service.spec.ts b/apps/server/src/modules/learnroom/service/course.service.spec.ts index 379c588d518..923bc66c808 100644 --- a/apps/server/src/modules/learnroom/service/course.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/course.service.spec.ts @@ -4,6 +4,8 @@ import { Course } from '@shared/domain/entity'; import { CourseRepo, UserRepo } from '@shared/repo'; import { courseFactory, setupEntities, userFactory } from '@shared/testing'; import { Logger } from '@src/core/logger'; +import { DomainOperationBuilder } from '@shared/domain/builder'; +import { DomainName, OperationType } from '@shared/domain/types'; import { CourseService } from './course.service'; describe('CourseService', () => { @@ -109,7 +111,14 @@ describe('CourseService', () => { userRepo.findById.mockResolvedValue(user); courseRepo.findAllByUserId.mockResolvedValue([allCourses, allCourses.length]); + const expectedResult = DomainOperationBuilder.build(DomainName.COURSE, OperationType.UPDATE, 3, [ + course1.id, + course2.id, + course3.id, + ]); + return { + expectedResult, user, }; }; @@ -121,9 +130,9 @@ describe('CourseService', () => { }); it('should update courses without deleted user', async () => { - const { user } = setup(); + const { expectedResult, user } = setup(); const result = await courseService.deleteUserDataFromCourse(user.id); - expect(result).toEqual(3); + expect(result).toEqual(expectedResult); }); }); diff --git a/apps/server/src/modules/learnroom/service/course.service.ts b/apps/server/src/modules/learnroom/service/course.service.ts index f85526bcc83..38a9c974c7a 100644 --- a/apps/server/src/modules/learnroom/service/course.service.ts +++ b/apps/server/src/modules/learnroom/service/course.service.ts @@ -1,7 +1,9 @@ import { Injectable } from '@nestjs/common'; import { DataDeletionDomainOperationLoggable } from '@shared/common/loggable'; +import { DomainOperationBuilder } from '@shared/domain/builder'; import { Course } from '@shared/domain/entity'; -import { Counted, DomainModel, EntityId, StatusModel } from '@shared/domain/types'; +import { DomainOperation } from '@shared/domain/interface'; +import { Counted, DomainName, EntityId, OperationType, StatusModel } from '@shared/domain/types'; import { CourseRepo } from '@shared/repo'; import { Logger } from '@src/core/logger'; @@ -21,11 +23,11 @@ export class CourseService { return [courses, count]; } - public async deleteUserDataFromCourse(userId: EntityId): Promise { + public async deleteUserDataFromCourse(userId: EntityId): Promise { this.logger.info( new DataDeletionDomainOperationLoggable( 'Deleting data from Courses', - DomainModel.COURSE, + DomainName.COURSE, userId, StatusModel.PENDING ) @@ -35,10 +37,18 @@ export class CourseService { courses.forEach((course: Course) => course.removeUser(userId)); await this.repo.save(courses); + + const result = DomainOperationBuilder.build( + DomainName.COURSE, + OperationType.UPDATE, + count, + this.getCoursesId(courses) + ); + this.logger.info( new DataDeletionDomainOperationLoggable( 'Successfully removed data from Courses', - DomainModel.COURSE, + DomainName.COURSE, userId, StatusModel.FINISHED, 0, @@ -46,7 +56,7 @@ export class CourseService { ) ); - return count; + return result; } async findAllByUserId(userId: EntityId): Promise { @@ -54,4 +64,8 @@ export class CourseService { return courses; } + + private getCoursesId(courses: Course[]): EntityId[] { + return courses.map((course) => course.id); + } } diff --git a/apps/server/src/modules/learnroom/service/coursegroup.service.spec.ts b/apps/server/src/modules/learnroom/service/coursegroup.service.spec.ts index 438be147390..47518b4aba2 100644 --- a/apps/server/src/modules/learnroom/service/coursegroup.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/coursegroup.service.spec.ts @@ -3,6 +3,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import { CourseGroupRepo, UserRepo } from '@shared/repo'; import { courseGroupFactory, setupEntities, userFactory } from '@shared/testing'; import { Logger } from '@src/core/logger'; +import { DomainOperationBuilder } from '@shared/domain/builder'; +import { DomainName, OperationType } from '@shared/domain/types'; import { CourseGroupService } from './coursegroup.service'; describe('CourseGroupService', () => { @@ -86,7 +88,13 @@ describe('CourseGroupService', () => { userRepo.findById.mockResolvedValue(user); courseGroupRepo.findByUserId.mockResolvedValue([[courseGroup1, courseGroup2], 2]); + const expectedResult = DomainOperationBuilder.build(DomainName.COURSEGROUP, OperationType.UPDATE, 2, [ + courseGroup1.id, + courseGroup2.id, + ]); + return { + expectedResult, user, }; }; @@ -100,11 +108,11 @@ describe('CourseGroupService', () => { }); it('should update courses without deleted user', async () => { - const { user } = setup(); + const { expectedResult, user } = setup(); const result = await courseGroupService.deleteUserDataFromCourseGroup(user.id); - expect(result).toEqual(2); + expect(result).toEqual(expectedResult); }); }); }); diff --git a/apps/server/src/modules/learnroom/service/coursegroup.service.ts b/apps/server/src/modules/learnroom/service/coursegroup.service.ts index b9da03616d2..f148f43838d 100644 --- a/apps/server/src/modules/learnroom/service/coursegroup.service.ts +++ b/apps/server/src/modules/learnroom/service/coursegroup.service.ts @@ -1,7 +1,9 @@ import { Injectable } from '@nestjs/common'; import { DataDeletionDomainOperationLoggable } from '@shared/common/loggable'; +import { DomainOperationBuilder } from '@shared/domain/builder'; import { CourseGroup } from '@shared/domain/entity'; -import { Counted, DomainModel, EntityId, StatusModel } from '@shared/domain/types'; +import { DomainOperation } from '@shared/domain/interface'; +import { Counted, DomainName, EntityId, OperationType, StatusModel } from '@shared/domain/types'; import { CourseGroupRepo } from '@shared/repo'; import { Logger } from '@src/core/logger'; @@ -17,11 +19,11 @@ export class CourseGroupService { return [courseGroups, count]; } - public async deleteUserDataFromCourseGroup(userId: EntityId): Promise { + public async deleteUserDataFromCourseGroup(userId: EntityId): Promise { this.logger.info( new DataDeletionDomainOperationLoggable( 'Deleting user data from CourseGroup', - DomainModel.COURSEGROUP, + DomainName.COURSEGROUP, userId, StatusModel.PENDING ) @@ -31,10 +33,18 @@ export class CourseGroupService { courseGroups.forEach((courseGroup) => courseGroup.removeStudent(userId)); await this.repo.save(courseGroups); + + const result = DomainOperationBuilder.build( + DomainName.COURSEGROUP, + OperationType.UPDATE, + count, + this.getCourseGroupsId(courseGroups) + ); + this.logger.info( new DataDeletionDomainOperationLoggable( 'Successfully deleted user data from CourseGroup', - DomainModel.COURSEGROUP, + DomainName.COURSEGROUP, userId, StatusModel.FINISHED, count, @@ -42,6 +52,10 @@ export class CourseGroupService { ) ); - return count; + return result; + } + + private getCourseGroupsId(courseGroups: CourseGroup[]): EntityId[] { + return courseGroups.map((courseGroup) => courseGroup.id); } } diff --git a/apps/server/src/modules/learnroom/service/dashboard.service.spec.ts b/apps/server/src/modules/learnroom/service/dashboard.service.spec.ts index 51598bb1dc7..93d3497677a 100644 --- a/apps/server/src/modules/learnroom/service/dashboard.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/dashboard.service.spec.ts @@ -2,9 +2,11 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { DashboardElementRepo, IDashboardRepo, UserRepo } from '@shared/repo'; import { setupEntities, userFactory } from '@shared/testing'; -import { LearnroomMetadata, LearnroomTypes } from '@shared/domain/types'; +import { DomainName, LearnroomMetadata, LearnroomTypes, OperationType } from '@shared/domain/types'; import { DashboardEntity, GridElement } from '@shared/domain/entity'; import { Logger } from '@src/core/logger'; +import { DomainOperationBuilder } from '@shared/domain/builder'; +import { ObjectId } from 'bson'; import { DashboardService } from '.'; const learnroomMock = (id: string, name: string) => { @@ -68,9 +70,21 @@ describe(DashboardService.name, () => { describe('when deleting dashboard by userId', () => { const setup = () => { const user = userFactory.buildWithId(); + const dashboardId = new ObjectId().toHexString(); + const dashboard = new DashboardEntity(dashboardId, { + grid: [ + { + pos: { x: 1, y: 2 }, + gridElement: GridElement.FromPersistedReference('elementId', learnroomMock('referenceId', 'Mathe')), + }, + ], + userId: user.id, + }); userRepo.findById.mockResolvedValue(user); - return { user }; + const expectedResult = DomainOperationBuilder.build(DomainName.DASHBOARD, OperationType.DELETE, 1, [dashboardId]); + + return { dashboard, expectedResult, user }; }; describe('when dashboard exist', () => { @@ -84,23 +98,13 @@ describe(DashboardService.name, () => { }); it('should call dashboardElementRepo.deleteByDashboardId', async () => { - const { user } = setup(); - jest.spyOn(dashboardRepo, 'getUsersDashboardIfExist').mockResolvedValueOnce( - new DashboardEntity('dashboardId', { - grid: [ - { - pos: { x: 1, y: 2 }, - gridElement: GridElement.FromPersistedReference('elementId', learnroomMock('referenceId', 'Mathe')), - }, - ], - userId: 'userId', - }) - ); + const { dashboard, user } = setup(); + jest.spyOn(dashboardRepo, 'getUsersDashboardIfExist').mockResolvedValueOnce(dashboard); const spy = jest.spyOn(dashboardElementRepo, 'deleteByDashboardId'); await dashboardService.deleteDashboardByUserId(user.id); - expect(spy).toHaveBeenCalledWith('dashboardId'); + expect(spy).toHaveBeenCalledWith(dashboard.id); }); it('should call dashboardRepo.deleteDashboardByUserId', async () => { @@ -113,12 +117,13 @@ describe(DashboardService.name, () => { }); it('should delete users dashboard', async () => { - const { user } = setup(); + const { dashboard, expectedResult, user } = setup(); + jest.spyOn(dashboardRepo, 'getUsersDashboardIfExist').mockResolvedValueOnce(dashboard); jest.spyOn(dashboardRepo, 'deleteDashboardByUserId').mockImplementation(() => Promise.resolve(1)); const result = await dashboardService.deleteDashboardByUserId(user.id); - expect(result).toEqual(1); + expect(result).toEqual(expectedResult); }); }); }); diff --git a/apps/server/src/modules/learnroom/service/dashboard.service.ts b/apps/server/src/modules/learnroom/service/dashboard.service.ts index b4851749a1a..e58f5601518 100644 --- a/apps/server/src/modules/learnroom/service/dashboard.service.ts +++ b/apps/server/src/modules/learnroom/service/dashboard.service.ts @@ -1,6 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { DataDeletionDomainOperationLoggable } from '@shared/common/loggable'; -import { DomainModel, EntityId, StatusModel } from '@shared/domain/types'; +import { DomainOperationBuilder } from '@shared/domain/builder'; +import { DomainOperation } from '@shared/domain/interface'; +import { DomainName, EntityId, OperationType, StatusModel } from '@shared/domain/types'; import { IDashboardRepo, DashboardElementRepo } from '@shared/repo'; import { Logger } from '@src/core/logger'; @@ -14,30 +16,34 @@ export class DashboardService { this.logger.setContext(DashboardService.name); } - async deleteDashboardByUserId(userId: EntityId): Promise { + async deleteDashboardByUserId(userId: EntityId): Promise { this.logger.info( new DataDeletionDomainOperationLoggable( 'Deleting user data from Dashboard', - DomainModel.DASHBOARD, + DomainName.DASHBOARD, userId, StatusModel.PENDING ) ); - let result = 0; + let deletedDashboard = 0; + const refs: string[] = []; const usersDashboard = await this.dashboardRepo.getUsersDashboardIfExist(userId); if (usersDashboard !== null) { await this.dashboardElementRepo.deleteByDashboardId(usersDashboard.id); - result = await this.dashboardRepo.deleteDashboardByUserId(userId); + deletedDashboard = await this.dashboardRepo.deleteDashboardByUserId(userId); + refs.push(usersDashboard.id); } + const result = DomainOperationBuilder.build(DomainName.DASHBOARD, OperationType.DELETE, deletedDashboard, refs); + this.logger.info( new DataDeletionDomainOperationLoggable( 'Successfully deleted user data from Dashboard', - DomainModel.DASHBOARD, + DomainName.DASHBOARD, userId, StatusModel.FINISHED, 0, - result + deletedDashboard ) ); diff --git a/apps/server/src/modules/lesson/service/lesson.service.spec.ts b/apps/server/src/modules/lesson/service/lesson.service.spec.ts index 9eba963ad8a..57e42e6ccd8 100644 --- a/apps/server/src/modules/lesson/service/lesson.service.spec.ts +++ b/apps/server/src/modules/lesson/service/lesson.service.spec.ts @@ -5,6 +5,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ComponentProperties, ComponentType } from '@shared/domain/entity'; import { lessonFactory, setupEntities } from '@shared/testing'; import { Logger } from '@src/core/logger'; +import { DomainOperationBuilder } from '@shared/domain/builder'; +import { DomainName, OperationType } from '@shared/domain/types'; import { LessonRepo } from '../repository'; import { LessonService } from './lesson.service'; @@ -149,7 +151,13 @@ describe('LessonService', () => { lessonRepo.findByUserId.mockResolvedValue([lesson1, lesson2]); + const expectedResult = DomainOperationBuilder.build(DomainName.LESSONS, OperationType.UPDATE, 2, [ + lesson1.id, + lesson2.id, + ]); + return { + expectedResult, userId, }; }; @@ -163,11 +171,11 @@ describe('LessonService', () => { }); it('should update lessons without deleted user', async () => { - const { userId } = setup(); + const { expectedResult, userId } = setup(); const result = await lessonService.deleteUserDataFromLessons(userId); - expect(result).toEqual(2); + expect(result).toEqual(expectedResult); }); }); }); diff --git a/apps/server/src/modules/lesson/service/lesson.service.ts b/apps/server/src/modules/lesson/service/lesson.service.ts index af38c26df8d..307110a11b1 100644 --- a/apps/server/src/modules/lesson/service/lesson.service.ts +++ b/apps/server/src/modules/lesson/service/lesson.service.ts @@ -1,10 +1,12 @@ import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; import { Injectable } from '@nestjs/common'; import { ComponentProperties, LessonEntity } from '@shared/domain/entity'; -import { Counted, DomainModel, EntityId, StatusModel } from '@shared/domain/types'; +import { Counted, DomainName, EntityId, OperationType, StatusModel } from '@shared/domain/types'; import { AuthorizationLoaderService } from '@src/modules/authorization'; import { Logger } from '@src/core/logger'; import { DataDeletionDomainOperationLoggable } from '@shared/common/loggable'; +import { DomainOperation } from '@shared/domain/interface'; +import { DomainOperationBuilder } from '@shared/domain/builder'; import { LessonRepo } from '../repository'; @Injectable() @@ -37,11 +39,11 @@ export class LessonService implements AuthorizationLoaderService { return lessons; } - async deleteUserDataFromLessons(userId: EntityId): Promise { + async deleteUserDataFromLessons(userId: EntityId): Promise { this.logger.info( new DataDeletionDomainOperationLoggable( 'Deleting user data from Lessons', - DomainModel.LESSONS, + DomainName.LESSONS, userId, StatusModel.PENDING ) @@ -62,10 +64,17 @@ export class LessonService implements AuthorizationLoaderService { const numberOfUpdatedLessons = updatedLessons.length; + const result = DomainOperationBuilder.build( + DomainName.LESSONS, + OperationType.UPDATE, + numberOfUpdatedLessons, + this.getLessonsId(updatedLessons) + ); + this.logger.info( new DataDeletionDomainOperationLoggable( 'Successfully removed user data from Classes', - DomainModel.LESSONS, + DomainName.LESSONS, userId, StatusModel.FINISHED, numberOfUpdatedLessons, @@ -73,6 +82,10 @@ export class LessonService implements AuthorizationLoaderService { ) ); - return numberOfUpdatedLessons; + return result; + } + + private getLessonsId(lessons: LessonEntity[]): EntityId[] { + return lessons.map((lesson) => lesson.id); } } diff --git a/apps/server/src/modules/pseudonym/repo/external-tool-pseudonym.repo.integration.spec.ts b/apps/server/src/modules/pseudonym/repo/external-tool-pseudonym.repo.integration.spec.ts index 1b9e8e64843..bfd074b2599 100644 --- a/apps/server/src/modules/pseudonym/repo/external-tool-pseudonym.repo.integration.spec.ts +++ b/apps/server/src/modules/pseudonym/repo/external-tool-pseudonym.repo.integration.spec.ts @@ -240,6 +240,24 @@ describe('ExternalToolPseudonymRepo', () => { }); describe('deletePseudonymsByUserId', () => { + describe('when pseudonyms are not existing', () => { + const setup = () => { + const user = userFactory.buildWithId(); + + return { + user, + }; + }; + + it('should return empty array', async () => { + const { user } = setup(); + + const result = await repo.deletePseudonymsByUserId(user.id); + + expect(result).toEqual([]); + }); + }); + describe('when pseudonyms are existing', () => { const setup = async () => { const user1 = userFactory.buildWithId(); @@ -259,7 +277,10 @@ describe('ExternalToolPseudonymRepo', () => { await em.persistAndFlush([pseudonym1, pseudonym2, pseudonym3, pseudonym4]); + const expectedResult = [pseudonym1.id, pseudonym2.id]; + return { + expectedResult, user1, pseudonym1, pseudonym2, @@ -267,18 +288,11 @@ describe('ExternalToolPseudonymRepo', () => { }; it('should delete all pseudonyms for userId', async () => { - const { user1 } = await setup(); + const { expectedResult, user1 } = await setup(); - const result: number = await repo.deletePseudonymsByUserId(user1.id); - - expect(result).toEqual(2); - }); - }); + const result = await repo.deletePseudonymsByUserId(user1.id); - describe('should return empty array when there is no pseudonym', () => { - it('should return empty array', async () => { - const result: Pseudonym[] = await repo.findByUserId(new ObjectId().toHexString()); - expect(result).toHaveLength(0); + expect(result).toEqual(expectedResult); }); }); }); diff --git a/apps/server/src/modules/pseudonym/repo/external-tool-pseudonym.repo.ts b/apps/server/src/modules/pseudonym/repo/external-tool-pseudonym.repo.ts index 77f90894e47..7b64c67e61b 100644 --- a/apps/server/src/modules/pseudonym/repo/external-tool-pseudonym.repo.ts +++ b/apps/server/src/modules/pseudonym/repo/external-tool-pseudonym.repo.ts @@ -68,12 +68,17 @@ export class ExternalToolPseudonymRepo { return savedDomainObject; } - async deletePseudonymsByUserId(userId: EntityId): Promise { - const promise: Promise = this.em.nativeDelete(ExternalToolPseudonymEntity, { - userId: new ObjectId(userId), - }); + async deletePseudonymsByUserId(userId: EntityId): Promise { + const externalPseudonyms = await this.em.find(ExternalToolPseudonymEntity, { userId: new ObjectId(userId) }); + if (externalPseudonyms.length === 0) { + return []; + } + + const removePromises = externalPseudonyms.map((externalPseudonym) => this.em.removeAndFlush(externalPseudonym)); - return promise; + await Promise.all(removePromises); + + return this.getExternalPseudonymId(externalPseudonyms); } async findPseudonymByPseudonym(pseudonym: string): Promise { @@ -90,6 +95,10 @@ export class ExternalToolPseudonymRepo { return domainObject; } + private getExternalPseudonymId(externalPseudonyms: ExternalToolPseudonymEntity[]): EntityId[] { + return externalPseudonyms.map((externalPseudonym) => externalPseudonym.id); + } + protected mapEntityToDomainObject(entity: ExternalToolPseudonymEntity): Pseudonym { const pseudonym = new Pseudonym({ id: entity.id, diff --git a/apps/server/src/modules/pseudonym/repo/pseudonyms.repo.spec.ts b/apps/server/src/modules/pseudonym/repo/pseudonyms.repo.spec.ts index afa77d64935..404ada59a66 100644 --- a/apps/server/src/modules/pseudonym/repo/pseudonyms.repo.spec.ts +++ b/apps/server/src/modules/pseudonym/repo/pseudonyms.repo.spec.ts @@ -230,6 +230,23 @@ describe('PseudonymRepo', () => { }); describe('deletePseudonymsByUserId', () => { + describe('when pseudonyms are not existing', () => { + const setup = () => { + const user = userFactory.buildWithId(); + + return { + user, + }; + }; + + it('should return empty array', async () => { + const { user } = setup(); + + const result = await repo.deletePseudonymsByUserId(user.id); + + expect(result).toEqual([]); + }); + }); describe('when pseudonyms are existing', () => { const setup = async () => { const user1 = userFactory.buildWithId(); @@ -241,24 +258,20 @@ describe('PseudonymRepo', () => { await em.persistAndFlush([pseudonym1, pseudonym2, pseudonym3, pseudonym4]); + const expectedResult = [pseudonym1.id, pseudonym2.id]; + return { + expectedResult, user1, }; }; it('should delete all pseudonyms for userId', async () => { - const { user1 } = await setup(); - - const result: number = await repo.deletePseudonymsByUserId(user1.id); + const { expectedResult, user1 } = await setup(); - expect(result).toEqual(2); - }); - }); + const result = await repo.deletePseudonymsByUserId(user1.id); - describe('should return empty array when there is no pseudonym', () => { - it('should return empty array', async () => { - const result: Pseudonym[] = await repo.findByUserId(new ObjectId().toHexString()); - expect(result).toHaveLength(0); + expect(result).toEqual(expectedResult); }); }); }); diff --git a/apps/server/src/modules/pseudonym/repo/pseudonyms.repo.ts b/apps/server/src/modules/pseudonym/repo/pseudonyms.repo.ts index d0979e91fca..2ba378ea73a 100644 --- a/apps/server/src/modules/pseudonym/repo/pseudonyms.repo.ts +++ b/apps/server/src/modules/pseudonym/repo/pseudonyms.repo.ts @@ -63,10 +63,21 @@ export class PseudonymsRepo { return savedDomainObject; } - async deletePseudonymsByUserId(userId: EntityId): Promise { - const promise: Promise = this.em.nativeDelete(PseudonymEntity, { userId: new ObjectId(userId) }); + async deletePseudonymsByUserId(userId: EntityId): Promise { + const pseudonyms = await this.em.find(PseudonymEntity, { userId: new ObjectId(userId) }); + if (pseudonyms.length === 0) { + return []; + } + + const removePromises = pseudonyms.map((pseudonym) => this.em.removeAndFlush(pseudonym)); + + await Promise.all(removePromises); + + return this.getPseudonymId(pseudonyms); + } - return promise; + private getPseudonymId(pseudonyms: PseudonymEntity[]): EntityId[] { + return pseudonyms.map((pseudonym) => pseudonym.id); } protected mapEntityToDomainObject(entity: PseudonymEntity): Pseudonym { diff --git a/apps/server/src/modules/pseudonym/service/pseudonym.service.spec.ts b/apps/server/src/modules/pseudonym/service/pseudonym.service.spec.ts index 53c1c58cf3b..dbe080cd227 100644 --- a/apps/server/src/modules/pseudonym/service/pseudonym.service.spec.ts +++ b/apps/server/src/modules/pseudonym/service/pseudonym.service.spec.ts @@ -8,6 +8,9 @@ import { IFindOptions } from '@shared/domain/interface'; import { LtiToolDO, Page, Pseudonym, UserDO } from '@shared/domain/domainobject'; import { externalToolFactory, ltiToolDOFactory, pseudonymFactory, userDoFactory } from '@shared/testing/factory'; import { Logger } from '@src/core/logger'; +import { ObjectId } from 'bson'; +import { DomainOperationBuilder } from '@shared/domain/builder'; +import { DomainName, OperationType } from '@shared/domain/types'; import { PseudonymSearchQuery } from '../domain'; import { ExternalToolPseudonymRepo, PseudonymsRepo } from '../repo'; import { PseudonymService } from './pseudonym.service'; @@ -412,21 +415,35 @@ describe('PseudonymService', () => { describe('when deleting by userId', () => { const setup = () => { const user: UserDO = userDoFactory.buildWithId(); + const pseudonymsDeleted = [new ObjectId().toHexString(), new ObjectId().toHexString()]; + const externalPseudonymsDeleted = [ + new ObjectId().toHexString(), + new ObjectId().toHexString(), + new ObjectId().toHexString(), + ]; + + const expectedResult = DomainOperationBuilder.build( + DomainName.PSEUDONYMS, + OperationType.DELETE, + pseudonymsDeleted.length + externalPseudonymsDeleted.length, + [...pseudonymsDeleted, ...externalPseudonymsDeleted] + ); - pseudonymRepo.deletePseudonymsByUserId.mockResolvedValue(2); - externalToolPseudonymRepo.deletePseudonymsByUserId.mockResolvedValue(3); + pseudonymRepo.deletePseudonymsByUserId.mockResolvedValue(pseudonymsDeleted); + externalToolPseudonymRepo.deletePseudonymsByUserId.mockResolvedValue(externalPseudonymsDeleted); return { + expectedResult, user, }; }; it('should delete pseudonyms by userId', async () => { - const { user } = setup(); + const { expectedResult, user } = setup(); - const result5 = await service.deleteByUserId(user.id as string); + const result = await service.deleteByUserId(user.id as string); - expect(result5).toEqual(5); + expect(result).toEqual(expectedResult); }); }); }); diff --git a/apps/server/src/modules/pseudonym/service/pseudonym.service.ts b/apps/server/src/modules/pseudonym/service/pseudonym.service.ts index 7fbfd09464b..47d3f6239f1 100644 --- a/apps/server/src/modules/pseudonym/service/pseudonym.service.ts +++ b/apps/server/src/modules/pseudonym/service/pseudonym.service.ts @@ -3,11 +3,12 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { ExternalTool } from '@modules/tool/external-tool/domain'; import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { LtiToolDO, Page, Pseudonym, UserDO } from '@shared/domain/domainobject'; -import { IFindOptions } from '@shared/domain/interface'; +import { DomainOperation, IFindOptions } from '@shared/domain/interface'; import { v4 as uuidv4 } from 'uuid'; import { Logger } from '@src/core/logger'; import { DataDeletionDomainOperationLoggable } from '@shared/common/loggable'; -import { DomainModel, StatusModel } from '@shared/domain/types'; +import { DomainName, EntityId, OperationType, StatusModel } from '@shared/domain/types'; +import { DomainOperationBuilder } from '@shared/domain/builder'; import { PseudonymSearchQuery } from '../domain'; import { ExternalToolPseudonymRepo, PseudonymsRepo } from '../repo'; @@ -78,11 +79,11 @@ export class PseudonymService { return pseudonym; } - public async deleteByUserId(userId: string): Promise { + public async deleteByUserId(userId: string): Promise { this.logger.info( new DataDeletionDomainOperationLoggable( 'Deleting user data from Pseudonyms', - DomainModel.PSEUDONYMS, + DomainName.PSEUDONYMS, userId, StatusModel.PENDING ) @@ -96,12 +97,19 @@ export class PseudonymService { this.deleteExternalToolPseudonymsByUserId(userId), ]); - const numberOfDeletedPseudonyms = deletedPseudonyms + deletedExternalToolPseudonyms; + const numberOfDeletedPseudonyms = deletedPseudonyms.length + deletedExternalToolPseudonyms.length; + + const result = DomainOperationBuilder.build( + DomainName.PSEUDONYMS, + OperationType.DELETE, + numberOfDeletedPseudonyms, + [...deletedPseudonyms, ...deletedExternalToolPseudonyms] + ); this.logger.info( new DataDeletionDomainOperationLoggable( 'Successfully deleted user data from Pseudonyms', - DomainModel.PSEUDONYMS, + DomainName.PSEUDONYMS, userId, StatusModel.FINISHED, 0, @@ -109,7 +117,7 @@ export class PseudonymService { ) ); - return numberOfDeletedPseudonyms; + return result; } private async findPseudonymsByUserId(userId: string): Promise { @@ -124,14 +132,14 @@ export class PseudonymService { return externalToolPseudonymPromise; } - private async deletePseudonymsByUserId(userId: string): Promise { - const pseudonymPromise: Promise = this.pseudonymRepo.deletePseudonymsByUserId(userId); + private async deletePseudonymsByUserId(userId: string): Promise { + const pseudonymPromise: Promise = this.pseudonymRepo.deletePseudonymsByUserId(userId); return pseudonymPromise; } - private async deleteExternalToolPseudonymsByUserId(userId: string): Promise { - const externalToolPseudonymPromise: Promise = + private async deleteExternalToolPseudonymsByUserId(userId: string): Promise { + const externalToolPseudonymPromise: Promise = this.externalToolPseudonymRepo.deletePseudonymsByUserId(userId); return externalToolPseudonymPromise; diff --git a/apps/server/src/modules/registration-pin/repo/registration-pin.repo.spec.ts b/apps/server/src/modules/registration-pin/repo/registration-pin.repo.spec.ts index c357351fa37..785829f14fd 100644 --- a/apps/server/src/modules/registration-pin/repo/registration-pin.repo.spec.ts +++ b/apps/server/src/modules/registration-pin/repo/registration-pin.repo.spec.ts @@ -36,7 +36,10 @@ describe(RegistrationPinRepo.name, () => { await em.persistAndFlush(registrationPinForUser); + const expectedResult = registrationPinForUser.id; + return { + expectedResult, user, userWithoutRegistrationPin, }; @@ -44,11 +47,11 @@ describe(RegistrationPinRepo.name, () => { describe('when registrationPin exists', () => { it('should delete registrationPins by email', async () => { - const { user } = await setup(); + const { expectedResult, user } = await setup(); - const result: number = await repo.deleteRegistrationPinByEmail(user.email); + const result = await repo.deleteRegistrationPinByEmail(user.email); - expect(result).toEqual(1); + expect(result).toEqual(expectedResult); }); }); @@ -56,8 +59,8 @@ describe(RegistrationPinRepo.name, () => { it('should return empty array', async () => { const { userWithoutRegistrationPin } = await setup(); - const result: number = await repo.deleteRegistrationPinByEmail(userWithoutRegistrationPin.email); - expect(result).toEqual(0); + const result = await repo.deleteRegistrationPinByEmail(userWithoutRegistrationPin.email); + expect(result).toBeNull(); }); }); }); diff --git a/apps/server/src/modules/registration-pin/repo/registration-pin.repo.ts b/apps/server/src/modules/registration-pin/repo/registration-pin.repo.ts index 6ca68bc089d..95b53002c37 100644 --- a/apps/server/src/modules/registration-pin/repo/registration-pin.repo.ts +++ b/apps/server/src/modules/registration-pin/repo/registration-pin.repo.ts @@ -1,14 +1,20 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain/types'; import { RegistrationPinEntity } from '../entity'; @Injectable() export class RegistrationPinRepo { constructor(private readonly em: EntityManager) {} - async deleteRegistrationPinByEmail(email: string): Promise { - const promise: Promise = this.em.nativeDelete(RegistrationPinEntity, { email }); + async deleteRegistrationPinByEmail(email: string): Promise { + const registrationPin = await this.em.findOne(RegistrationPinEntity, { email }); + if (registrationPin === null) { + return null; + } - return promise; + await this.em.removeAndFlush(registrationPin); + + return registrationPin.id; } } diff --git a/apps/server/src/modules/registration-pin/service/registration-pin.service.spec.ts b/apps/server/src/modules/registration-pin/service/registration-pin.service.spec.ts index c5a7545c32f..076fa01b7a2 100644 --- a/apps/server/src/modules/registration-pin/service/registration-pin.service.spec.ts +++ b/apps/server/src/modules/registration-pin/service/registration-pin.service.spec.ts @@ -2,8 +2,11 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { setupEntities, userDoFactory } from '@shared/testing'; import { Logger } from '@src/core/logger'; -import { RegistrationPinService } from '.'; +import { DomainOperationBuilder } from '@shared/domain/builder'; +import { DomainName, OperationType } from '@shared/domain/types'; +import { ObjectId } from 'bson'; import { RegistrationPinRepo } from '../repo'; +import { RegistrationPinService } from '.'; describe(RegistrationPinService.name, () => { let module: TestingModule; @@ -40,13 +43,42 @@ describe(RegistrationPinService.name, () => { }); describe('deleteRegistrationPinByEmail', () => { - describe('when deleting registrationPin', () => { + describe('when there is no registrationPin', () => { const setup = () => { const user = userDoFactory.buildWithId(); - registrationPinRepo.deleteRegistrationPinByEmail.mockResolvedValueOnce(1); + registrationPinRepo.deleteRegistrationPinByEmail.mockResolvedValueOnce(null); + + const expectedResult = DomainOperationBuilder.build(DomainName.REGISTRATIONPIN, OperationType.DELETE, 0, []); return { + expectedResult, + user, + }; + }; + + it('should return domainOperation object with proper values', async () => { + const { expectedResult, user } = setup(); + + const result = await service.deleteRegistrationPinByEmail(user.email); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('when deleting existing registrationPin', () => { + const setup = () => { + const user = userDoFactory.buildWithId(); + const registrationPinId = new ObjectId().toHexString(); + + registrationPinRepo.deleteRegistrationPinByEmail.mockResolvedValueOnce(registrationPinId); + + const expectedResult = DomainOperationBuilder.build(DomainName.REGISTRATIONPIN, OperationType.DELETE, 1, [ + registrationPinId, + ]); + + return { + expectedResult, user, }; }; @@ -60,11 +92,11 @@ describe(RegistrationPinService.name, () => { }); it('should delete registrationPin by email', async () => { - const { user } = setup(); + const { expectedResult, user } = setup(); - const result: number = await service.deleteRegistrationPinByEmail(user.email); + const result = await service.deleteRegistrationPinByEmail(user.email); - expect(result).toEqual(1); + expect(result).toEqual(expectedResult); }); }); }); diff --git a/apps/server/src/modules/registration-pin/service/registration-pin.service.ts b/apps/server/src/modules/registration-pin/service/registration-pin.service.ts index 8b750802268..00be7ae2e85 100644 --- a/apps/server/src/modules/registration-pin/service/registration-pin.service.ts +++ b/apps/server/src/modules/registration-pin/service/registration-pin.service.ts @@ -1,7 +1,9 @@ import { Injectable } from '@nestjs/common'; import { Logger } from '@src/core/logger'; import { DataDeletionDomainOperationLoggable } from '@shared/common/loggable'; -import { DomainModel, StatusModel } from '@shared/domain/types'; +import { DomainName, OperationType, StatusModel } from '@shared/domain/types'; +import { DomainOperation } from '@shared/domain/interface'; +import { DomainOperationBuilder } from '@shared/domain/builder'; import { RegistrationPinRepo } from '../repo'; @Injectable() @@ -10,24 +12,36 @@ export class RegistrationPinService { this.logger.setContext(RegistrationPinService.name); } - async deleteRegistrationPinByEmail(email: string): Promise { + async deleteRegistrationPinByEmail(email: string): Promise { this.logger.info( new DataDeletionDomainOperationLoggable( 'Deleting user data from RegistrationPin', - DomainModel.REGISTRATIONPIN, + DomainName.REGISTRATIONPIN, email, StatusModel.PENDING ) ); - const result = await this.registrationPinRepo.deleteRegistrationPinByEmail(email); + const response = await this.registrationPinRepo.deleteRegistrationPinByEmail(email); + + const deletedRegistrationPins = response !== null ? [response] : []; + + const numberOfDeletedRegistrationPins = deletedRegistrationPins.length; + + const result = DomainOperationBuilder.build( + DomainName.REGISTRATIONPIN, + OperationType.DELETE, + numberOfDeletedRegistrationPins, + deletedRegistrationPins + ); + this.logger.info( new DataDeletionDomainOperationLoggable( 'Successfully deleted user data from RegistrationPin', - DomainModel.REGISTRATIONPIN, + DomainName.REGISTRATIONPIN, email, StatusModel.FINISHED, 0, - result + numberOfDeletedRegistrationPins ) ); diff --git a/apps/server/src/modules/rocketchat-user/service/rocket-chat-user.service.spec.ts b/apps/server/src/modules/rocketchat-user/service/rocket-chat-user.service.spec.ts index 6195116b5d7..76e2c3dee38 100644 --- a/apps/server/src/modules/rocketchat-user/service/rocket-chat-user.service.spec.ts +++ b/apps/server/src/modules/rocketchat-user/service/rocket-chat-user.service.spec.ts @@ -3,6 +3,9 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { setupEntities } from '@shared/testing'; import { Logger } from '@src/core/logger'; +import { DomainName, OperationType } from '@shared/domain/types'; +import { DomainOperationBuilder } from '@shared/domain/builder'; +import { DomainOperation } from '@shared/domain/interface'; import { RocketChatUserService } from './rocket-chat-user.service'; import { RocketChatUserRepo } from '../repo'; import { rocketChatUserFactory } from '../domain/testing/rocket-chat-user.factory'; @@ -71,11 +74,18 @@ describe(RocketChatUserService.name, () => { describe('when deleting rocketChatUser', () => { const setup = () => { const userId = new ObjectId().toHexString(); + const rocketChatUser: RocketChatUser = rocketChatUserFactory.build(); + rocketChatUserRepo.findByUserId.mockResolvedValueOnce([rocketChatUser]); rocketChatUserRepo.deleteByUserId.mockResolvedValueOnce(1); + const expectedResult = DomainOperationBuilder.build(DomainName.ROCKETCHATUSER, OperationType.DELETE, 1, [ + rocketChatUser.id, + ]); + return { userId, + expectedResult, }; }; @@ -84,15 +94,16 @@ describe(RocketChatUserService.name, () => { await service.deleteByUserId(userId); + expect(rocketChatUserRepo.findByUserId).toBeCalledWith(userId); expect(rocketChatUserRepo.deleteByUserId).toBeCalledWith(userId); }); it('should delete rocketChatUser by userId', async () => { - const { userId } = setup(); + const { userId, expectedResult } = setup(); - const result: number = await service.deleteByUserId(userId); + const result: DomainOperation = await service.deleteByUserId(userId); - expect(result).toEqual(1); + expect(result).toEqual(expectedResult); }); }); }); diff --git a/apps/server/src/modules/rocketchat-user/service/rocket-chat-user.service.ts b/apps/server/src/modules/rocketchat-user/service/rocket-chat-user.service.ts index a6c1bcdfeb8..484b4cabba0 100644 --- a/apps/server/src/modules/rocketchat-user/service/rocket-chat-user.service.ts +++ b/apps/server/src/modules/rocketchat-user/service/rocket-chat-user.service.ts @@ -1,7 +1,9 @@ import { Injectable } from '@nestjs/common'; -import { DomainModel, EntityId, StatusModel } from '@shared/domain/types'; +import { DomainName, EntityId, OperationType, StatusModel } from '@shared/domain/types'; import { Logger } from '@src/core/logger'; import { DataDeletionDomainOperationLoggable } from '@shared/common/loggable'; +import { DomainOperationBuilder } from '@shared/domain/builder'; +import { DomainOperation } from '@shared/domain/interface'; import { RocketChatUser } from '../domain'; import { RocketChatUserRepo } from '../repo'; @@ -17,21 +19,27 @@ export class RocketChatUserService { return user; } - public async deleteByUserId(userId: EntityId): Promise { + public async deleteByUserId(userId: EntityId): Promise { this.logger.info( new DataDeletionDomainOperationLoggable( 'Deleting user from rocket chat', - DomainModel.ROCKETCHATUSER, + DomainName.ROCKETCHATUSER, userId, StatusModel.PENDING ) ); + const rocketChatUser = await this.rocketChatUserRepo.findByUserId(userId); + const deletedRocketChatUser = await this.rocketChatUserRepo.deleteByUserId(userId); + const result = DomainOperationBuilder.build(DomainName.ROCKETCHATUSER, OperationType.DELETE, 1, [ + rocketChatUser[0].id, + ]); + this.logger.info( new DataDeletionDomainOperationLoggable( 'Successfully deleted user from rocket chat', - DomainModel.ROCKETCHATUSER, + DomainName.ROCKETCHATUSER, userId, StatusModel.FINISHED, 0, @@ -39,6 +47,6 @@ export class RocketChatUserService { ) ); - return deletedRocketChatUser; + return result; } } diff --git a/apps/server/src/modules/task/service/task.service.spec.ts b/apps/server/src/modules/task/service/task.service.spec.ts index 913ae9745b2..10a8035e04d 100644 --- a/apps/server/src/modules/task/service/task.service.spec.ts +++ b/apps/server/src/modules/task/service/task.service.spec.ts @@ -3,7 +3,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { TaskRepo } from '@shared/repo'; import { courseFactory, setupEntities, submissionFactory, taskFactory, userFactory } from '@shared/testing'; import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; -import { DomainModel } from '@shared/domain/types'; +import { DomainName, OperationType } from '@shared/domain/types'; import { DomainOperationBuilder } from '@shared/domain/builder'; import { Logger } from '@src/core/logger'; import { SubmissionService } from './submission.service'; @@ -118,7 +118,9 @@ describe('TaskService', () => { taskRepo.findByOnlyCreatorId.mockResolvedValue([[taskWithoutCourse], 1]); - const expectedResult = DomainOperationBuilder.build(DomainModel.TASK, 0, 1); + const expectedResult = DomainOperationBuilder.build(DomainName.TASK, OperationType.DELETE, 1, [ + taskWithoutCourse.id, + ]); return { creator, expectedResult }; }; @@ -150,7 +152,9 @@ describe('TaskService', () => { taskRepo.findByCreatorIdWithCourseAndLesson.mockResolvedValue([[taskWithCourse], 1]); - const expectedResult = DomainOperationBuilder.build(DomainModel.TASK, 1, 0); + const expectedResult = DomainOperationBuilder.build(DomainName.TASK, OperationType.UPDATE, 1, [ + taskWithCourse.id, + ]); const taskWithCourseToUpdate = { ...taskWithCourse, creator: undefined }; return { creator, expectedResult, taskWithCourseToUpdate }; @@ -190,7 +194,9 @@ describe('TaskService', () => { taskRepo.findByUserIdInFinished.mockResolvedValue([[finishedTask], 1]); - const expectedResult = DomainOperationBuilder.build(DomainModel.TASK, 1, 0); + const expectedResult = DomainOperationBuilder.build(DomainName.TASK, OperationType.UPDATE, 1, [ + finishedTask.id, + ]); return { creator, expectedResult }; }; diff --git a/apps/server/src/modules/task/service/task.service.ts b/apps/server/src/modules/task/service/task.service.ts index 337358f10fd..7be7defcd12 100644 --- a/apps/server/src/modules/task/service/task.service.ts +++ b/apps/server/src/modules/task/service/task.service.ts @@ -2,7 +2,7 @@ import { FilesStorageClientAdapterService } from '@modules/files-storage-client' import { Injectable } from '@nestjs/common'; import { Task } from '@shared/domain/entity'; import { DomainOperation, IFindOptions } from '@shared/domain/interface'; -import { Counted, DomainModel, EntityId, StatusModel } from '@shared/domain/types'; +import { Counted, DomainName, EntityId, OperationType, StatusModel } from '@shared/domain/types'; import { TaskRepo } from '@shared/repo'; import { DomainOperationBuilder } from '@shared/domain/builder'; import { Logger } from '@src/core/logger'; @@ -52,7 +52,7 @@ export class TaskService { this.logger.info( new DataDeletionDomainOperationLoggable( 'Deleting data from Task', - DomainModel.TASK, + DomainName.TASK, creatorId, StatusModel.PENDING ) @@ -65,11 +65,16 @@ export class TaskService { await Promise.all(promiseDeletedTasks); } - const result = DomainOperationBuilder.build(DomainModel.TASK, 0, counterOfTasksOnlyWithCreatorId); + const result = DomainOperationBuilder.build( + DomainName.TASK, + OperationType.DELETE, + counterOfTasksOnlyWithCreatorId, + this.getTasksId(tasksByOnlyCreatorId) + ); this.logger.info( new DataDeletionDomainOperationLoggable( 'Successfully deleted data from Task', - DomainModel.TASK, + DomainName.TASK, creatorId, StatusModel.FINISHED, counterOfTasksOnlyWithCreatorId, @@ -84,7 +89,7 @@ export class TaskService { this.logger.info( new DataDeletionDomainOperationLoggable( 'Deleting user data from Task', - DomainModel.TASK, + DomainName.TASK, creatorId, StatusModel.PENDING ) @@ -97,11 +102,16 @@ export class TaskService { await this.taskRepo.save(tasksByCreatorIdWithCoursesAndLessons); } - const result = DomainOperationBuilder.build(DomainModel.TASK, counterOfTasksWithCoursesorLessons, 0); + const result = DomainOperationBuilder.build( + DomainName.TASK, + OperationType.UPDATE, + counterOfTasksWithCoursesorLessons, + this.getTasksId(tasksByCreatorIdWithCoursesAndLessons) + ); this.logger.info( new DataDeletionDomainOperationLoggable( 'Successfully deleted user data from Task', - DomainModel.TASK, + DomainName.TASK, creatorId, StatusModel.FINISHED, counterOfTasksWithCoursesorLessons, @@ -115,7 +125,7 @@ export class TaskService { this.logger.info( new DataDeletionDomainOperationLoggable( 'Deleting user data from Task archive collection', - DomainModel.TASK, + DomainName.TASK, userId, StatusModel.PENDING ) @@ -130,11 +140,16 @@ export class TaskService { await this.taskRepo.save(tasksWithUserInFinished); } - const result = DomainOperationBuilder.build(DomainModel.TASK, counterOfTasksWithUserInFinished, 0); + const result = DomainOperationBuilder.build( + DomainName.TASK, + OperationType.UPDATE, + counterOfTasksWithUserInFinished, + this.getTasksId(tasksWithUserInFinished) + ); this.logger.info( new DataDeletionDomainOperationLoggable( 'Successfully deleted user data from Task archive collection', - DomainModel.TASK, + DomainName.TASK, userId, StatusModel.FINISHED, counterOfTasksWithUserInFinished, @@ -144,4 +159,8 @@ export class TaskService { return result; } + + private getTasksId(tasks: Task[]): EntityId[] { + return tasks.map((task) => task.id); + } } diff --git a/apps/server/src/modules/teams/service/team.service.spec.ts b/apps/server/src/modules/teams/service/team.service.spec.ts index d14f9fbfc19..43a5f4b80e7 100644 --- a/apps/server/src/modules/teams/service/team.service.spec.ts +++ b/apps/server/src/modules/teams/service/team.service.spec.ts @@ -3,6 +3,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import { TeamsRepo } from '@shared/repo'; import { setupEntities, teamFactory, teamUserFactory } from '@shared/testing'; import { Logger } from '@src/core/logger'; +import { DomainOperationBuilder } from '@shared/domain/builder'; +import { DomainName, OperationType } from '@shared/domain/types'; import { TeamService } from './team.service'; describe('TeamService', () => { @@ -81,7 +83,13 @@ describe('TeamService', () => { teamsRepo.findByUserId.mockResolvedValue([team1, team2]); + const expectedResult = DomainOperationBuilder.build(DomainName.TEAMS, OperationType.UPDATE, 2, [ + team1.id, + team2.id, + ]); + return { + expectedResult, teamUser, }; }; @@ -95,11 +103,11 @@ describe('TeamService', () => { }); it('should update teams without deleted user', async () => { - const { teamUser } = setup(); + const { expectedResult, teamUser } = setup(); const result = await service.deleteUserDataFromTeams(teamUser.user.id); - expect(result).toEqual(2); + expect(result).toEqual(expectedResult); }); }); }); diff --git a/apps/server/src/modules/teams/service/team.service.ts b/apps/server/src/modules/teams/service/team.service.ts index 2264af9a56c..6f0f3de6610 100644 --- a/apps/server/src/modules/teams/service/team.service.ts +++ b/apps/server/src/modules/teams/service/team.service.ts @@ -1,7 +1,9 @@ import { Injectable } from '@nestjs/common'; import { DataDeletionDomainOperationLoggable } from '@shared/common/loggable'; +import { DomainOperationBuilder } from '@shared/domain/builder'; import { TeamEntity } from '@shared/domain/entity'; -import { DomainModel, EntityId, StatusModel } from '@shared/domain/types'; +import { DomainOperation } from '@shared/domain/interface'; +import { DomainName, EntityId, OperationType, StatusModel } from '@shared/domain/types'; import { TeamsRepo } from '@shared/repo'; import { Logger } from '@src/core/logger'; @@ -17,11 +19,11 @@ export class TeamService { return teams; } - public async deleteUserDataFromTeams(userId: EntityId): Promise { + public async deleteUserDataFromTeams(userId: EntityId): Promise { this.logger.info( new DataDeletionDomainOperationLoggable( 'Deleting user data from Teams', - DomainModel.TEAMS, + DomainName.TEAMS, userId, StatusModel.PENDING ) @@ -36,17 +38,28 @@ export class TeamService { const numberOfUpdatedTeams = teams.length; + const result = DomainOperationBuilder.build( + DomainName.TEAMS, + OperationType.UPDATE, + numberOfUpdatedTeams, + this.getTeamsId(teams) + ); + this.logger.info( new DataDeletionDomainOperationLoggable( 'Successfully deleted user data from Teams', - DomainModel.TEAMS, + DomainName.TEAMS, userId, - StatusModel.PENDING, + StatusModel.FINISHED, numberOfUpdatedTeams, 0 ) ); - return numberOfUpdatedTeams; + return result; + } + + private getTeamsId(teams: TeamEntity[]): EntityId[] { + return teams.map((team) => team.id); } } diff --git a/apps/server/src/modules/user/service/user.service.spec.ts b/apps/server/src/modules/user/service/user.service.spec.ts index a217ee65185..dd8b821f3cc 100644 --- a/apps/server/src/modules/user/service/user.service.spec.ts +++ b/apps/server/src/modules/user/service/user.service.spec.ts @@ -9,11 +9,12 @@ import { Test, TestingModule } from '@nestjs/testing'; import { UserDO } from '@shared/domain/domainobject/user.do'; import { LanguageType, Role, User } from '@shared/domain/entity'; import { IFindOptions, Permission, RoleName, SortOrder } from '@shared/domain/interface'; -import { EntityId } from '@shared/domain/types'; +import { DomainName, EntityId, OperationType } from '@shared/domain/types'; import { UserRepo } from '@shared/repo'; import { UserDORepo } from '@shared/repo/user/user-do.repo'; import { roleFactory, setupEntities, userDoFactory, userFactory } from '@shared/testing'; import { Logger } from '@src/core/logger'; +import { DomainOperationBuilder } from '@shared/domain/builder'; import { UserDto } from '../uc/dto/user.dto'; import { UserQuery } from './user-query.type'; import { UserService } from './user.service'; @@ -392,41 +393,54 @@ describe('UserService', () => { const user: UserDO = userDoFactory.build({ id: undefined }); const userId: EntityId = user.id as EntityId; - userRepo.deleteUser.mockResolvedValue(0); + userRepo.deleteUser.mockResolvedValue(null); + + const expectedResult = DomainOperationBuilder.build(DomainName.USER, OperationType.DELETE, 0, []); return { + expectedResult, userId, }; }; - it('should return 0', async () => { - const { userId } = setup(); + it('should return null', async () => { + const { expectedResult, userId } = setup(); const result = await service.deleteUser(userId); - expect(result).toEqual(0); + expect(result).toEqual(expectedResult); }); }); - describe('when deleting by userId', () => { + describe('when user exists', () => { const setup = () => { const user1: User = userFactory.asStudent().buildWithId(); + const expectedResult = DomainOperationBuilder.build(DomainName.USER, OperationType.DELETE, 1, [user1.id]); + userRepo.findById.mockResolvedValue(user1); - userRepo.deleteUser.mockResolvedValue(1); + userRepo.deleteUser.mockResolvedValue(user1.id); return { + expectedResult, user1, }; }; - it('should delete user by userId', async () => { + it('should call userRepo.deleteUser with userId', async () => { const { user1 } = setup(); - const result = await service.deleteUser(user1.id); + await service.deleteUser(user1.id); expect(userRepo.deleteUser).toHaveBeenCalledWith(user1.id); - expect(result).toEqual(1); + }); + + it('should return domainOperation object with information about deleted user', async () => { + const { expectedResult, user1 } = setup(); + + const result = await service.deleteUser(user1.id); + + expect(result).toEqual(expectedResult); }); }); }); diff --git a/apps/server/src/modules/user/service/user.service.ts b/apps/server/src/modules/user/service/user.service.ts index acc97cf2d81..d80ed936f08 100644 --- a/apps/server/src/modules/user/service/user.service.ts +++ b/apps/server/src/modules/user/service/user.service.ts @@ -10,11 +10,12 @@ import { ConfigService } from '@nestjs/config'; import { DataDeletionDomainOperationLoggable } from '@shared/common/loggable'; import { Page, RoleReference, UserDO } from '@shared/domain/domainobject'; import { LanguageType, User } from '@shared/domain/entity'; -import { IFindOptions } from '@shared/domain/interface'; -import { DomainModel, EntityId, StatusModel } from '@shared/domain/types'; +import { DomainOperation, IFindOptions } from '@shared/domain/interface'; +import { DomainName, EntityId, OperationType, StatusModel } from '@shared/domain/types'; import { UserRepo } from '@shared/repo'; import { UserDORepo } from '@shared/repo/user/user-do.repo'; import { Logger } from '@src/core/logger'; +import { DomainOperationBuilder } from '@shared/domain/builder'; import { UserConfig } from '../interfaces'; import { UserMapper } from '../mapper/user.mapper'; import { UserDto } from '../uc/dto/user.dto'; @@ -128,23 +129,35 @@ export class UserService { } } - async deleteUser(userId: EntityId): Promise { + async deleteUser(userId: EntityId): Promise { this.logger.info( - new DataDeletionDomainOperationLoggable('Deleting user', DomainModel.USER, userId, StatusModel.PENDING) + new DataDeletionDomainOperationLoggable('Deleting user', DomainName.USER, userId, StatusModel.PENDING) ); - const deletedUserNumber = await this.userRepo.deleteUser(userId); + const response = await this.userRepo.deleteUser(userId); + + const deletedUsers = response !== null ? [response] : []; + + const numberOfDeletedUsers = deletedUsers.length; + + const result = DomainOperationBuilder.build( + DomainName.USER, + OperationType.DELETE, + numberOfDeletedUsers, + deletedUsers + ); + this.logger.info( new DataDeletionDomainOperationLoggable( 'Successfully deleted user', - DomainModel.USER, + DomainName.USER, userId, StatusModel.FINISHED, 0, - deletedUserNumber + numberOfDeletedUsers ) ); - return deletedUserNumber; + return result; } async getParentEmailsFromUser(userId: EntityId): Promise { diff --git a/apps/server/src/shared/common/loggable/data-deletion-domain-operation-loggable.spec.ts b/apps/server/src/shared/common/loggable/data-deletion-domain-operation-loggable.spec.ts index b9ed8b111d8..f8d8e18a5ee 100644 --- a/apps/server/src/shared/common/loggable/data-deletion-domain-operation-loggable.spec.ts +++ b/apps/server/src/shared/common/loggable/data-deletion-domain-operation-loggable.spec.ts @@ -1,5 +1,5 @@ import { ObjectId } from 'bson'; -import { DomainModel, EntityId, StatusModel } from '@shared/domain/types'; +import { DomainName, EntityId, StatusModel } from '@shared/domain/types'; import { DataDeletionDomainOperationLoggable } from './data-deletion-domain-operation-loggable'; describe(DataDeletionDomainOperationLoggable.name, () => { @@ -7,7 +7,7 @@ describe(DataDeletionDomainOperationLoggable.name, () => { const setup = () => { const user: EntityId = new ObjectId().toHexString(); const message = 'Test message.'; - const domain = DomainModel.USER; + const domain = DomainName.USER; const status = StatusModel.FINISHED; const modifiedCount = 0; const deletedCount = 1; diff --git a/apps/server/src/shared/common/loggable/data-deletion-domain-operation-loggable.ts b/apps/server/src/shared/common/loggable/data-deletion-domain-operation-loggable.ts index 1be7727a6cd..494628fd020 100644 --- a/apps/server/src/shared/common/loggable/data-deletion-domain-operation-loggable.ts +++ b/apps/server/src/shared/common/loggable/data-deletion-domain-operation-loggable.ts @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import { DomainModel, EntityId, StatusModel } from '@shared/domain/types'; +import { DomainName, EntityId, StatusModel } from '@shared/domain/types'; import { ErrorLogMessage, LogMessage, Loggable, ValidationErrorLogMessage } from '@src/core/logger'; export class DataDeletionDomainOperationLoggable implements Loggable { constructor( private readonly message: string, - private readonly domain: DomainModel, + private readonly domain: DomainName, private readonly user: EntityId, private readonly status: StatusModel, private readonly modifiedCount?: number, diff --git a/apps/server/src/shared/domain/builder/domain-operation.builder.spec.ts b/apps/server/src/shared/domain/builder/domain-operation.builder.spec.ts index 940224c3cf7..6a66b724096 100644 --- a/apps/server/src/shared/domain/builder/domain-operation.builder.spec.ts +++ b/apps/server/src/shared/domain/builder/domain-operation.builder.spec.ts @@ -1,4 +1,4 @@ -import { DomainModel } from '@shared/domain/types'; +import { DomainName, OperationType } from '@shared/domain/types'; import { ObjectId } from 'bson'; import { DomainOperationBuilder } from '.'; @@ -8,22 +8,22 @@ describe(DomainOperationBuilder.name, () => { }); const setup = () => { - const domain = DomainModel.PSEUDONYMS; - const modifiedCount = 0; - const modifiedRef = []; - const deletedRef = [new ObjectId().toHexString(), new ObjectId().toHexString()]; - const deletedCount = 2; + const domain = DomainName.PSEUDONYMS; + const operation = OperationType.DELETE; + const refs = [new ObjectId().toHexString(), new ObjectId().toHexString()]; + const count = 2; - return { domain, modifiedCount, deletedCount, modifiedRef, deletedRef }; + return { domain, count, operation, refs }; }; it('should build generic domainOperation with all attributes', () => { - const { domain, modifiedCount, deletedCount, modifiedRef, deletedRef } = setup(); + const { domain, count, operation, refs } = setup(); - const result = DomainOperationBuilder.build(domain, modifiedCount, deletedCount, modifiedRef, deletedRef); + const result = DomainOperationBuilder.build(domain, operation, count, refs); expect(result.domain).toEqual(domain); - expect(result.modifiedCount).toEqual(modifiedCount); - expect(result.deletedCount).toEqual(deletedCount); + expect(result.operation).toEqual(operation); + expect(result.count).toEqual(count); + expect(result.refs).toEqual(refs); }); }); diff --git a/apps/server/src/shared/domain/builder/domain-operation.builder.ts b/apps/server/src/shared/domain/builder/domain-operation.builder.ts index b9c6619482f..12a9e50c2dd 100644 --- a/apps/server/src/shared/domain/builder/domain-operation.builder.ts +++ b/apps/server/src/shared/domain/builder/domain-operation.builder.ts @@ -1,15 +1,9 @@ import { DomainOperation } from '@shared/domain/interface'; -import { DomainModel } from '@shared/domain/types'; +import { DomainName, EntityId, OperationType } from '@shared/domain/types'; export class DomainOperationBuilder { - static build( - domain: DomainModel, - modifiedCount: number, - deletedCount: number, - modifiedRef?: string[], - deletedRef?: string[] - ): DomainOperation { - const domainOperation = { domain, modifiedCount, deletedCount, modifiedRef, deletedRef }; + static build(domain: DomainName, operation: OperationType, count: number, refs: EntityId[]): DomainOperation { + const domainOperation = { domain, operation, count, refs }; return domainOperation; } diff --git a/apps/server/src/shared/domain/interface/domain-operation.ts b/apps/server/src/shared/domain/interface/domain-operation.ts index d4900ed2314..7bb0e689a9c 100644 --- a/apps/server/src/shared/domain/interface/domain-operation.ts +++ b/apps/server/src/shared/domain/interface/domain-operation.ts @@ -1,9 +1,8 @@ -import { DomainModel } from '../types'; +import { DomainName, OperationType } from '../types'; export interface DomainOperation { - domain: DomainModel; - modifiedCount: number; - deletedCount: number; - modifiedRef?: string[]; - deletedRef?: string[]; + domain: DomainName; + operation: OperationType; + count: number; + refs: string[]; } diff --git a/apps/server/src/shared/domain/types/domain.ts b/apps/server/src/shared/domain/types/domain-name.enum.ts similarity index 82% rename from apps/server/src/shared/domain/types/domain.ts rename to apps/server/src/shared/domain/types/domain-name.enum.ts index babc23631d1..3d3db093e38 100644 --- a/apps/server/src/shared/domain/types/domain.ts +++ b/apps/server/src/shared/domain/types/domain-name.enum.ts @@ -1,4 +1,4 @@ -export const enum DomainModel { +export const enum DomainName { ACCOUNT = 'account', CLASS = 'class', COURSEGROUP = 'courseGroup', @@ -10,6 +10,7 @@ export const enum DomainModel { PSEUDONYMS = 'pseudonyms', REGISTRATIONPIN = 'registrationPin', ROCKETCHATUSER = 'rocketChatUser', + ROCKETCHATSERVICE = 'rocketChatService', TASK = 'task', TEAMS = 'teams', USER = 'user', diff --git a/apps/server/src/shared/domain/types/index.ts b/apps/server/src/shared/domain/types/index.ts index 4957fc6de59..41d7dba5ac9 100644 --- a/apps/server/src/shared/domain/types/index.ts +++ b/apps/server/src/shared/domain/types/index.ts @@ -10,5 +10,6 @@ export * from './school-purpose.enum'; export * from './system.type'; export * from './task.types'; export * from './value-of'; -export * from './domain'; +export * from './domain-name.enum'; export * from './status-model.enum'; +export * from './operation-type.enum'; diff --git a/apps/server/src/shared/domain/types/operation-type.enum.ts b/apps/server/src/shared/domain/types/operation-type.enum.ts new file mode 100644 index 00000000000..754ee6add0b --- /dev/null +++ b/apps/server/src/shared/domain/types/operation-type.enum.ts @@ -0,0 +1,4 @@ +export const enum OperationType { + DELETE = 'delete', + UPDATE = 'update', +} diff --git a/apps/server/src/shared/repo/user/user.repo.integration.spec.ts b/apps/server/src/shared/repo/user/user.repo.integration.spec.ts index 92e56786cb3..5f97df9d68a 100644 --- a/apps/server/src/shared/repo/user/user.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/user/user.repo.integration.spec.ts @@ -413,43 +413,71 @@ describe('user repo', () => { }); }); - describe('delete', () => { - const setup = async () => { - const user1: User = userFactory.buildWithId(); - const user2: User = userFactory.buildWithId(); - const user3: User = userFactory.buildWithId(); - await em.persistAndFlush([user1, user2, user3]); + describe('deleteUser', () => { + describe('when user does not exist', () => { + const setup = () => { + const user = userFactory.buildWithId(); - return { - user1, - user2, - user3, + return { + user, + }; }; - }; - it('should delete user', async () => { - const { user1, user2, user3 } = await setup(); - const deleteResult = await repo.deleteUser(user1.id); - expect(deleteResult).toEqual(1); - - const result1 = await em.find(User, { id: user1.id }); - expect(result1).toHaveLength(0); - - const result2 = await repo.findById(user2.id); - expect(result2).toMatchObject({ - firstName: user2.firstName, - lastName: user2.lastName, - email: user2.email, - roles: user2.roles, - school: user2.school, + + it('should return empty array', async () => { + const { user } = setup(); + + const result = await repo.deleteUser(user.id); + + expect(result).toBeNull(); }); + }); + describe('when user exists', () => { + const setup = async () => { + const user1: User = userFactory.buildWithId(); + const user2: User = userFactory.buildWithId(); + const user3: User = userFactory.buildWithId(); + await em.persistAndFlush([user1, user2, user3]); + + const expectedUser2 = { + firstName: user2.firstName, + lastName: user2.lastName, + email: user2.email, + roles: user2.roles, + school: user2.school, + }; + + const expectedUser3 = { + firstName: user3.firstName, + lastName: user3.lastName, + email: user3.email, + roles: user3.roles, + school: user3.school, + }; + + const expectedResult = user1.id; + + return { + expectedResult, + expectedUser2, + expectedUser3, + user1, + user2, + user3, + }; + }; + it('should delete user', async () => { + const { expectedResult, expectedUser2, expectedUser3, user1, user2, user3 } = await setup(); + const deleteResult = await repo.deleteUser(user1.id); + expect(deleteResult).toEqual(expectedResult); + + const result1 = await em.find(User, { id: user1.id }); + expect(result1).toHaveLength(0); + + const result2 = await repo.findById(user2.id); + expect(result2).toMatchObject(expectedUser2); - const result3 = await repo.findById(user3.id); - expect(result3).toMatchObject({ - firstName: user3.firstName, - lastName: user3.lastName, - email: user3.email, - roles: user3.roles, - school: user3.school, + const result3 = await repo.findById(user3.id); + expect(result3).toMatchObject(expectedUser3); }); }); }); diff --git a/apps/server/src/shared/repo/user/user.repo.ts b/apps/server/src/shared/repo/user/user.repo.ts index bab86007782..d16c07ff313 100644 --- a/apps/server/src/shared/repo/user/user.repo.ts +++ b/apps/server/src/shared/repo/user/user.repo.ts @@ -155,11 +155,15 @@ export class UserRepo extends BaseRepo { return promise; } - async deleteUser(userId: EntityId): Promise { - const deletedUserNumber: Promise = this._em.nativeDelete(User, { - id: userId, - }); - return deletedUserNumber; + async deleteUser(userId: EntityId): Promise { + const user = await this._em.findOne(User, { id: userId }); + if (user === null) { + return null; + } + + await this._em.removeAndFlush(user); + + return user.id; } async getParentEmailsFromUser(userId: EntityId): Promise {