diff --git a/apps/server/src/apps/deletion.app.ts b/apps/server/src/apps/deletion.app.ts deleted file mode 100644 index 902831ba391..00000000000 --- a/apps/server/src/apps/deletion.app.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* istanbul ignore file */ -/* eslint-disable no-console */ -import { NestFactory } from '@nestjs/core'; -import { ExpressAdapter } from '@nestjs/platform-express'; -import express from 'express'; - -// register source-map-support for debugging -import { install as sourceMapInstall } from 'source-map-support'; - -// application imports -import { LegacyLogger } from '@src/core/logger'; -import { DeletionModule } from '@src/modules/deletion'; -import { enableOpenApiDocs } from '@src/shared/controller/swagger'; - -async function bootstrap() { - sourceMapInstall(); - - // create the NestJS application on a seperate express instance - const nestExpress = express(); - - const nestExpressAdapter = new ExpressAdapter(nestExpress); - const nestApp = await NestFactory.create(DeletionModule, nestExpressAdapter); - // WinstonLogger - nestApp.useLogger(await nestApp.resolve(LegacyLogger)); - - // customize nest app settings - nestApp.enableCors({ exposedHeaders: ['Content-Disposition'] }); - enableOpenApiDocs(nestApp, 'docs'); - - await nestApp.init(); - - // mount instances - const rootExpress = express(); - - const port = 4450; - const basePath = '/api/v3'; - - // exposed alias mounts - rootExpress.use(basePath, nestExpress); - rootExpress.listen(port); - - console.log('##########################################'); - console.log(`### Start KNL Deletion Server ###`); - console.log(`### Port: ${port} ###`); - console.log(`### Base path: ${basePath} ###`); - console.log('##########################################'); -} - -void bootstrap(); diff --git a/apps/server/src/config/database.config.ts b/apps/server/src/config/database.config.ts index ed0b3767e21..ad97e4c3d66 100644 --- a/apps/server/src/config/database.config.ts +++ b/apps/server/src/config/database.config.ts @@ -4,10 +4,9 @@ interface GlobalConstants { DB_URL: string; DB_PASSWORD?: string; DB_USERNAME?: string; - DEL_DB_URL: string; } const usedGlobals: GlobalConstants = globals; /** Database URL */ -export const { DB_URL, DB_PASSWORD, DB_USERNAME, DEL_DB_URL } = usedGlobals; +export const { DB_URL, DB_PASSWORD, DB_USERNAME } = usedGlobals; diff --git a/apps/server/src/modules/deletion/deletion.module.ts b/apps/server/src/modules/deletion/deletion.module.ts index 4481d7ce492..7d508b1650a 100644 --- a/apps/server/src/modules/deletion/deletion.module.ts +++ b/apps/server/src/modules/deletion/deletion.module.ts @@ -1,37 +1,38 @@ -import { Module, NotFoundException } from '@nestjs/common'; -import { DB_PASSWORD, DB_USERNAME, DEL_DB_URL } from '@src/config'; -import { CoreModule } from '@src/core'; +import { Module } from '@nestjs/common'; +// import { Module, NotFoundException } from '@nestjs/common'; +// import { DB_PASSWORD, DB_USERNAME, DEL_DB_URL } from '@src/config'; +// import { CoreModule } from '@src/core'; import { Logger } from '@src/core/logger'; -import { MikroOrmModule, MikroOrmModuleSyncOptions } from '@mikro-orm/nestjs'; -import { AuthenticationModule } from '@src/modules/authentication/authentication.module'; -import { AuthorizationModule } from '@src/modules'; -import { RabbitMQWrapperTestModule } from '@shared/infra/rabbitmq'; -import { Dictionary, IPrimaryKey } from '@mikro-orm/core'; +// import { MikroOrmModule, MikroOrmModuleSyncOptions } from '@mikro-orm/nestjs'; +// import { AuthenticationModule } from '@src/modules/authentication/authentication.module'; +// import { AuthorizationModule } from '@src/modules'; +// import { RabbitMQWrapperTestModule } from '@shared/infra/rabbitmq'; +// import { Dictionary, IPrimaryKey } from '@mikro-orm/core'; import { DeletionRequestService } from './services/deletion-request.service'; import { DeletionRequestRepo } from './repo/deletion-request.repo'; -const defaultMikroOrmOptions: MikroOrmModuleSyncOptions = { - findOneOrFailHandler: (entityName: string, where: Dictionary | IPrimaryKey) => - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - new NotFoundException(`The requested ${entityName}: ${where} has not been found.`), -}; +// const defaultMikroOrmOptions: MikroOrmModuleSyncOptions = { +// findOneOrFailHandler: (entityName: string, where: Dictionary | IPrimaryKey) => +// // eslint-disable-next-line @typescript-eslint/restrict-template-expressions +// new NotFoundException(`The requested ${entityName}: ${where} has not been found.`), +// }; @Module({ - imports: [ - AuthorizationModule, - AuthenticationModule, - CoreModule, - RabbitMQWrapperTestModule, - MikroOrmModule.forRoot({ - ...defaultMikroOrmOptions, - type: 'mongo', - clientUrl: DEL_DB_URL, - password: DB_PASSWORD, - user: DB_USERNAME, - entities: [], - }), - // ConfigModule.forRoot(createConfigModuleOptions(config)), - ], + // imports: [ + // AuthorizationModule, + // AuthenticationModule, + // CoreModule, + // RabbitMQWrapperTestModule, + // MikroOrmModule.forRoot({ + // ...defaultMikroOrmOptions, + // type: 'mongo', + // clientUrl: DEL_DB_URL, + // password: DB_PASSWORD, + // user: DB_USERNAME, + // entities: [], + // }), + // ConfigModule.forRoot(createConfigModuleOptions(config)), + // ], providers: [Logger, DeletionRequestService, DeletionRequestRepo], }) export class DeletionModule {} 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 new file mode 100644 index 00000000000..b18e4759af5 --- /dev/null +++ b/apps/server/src/modules/deletion/domain/deletion-log.do.spec.ts @@ -0,0 +1,64 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { deletionLogFactory } from './testing/factory/deletion-log.factory'; +import { DeletionLog } from './deletion-log.do'; +import { DeletionOperationModel } from './types/deletion-operation-model.enum'; +import { DeletionDomainModel } from './types/deletion-domain-model.enum'; + +describe(DeletionLog.name, () => { + describe('constructor', () => { + describe('When constructor is called', () => { + it('should create a deletionRequest by passing required properties', () => { + const domainObject: DeletionLog = deletionLogFactory.build(); + + expect(domainObject instanceof DeletionLog).toEqual(true); + }); + }); + + describe('when passed a valid id', () => { + const setup = () => { + const domainObject: DeletionLog = deletionLogFactory.buildWithId(); + + return { domainObject }; + }; + + it('should set the id', () => { + const { domainObject } = setup(); + + const deletionLogDomainObject: DeletionLog = new DeletionLog(domainObject); + + expect(deletionLogDomainObject.id).toEqual(domainObject.id); + }); + }); + }); + + describe('getters', () => { + describe('When getters are used', () => { + it('getters should return proper values', () => { + const props = { + id: new ObjectId().toHexString(), + domain: DeletionDomainModel.USER, + operation: DeletionOperationModel.DELETE, + modifiedCounter: 0, + deletedCounter: 1, + deletionRequestId: new ObjectId().toHexString(), + createdAt: new Date(), + updatedAt: new Date(), + }; + + const deletionLogDo = new DeletionLog(props); + const gettersValues = { + id: deletionLogDo.id, + domain: deletionLogDo.domain, + operation: deletionLogDo.operation, + modifiedCounter: deletionLogDo.modifiedCounter, + deletedCounter: deletionLogDo.deletedCounter, + deletionRequestId: deletionLogDo.deletionRequestId, + createdAt: deletionLogDo.createdAt, + updatedAt: deletionLogDo.updatedAt, + }; + + expect(gettersValues).toEqual(props); + }); + }); + }); +}); 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 a5ae8db4b89..89391ca65a8 100644 --- a/apps/server/src/modules/deletion/domain/deletion-log.do.ts +++ b/apps/server/src/modules/deletion/domain/deletion-log.do.ts @@ -1,12 +1,15 @@ import { EntityId } from '@shared/domain/types'; import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; +import { DeletionDomainModel } from './types/deletion-domain-model.enum'; +import { DeletionOperationModel } from './types/deletion-operation-model.enum'; export interface DeletionLogProps extends AuthorizableObject { createdAt: Date; updatedAt: Date; - scope?: string; - operation?: string; - docIds?: EntityId[]; + domain?: DeletionDomainModel; + operation?: DeletionOperationModel; + modifiedCounter?: number; + deletedCounter?: number; deletionRequestId?: EntityId; } @@ -19,19 +22,23 @@ export class DeletionLog extends DomainObject { return this.props.updatedAt; } - get scope(): string | undefined { - return this.props.scope; + get domain(): DeletionDomainModel | undefined { + return this.props.domain; } - get operation(): string | undefined { + get operation(): DeletionOperationModel | undefined { return this.props.operation; } - get deletionRequestId(): EntityId | undefined { - return this.props.deletionRequestId; + get modifiedCounter(): number | undefined { + return this.props.modifiedCounter; + } + + get deletedCounter(): number | undefined { + return this.props.deletedCounter; } - get docIds(): EntityId[] | undefined { - return this.props.docIds; + get deletionRequestId(): EntityId | undefined { + return this.props.deletionRequestId; } } 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 new file mode 100644 index 00000000000..a2262f0b614 --- /dev/null +++ b/apps/server/src/modules/deletion/domain/deletion-request.do.spec.ts @@ -0,0 +1,62 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { DeletionRequest } from './deletion-request.do'; +import { DeletionDomainModel } from './types/deletion-domain-model.enum'; +import { deletionRequestFactory } from './testing/factory/deletion-request.factory'; +import { DeletionStatusModel } from './types/deletion-status-model.enum'; + +describe(DeletionRequest.name, () => { + describe('constructor', () => { + describe('When constructor is called', () => { + it('should create a deletionRequest by passing required properties', () => { + const domainObject: DeletionRequest = deletionRequestFactory.build(); + + expect(domainObject instanceof DeletionRequest).toEqual(true); + }); + }); + + describe('when passed a valid id', () => { + const setup = () => { + const domainObject: DeletionRequest = deletionRequestFactory.buildWithId(); + + return { domainObject }; + }; + + it('should set the id', () => { + const { domainObject } = setup(); + + const deletionRequestDomainObject: DeletionRequest = new DeletionRequest(domainObject); + + expect(deletionRequestDomainObject.id).toEqual(domainObject.id); + }); + }); + }); + + describe('getters', () => { + describe('When getters are used', () => { + it('getters should return proper values', () => { + const props = { + id: new ObjectId().toHexString(), + domain: DeletionDomainModel.USER, + deleteAfter: new Date(), + itemId: new ObjectId().toHexString(), + status: DeletionStatusModel.REGISTERED, + createdAt: new Date(), + updatedAt: new Date(), + }; + + const deletionRequestDo = new DeletionRequest(props); + const gettersValues = { + id: deletionRequestDo.id, + domain: deletionRequestDo.domain, + deleteAfter: deletionRequestDo.deleteAfter, + itemId: deletionRequestDo.itemId, + status: deletionRequestDo.status, + createdAt: deletionRequestDo.createdAt, + updatedAt: deletionRequestDo.updatedAt, + }; + + expect(gettersValues).toEqual(props); + }); + }); + }); +}); 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 3a85ecd345c..881560359bc 100644 --- a/apps/server/src/modules/deletion/domain/deletion-request.do.ts +++ b/apps/server/src/modules/deletion/domain/deletion-request.do.ts @@ -1,12 +1,15 @@ import { EntityId } from '@shared/domain/types'; import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; +import { DeletionDomainModel } from './types/deletion-domain-model.enum'; +import { DeletionStatusModel } from './types/deletion-status-model.enum'; export interface DeletionRequestProps extends AuthorizableObject { createdAt?: Date; updatedAt?: Date; - source?: string; + domain?: DeletionDomainModel; deleteAfter?: Date; - userId?: EntityId; + itemId?: EntityId; + status?: DeletionStatusModel; } export class DeletionRequest extends DomainObject { @@ -18,15 +21,19 @@ export class DeletionRequest extends DomainObject { return this.props.updatedAt; } - get source(): string | undefined { - return this.props.source; + get domain(): DeletionDomainModel | undefined { + return this.props.domain; } get deleteAfter(): Date | undefined { return this.props.deleteAfter; } - get userId(): EntityId | undefined { - return this.props.userId; + get itemId(): EntityId | undefined { + return this.props.itemId; + } + + get status(): DeletionStatusModel | undefined { + return this.props.status; } } 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 new file mode 100644 index 00000000000..25249fa3518 --- /dev/null +++ b/apps/server/src/modules/deletion/domain/testing/factory/deletion-log.factory.ts @@ -0,0 +1,18 @@ +import { DoBaseFactory } from '@shared/testing'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { DeletionLog, DeletionLogProps } from '../../deletion-log.do'; +import { DeletionOperationModel } from '../../types/deletion-operation-model.enum'; +import { DeletionDomainModel } from '../../types/deletion-domain-model.enum'; + +export const deletionLogFactory = DoBaseFactory.define(DeletionLog, () => { + return { + id: new ObjectId().toHexString(), + domain: DeletionDomainModel.USER, + operation: DeletionOperationModel.DELETE, + modifiedCounter: 0, + deletedCounter: 1, + deletionRequestId: new ObjectId().toHexString(), + createdAt: new Date(), + updatedAt: 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 new file mode 100644 index 00000000000..36cad2e8aa0 --- /dev/null +++ b/apps/server/src/modules/deletion/domain/testing/factory/deletion-request.factory.ts @@ -0,0 +1,28 @@ +import { DoBaseFactory } from '@shared/testing'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { DeepPartial } from 'fishery'; +import { DeletionRequest, DeletionRequestProps } from '../../deletion-request.do'; +import { DeletionDomainModel } from '../../types/deletion-domain-model.enum'; +import { DeletionStatusModel } from '../../types/deletion-status-model.enum'; + +class DeletionRequestFactory extends DoBaseFactory { + withUserIds(itemId: string): this { + const params: DeepPartial = { + itemId, + }; + + return this.params(params); + } +} + +export const deletionRequestFactory = DeletionRequestFactory.define(DeletionRequest, () => { + return { + id: new ObjectId().toHexString(), + domain: DeletionDomainModel.USER, + deleteAfter: new Date(), + itemId: new ObjectId().toHexString(), + status: DeletionStatusModel.REGISTERED, + createdAt: new Date(), + updatedAt: new Date(), + }; +}); diff --git a/apps/server/src/modules/deletion/domain/types/deletion-domain-model.enum.ts b/apps/server/src/modules/deletion/domain/types/deletion-domain-model.enum.ts new file mode 100644 index 00000000000..7df1a28e4fe --- /dev/null +++ b/apps/server/src/modules/deletion/domain/types/deletion-domain-model.enum.ts @@ -0,0 +1,7 @@ +export const enum DeletionDomainModel { + USER = 'user', + TEAMS = 'teams', + LESSONS = 'lessons', + PSEUDONYMS = 'pseudonyms', + ACCOUNT = 'account', +} diff --git a/apps/server/src/modules/deletion/domain/types/deletion-operation-model.enum.ts b/apps/server/src/modules/deletion/domain/types/deletion-operation-model.enum.ts new file mode 100644 index 00000000000..675189e634b --- /dev/null +++ b/apps/server/src/modules/deletion/domain/types/deletion-operation-model.enum.ts @@ -0,0 +1,4 @@ +export const enum DeletionOperationModel { + DELETE = 'delete', + UPDATE = 'update', +} diff --git a/apps/server/src/modules/deletion/domain/types/deletion-status-model.enum.ts b/apps/server/src/modules/deletion/domain/types/deletion-status-model.enum.ts new file mode 100644 index 00000000000..884cccdf68f --- /dev/null +++ b/apps/server/src/modules/deletion/domain/types/deletion-status-model.enum.ts @@ -0,0 +1,4 @@ +export enum DeletionStatusModel { + 'REGISTERED' = 'registered', + 'SUCCESS' = 'success', +} 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 new file mode 100644 index 00000000000..022582d34c2 --- /dev/null +++ b/apps/server/src/modules/deletion/entity/deletion-log.entity.spec.ts @@ -0,0 +1,60 @@ +import { setupEntities } from '@shared/testing'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { DeletionLogEntity } from './deletion-log.entity'; +import { DeletionOperationModel } from '../domain/types/deletion-operation-model.enum'; +import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; + +describe(DeletionLogEntity.name, () => { + beforeAll(async () => { + await setupEntities(); + }); + + describe('constructor', () => { + describe('When constructor is called', () => { + const setup = () => { + const props = { + id: new ObjectId().toHexString(), + domain: DeletionDomainModel.USER, + operation: DeletionOperationModel.DELETE, + modifiedCounter: 0, + deletedCounter: 1, + deletionRequestId: new ObjectId(), + createdAt: new Date(), + updatedAt: new Date(), + }; + + return { props }; + }; + it('should throw an error by empty constructor', () => { + // @ts-expect-error: Test case + const test = () => new DeletionLogEntity(); + expect(test).toThrow(); + }); + + it('should create a deletionLog by passing required properties', () => { + const { props } = setup(); + const entity: DeletionLogEntity = new DeletionLogEntity(props); + + expect(entity instanceof DeletionLogEntity).toEqual(true); + }); + + it(`should return a valid object with fields values set from the provided complete props object`, () => { + const { props } = setup(); + const entity: DeletionLogEntity = new DeletionLogEntity(props); + + const entityProps = { + id: entity.id, + domain: entity.domain, + operation: entity.operation, + modifiedCounter: entity.modifiedCounter, + deletedCounter: entity.deletedCounter, + deletionRequestId: entity.deletionRequestId, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + }; + + expect(entityProps).toEqual(props); + }); + }); + }); +}); 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 839655016fa..f2139ca6aaa 100644 --- a/apps/server/src/modules/deletion/entity/deletion-log.entity.ts +++ b/apps/server/src/modules/deletion/entity/deletion-log.entity.ts @@ -1,58 +1,69 @@ import { Entity, Property } from '@mikro-orm/core'; import { BaseEntityWithTimestamps, EntityId } from '@shared/domain'; import { ObjectId } from '@mikro-orm/mongodb'; +import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; +import { DeletionOperationModel } from '../domain/types/deletion-operation-model.enum'; export interface DeletionLogEntityProps { - id: EntityId; + id?: EntityId; + domain?: DeletionDomainModel; + operation?: DeletionOperationModel; + modifiedCounter?: number; + deletedCounter?: number; + deletionRequestId?: ObjectId; createdAt?: Date; updatedAt?: Date; - scope?: string; - operation?: string; - docIds?: ObjectId[]; - deletionRequestId?: ObjectId; } @Entity({ tableName: 'deletionlogs' }) export class DeletionLogEntity extends BaseEntityWithTimestamps { @Property({ nullable: true }) - scope?: string; + domain?: DeletionDomainModel; @Property({ nullable: true }) - operation?: string; + operation?: DeletionOperationModel; @Property({ nullable: true }) - docIds?: ObjectId[]; + modifiedCounter?: number; + + @Property({ nullable: true }) + deletedCounter?: number; @Property({ nullable: true }) deletionRequestId?: ObjectId; constructor(props: DeletionLogEntityProps) { super(); - if (props.id !== undefined) { this.id = props.id; } - if (props.createdAt !== undefined) { - this.createdAt = props.createdAt; - } - - if (props.scope !== undefined) { - this.scope = props.scope; + if (props.domain !== undefined) { + this.domain = props.domain; } if (props.operation !== undefined) { this.operation = props.operation; } - if (props.updatedAt !== undefined) { - this.updatedAt = props.updatedAt; + if (props.modifiedCounter !== undefined) { + this.modifiedCounter = props.modifiedCounter; + } + + if (props.deletedCounter !== undefined) { + this.deletedCounter = props.deletedCounter; } if (props.deletionRequestId !== undefined) { this.deletionRequestId = props.deletionRequestId; } - this.docIds = props.docIds; + if (props.createdAt !== undefined) { + this.createdAt = props.createdAt; + } + + if (props.updatedAt !== undefined) { + this.updatedAt = props.updatedAt; + } } } 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 new file mode 100644 index 00000000000..fca7bb87b57 --- /dev/null +++ b/apps/server/src/modules/deletion/entity/deletion-request.entity.spec.ts @@ -0,0 +1,59 @@ +import { setupEntities } from '@shared/testing'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { DeletionRequestEntity } from '@src/modules/deletion/entity/deletion-request.entity'; +import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; +import { DeletionStatusModel } from '../domain/types/deletion-status-model.enum'; + +describe(DeletionRequestEntity.name, () => { + beforeAll(async () => { + await setupEntities(); + }); + + describe('constructor', () => { + describe('When constructor is called', () => { + const setup = () => { + const props = { + id: new ObjectId().toHexString(), + domain: DeletionDomainModel.USER, + deleteAfter: new Date(), + itemId: new ObjectId().toHexString(), + status: DeletionStatusModel.REGISTERED, + createdAt: new Date(), + updatedAt: new Date(), + }; + + return { props }; + }; + + it('should throw an error by empty constructor', () => { + // @ts-expect-error: Test case + const test = () => new DeletionRequestEntity(); + expect(test).toThrow(); + }); + + it('should create a deletionRequest by passing required properties', () => { + const { props } = setup(); + const entity: DeletionRequestEntity = new DeletionRequestEntity(props); + + expect(entity instanceof DeletionRequestEntity).toEqual(true); + }); + + it(`should return a valid object with fields values set from the provided complete props object`, () => { + const { props } = setup(); + const entity: DeletionRequestEntity = new DeletionRequestEntity(props); + + const entityProps = { + id: entity.id, + domain: entity.domain, + deleteAfter: entity.deleteAfter, + itemId: entity.itemId, + status: entity.status, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + }; + + expect(entityProps).toEqual(props); + }); + }); + }); +}); 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 dd4ac0a2258..f76dbedcb8b 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,18 @@ -import { Entity, Property } from '@mikro-orm/core'; -import { BaseEntityWithTimestamps, EntityId } from '@shared/domain'; +import { Entity, Index, Property } from '@mikro-orm/core'; +import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; import { ObjectId } from '@mikro-orm/mongodb'; +import { EntityId } from '@shared/domain'; +import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; +import { DeletionStatusModel } from '../domain/types/deletion-status-model.enum'; export interface DeletionRequestEntityProps { id?: EntityId; - source?: string; + domain?: DeletionDomainModel; deleteAfter?: Date; - userId?: ObjectId; + itemId?: EntityId; + status?: DeletionStatusModel; + createdAt?: Date; + updatedAt?: Date; } @Entity({ tableName: 'deletionrequests' }) @@ -14,25 +20,48 @@ export class DeletionRequestEntity extends BaseEntityWithTimestamps { @Property({ nullable: true }) deleteAfter?: Date; - @Property({ fieldName: 'userToDeletion', nullable: true }) - userId?: ObjectId; + @Property({ fieldName: 'itemToDeletion', nullable: true }) + @Index() + _itemId?: ObjectId; + + get itemId(): EntityId | undefined { + return this._itemId?.toHexString(); + } + + @Property({ nullable: true }) + domain?: DeletionDomainModel; @Property({ nullable: true }) - source?: string; + status?: DeletionStatusModel; constructor(props: DeletionRequestEntityProps) { super(); + if (props.id !== undefined) { + this.id = props.id; + } - if (props.source !== undefined) { - this.source = props.source; + if (props.domain !== undefined) { + this.domain = props.domain; } if (props.deleteAfter !== undefined) { this.deleteAfter = props.deleteAfter; } - if (props.userId !== undefined) { - this.userId = props.userId; + if (props.itemId !== undefined) { + this._itemId = new ObjectId(props.itemId); + } + + if (props.status !== undefined) { + this.status = props.status; + } + + if (props.createdAt !== undefined) { + this.createdAt = props.createdAt; + } + + if (props.updatedAt !== undefined) { + this.updatedAt = props.updatedAt; } } } diff --git a/apps/server/src/modules/deletion/entity/testing/deletion-request.entity.spec.ts b/apps/server/src/modules/deletion/entity/testing/deletion-request.entity.spec.ts deleted file mode 100644 index ef279ee6a08..00000000000 --- a/apps/server/src/modules/deletion/entity/testing/deletion-request.entity.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { setupEntities } from '@shared/testing'; -import { DeletionRequestEntity } from '../deletion-request.entity'; -import { deletionRequestEntityFactory } from './factory/deletion-request.entity.factory'; - -describe(DeletionRequestEntity.name, () => { - beforeAll(async () => { - await setupEntities(); - }); - - describe('constructor', () => { - describe('When constructor is called', () => { - it('should create a deletionRequest by passing required properties', () => { - const entity: DeletionRequestEntity = deletionRequestEntityFactory.build(); - - expect(entity instanceof DeletionRequestEntity).toEqual(true); - }); - }); - }); -}); 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 new file mode 100644 index 00000000000..ed7f975cc0f --- /dev/null +++ b/apps/server/src/modules/deletion/entity/testing/factory/deletion-log.entity.factory.ts @@ -0,0 +1,21 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BaseFactory } from '@shared/testing'; +import { DeletionLogEntity, DeletionLogEntityProps } from '@src/modules/deletion/entity'; +import { DeletionOperationModel } from '@src/modules/deletion/domain/types/deletion-operation-model.enum'; +import { DeletionDomainModel } from '@src/modules/deletion/domain/types/deletion-domain-model.enum'; + +export const deletionLogEntityFactory = BaseFactory.define( + DeletionLogEntity, + () => { + return { + id: new ObjectId().toHexString(), + domain: DeletionDomainModel.USER, + operation: DeletionOperationModel.DELETE, + modifiedCounter: 0, + deletedCounter: 1, + 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 82cc7290c54..6e1a0f326ec 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,14 +1,20 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { BaseFactory } from '@shared/testing'; -import { DeletionRequestEntity, DeletionRequestEntityProps } from '../..'; +import { DeletionDomainModel } from '@src/modules/deletion/domain/types/deletion-domain-model.enum'; +import { DeletionStatusModel } from '@src/modules/deletion/domain/types/deletion-status-model.enum'; +import { DeletionRequestEntity, DeletionRequestEntityProps } from '@src/modules/deletion/entity'; export const deletionRequestEntityFactory = BaseFactory.define( DeletionRequestEntity, () => { return { - source: 'shd', + id: new ObjectId().toHexString(), + domain: DeletionDomainModel.USER, deleteAfter: new Date(), - userId: new ObjectId(), + itemId: new ObjectId().toHexString(), + status: DeletionStatusModel.REGISTERED, + createdAt: new Date(), + updatedAt: new Date(), }; } ); 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 new file mode 100644 index 00000000000..c36a00753a7 --- /dev/null +++ b/apps/server/src/modules/deletion/repo/deletion-log.repo.spec.ts @@ -0,0 +1,186 @@ +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { Test } from '@nestjs/testing'; +import { TestingModule } from '@nestjs/testing/testing-module'; +import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { cleanupCollections } from '@shared/testing'; +import { DeletionLogMapper } from './mapper'; +import { DeletionLogEntity } from '../entity'; +import { DeletionLogRepo } from './deletion-log.repo'; +import { deletionLogFactory } from '../domain/testing/factory/deletion-log.factory'; +import { DeletionLog } from '../domain/deletion-log.do'; +import { deletionLogEntityFactory } from '../entity/testing/factory/deletion-log.entity.factory'; + +describe(DeletionLogRepo.name, () => { + let module: TestingModule; + let repo: DeletionLogRepo; + let em: EntityManager; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ + MongoMemoryDatabaseModule.forRoot({ + entities: [DeletionLogEntity], + }), + ], + providers: [DeletionLogRepo, DeletionLogMapper], + }).compile(); + + repo = module.get(DeletionLogRepo); + em = module.get(EntityManager); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(async () => { + await cleanupCollections(em); + }); + + describe('defined', () => { + it('repo should be defined', () => { + expect(repo).toBeDefined(); + }); + + it('entity manager should be defined', () => { + expect(em).toBeDefined(); + }); + + it('should implement entityName getter', () => { + expect(repo.entityName).toBe(DeletionLogEntity); + }); + }); + + describe('create deletionLog', () => { + describe('when deletionLog is new', () => { + it('should create a new deletionLog', async () => { + const domainObject: DeletionLog = deletionLogFactory.build(); + const deletionLogId = domainObject.id; + await repo.create(domainObject); + + const expectedDomainObject = { + id: domainObject.id, + domain: domainObject.domain, + operation: domainObject.operation, + modifiedCounter: domainObject.modifiedCounter, + deletedCounter: domainObject.deletedCounter, + deletionRequestId: domainObject.deletionRequestId, + createdAt: domainObject.createdAt, + updatedAt: domainObject.updatedAt, + }; + + const result = await repo.findById(deletionLogId); + + // expect(result).toEqual(domainObject); + expect(result).toEqual(expect.objectContaining(expectedDomainObject)); + }); + }); + }); + + describe('findById', () => { + describe('when searching by Id', () => { + const setup = async () => { + // Test deletionLog entity + const entity: DeletionLogEntity = deletionLogEntityFactory.build(); + await em.persistAndFlush(entity); + + const expectedDeletionLog = { + id: entity.id, + domain: entity.domain, + operation: entity.operation, + modifiedCounter: entity.modifiedCounter, + deletedCounter: entity.deletedCounter, + deletionRequestId: entity.deletionRequestId?.toHexString(), + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + }; + + return { + entity, + expectedDeletionLog, + }; + }; + + it('should find the deletionRequest', async () => { + const { entity, expectedDeletionLog } = await setup(); + + const result: DeletionLog = await repo.findById(entity.id); + + // Verify explicit fields. + expect(result).toEqual(expect.objectContaining(expectedDeletionLog)); + }); + }); + }); + + describe('findAllByDeletionRequestId', () => { + describe('when there is no deletionLog for deletionRequestId', () => { + it('should return empty array', async () => { + const deletionRequestId = new ObjectId().toHexString(); + const result = await repo.findAllByDeletionRequestId(deletionRequestId); + + expect(result).toEqual([]); + }); + }); + + describe('when searching by deletionRequestId', () => { + const setup = async () => { + const deletionRequest1Id = new ObjectId(); + const deletionRequest2Id = new ObjectId(); + const deletionLogEntity1: DeletionLogEntity = deletionLogEntityFactory.build({ + deletionRequestId: deletionRequest1Id, + }); + const deletionLogEntity2: DeletionLogEntity = deletionLogEntityFactory.build({ + deletionRequestId: deletionRequest1Id, + }); + const deletionLogEntity3: DeletionLogEntity = deletionLogEntityFactory.build({ + deletionRequestId: deletionRequest2Id, + }); + + await em.persistAndFlush([deletionLogEntity1, deletionLogEntity2, deletionLogEntity3]); + em.clear(); + + return { deletionLogEntity1, deletionLogEntity2, deletionLogEntity3, deletionRequest1Id }; + }; + + it('should find deletionRequests with deleteAfter smaller then today', async () => { + const { deletionLogEntity1, deletionLogEntity2, deletionLogEntity3, deletionRequest1Id } = await setup(); + + const results = await repo.findAllByDeletionRequestId(deletionRequest1Id.toHexString()); + + const expectedArray = [ + { + id: deletionLogEntity1.id, + domain: deletionLogEntity1.domain, + operation: deletionLogEntity1.operation, + deletionRequestId: deletionLogEntity1.deletionRequestId?.toHexString(), + modifiedCounter: deletionLogEntity1.modifiedCounter, + deletedCounter: deletionLogEntity1.deletedCounter, + createdAt: deletionLogEntity1.createdAt, + updatedAt: deletionLogEntity1.updatedAt, + }, + { + id: deletionLogEntity2.id, + domain: deletionLogEntity2.domain, + operation: deletionLogEntity2.operation, + deletionRequestId: deletionLogEntity2.deletionRequestId?.toHexString(), + modifiedCounter: deletionLogEntity2.modifiedCounter, + deletedCounter: deletionLogEntity2.deletedCounter, + createdAt: deletionLogEntity2.createdAt, + updatedAt: deletionLogEntity2.updatedAt, + }, + ]; + + expect(results.length).toEqual(2); + + // Verify explicit fields. + expect(results).toEqual( + expect.arrayContaining([expect.objectContaining(expectedArray[0]), expect.objectContaining(expectedArray[1])]) + ); + + const result: DeletionLog = await repo.findById(deletionLogEntity3.id); + + expect(result.id).toEqual(deletionLogEntity3.id); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/repo/deletion-log.repo.ts b/apps/server/src/modules/deletion/repo/deletion-log.repo.ts index 0c0ace43c6d..d71032eb124 100644 --- a/apps/server/src/modules/deletion/repo/deletion-log.repo.ts +++ b/apps/server/src/modules/deletion/repo/deletion-log.repo.ts @@ -1,4 +1,4 @@ -import { EntityManager } from '@mikro-orm/mongodb'; +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain'; import { DeletionLog } from '../domain/deletion-log.do'; @@ -9,17 +9,33 @@ import { DeletionLogMapper } from './mapper/deletion-log.mapper'; export class DeletionLogRepo { constructor(private readonly em: EntityManager) {} - async findById(id: EntityId): Promise { - const deletionRequest: DeletionLogEntity = await this.em.findOneOrFail(DeletionLogEntity, { id }); + get entityName() { + return DeletionLogEntity; + } + + async findById(deletionLogId: EntityId): Promise { + const deletionLog: DeletionLogEntity = await this.em.findOneOrFail(DeletionLogEntity, { + id: deletionLogId, + }); - const mapped: DeletionLog = DeletionLogMapper.mapToDO(deletionRequest); + const mapped: DeletionLog = DeletionLogMapper.mapToDO(deletionLog); return mapped; } - // create + async findAllByDeletionRequestId(deletionRequestId: EntityId): Promise { + const deletionLogEntities: DeletionLogEntity[] = await this.em.find(DeletionLogEntity, { + deletionRequestId: new ObjectId(deletionRequestId), + }); + + const mapped: DeletionLog[] = DeletionLogMapper.mapToDOs(deletionLogEntities); - // update + return mapped; + } - // delete + async create(deletionLog: DeletionLog): Promise { + const deletionLogEntity: DeletionLogEntity = DeletionLogMapper.mapToEntity(deletionLog); + this.em.persist(deletionLogEntity); + await this.em.flush(); + } } diff --git a/apps/server/src/modules/deletion/repo/deletion-request.repo.spec.ts b/apps/server/src/modules/deletion/repo/deletion-request.repo.spec.ts new file mode 100644 index 00000000000..d52b43407aa --- /dev/null +++ b/apps/server/src/modules/deletion/repo/deletion-request.repo.spec.ts @@ -0,0 +1,218 @@ +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { Test } from '@nestjs/testing'; +import { TestingModule } from '@nestjs/testing/testing-module'; +import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { cleanupCollections } from '@shared/testing'; +import { DeletionRequestMapper } from './mapper'; +import { DeletionRequestRepo } from './deletion-request.repo'; +import { DeletionRequestEntity } from '../entity'; +import { DeletionRequest } from '../domain/deletion-request.do'; +import { deletionRequestEntityFactory } from '../entity/testing/factory/deletion-request.entity.factory'; +import { deletionRequestFactory } from '../domain/testing/factory/deletion-request.factory'; +import { DeletionStatusModel } from '../domain/types/deletion-status-model.enum'; + +describe(DeletionRequestRepo.name, () => { + let module: TestingModule; + let repo: DeletionRequestRepo; + let em: EntityManager; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ + MongoMemoryDatabaseModule.forRoot({ + entities: [DeletionRequestEntity], + }), + ], + providers: [DeletionRequestRepo, DeletionRequestMapper], + }).compile(); + + repo = module.get(DeletionRequestRepo); + em = module.get(EntityManager); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(async () => { + await cleanupCollections(em); + }); + + describe('defined', () => { + it('repo should be defined', () => { + expect(repo).toBeDefined(); + }); + + it('entity manager should be defined', () => { + expect(em).toBeDefined(); + }); + + it('should implement entityName getter', () => { + expect(repo.entityName).toBe(DeletionRequestEntity); + }); + }); + + describe('create deletionRequest', () => { + describe('when deletionRequest is new', () => { + it('should create a new deletionRequest', async () => { + const domainObject: DeletionRequest = deletionRequestFactory.build(); + const deletionRequestId = domainObject.id; + await repo.create(domainObject); + + const result = await repo.findById(deletionRequestId); + + expect(result).toEqual(domainObject); + }); + }); + }); + + describe('findById', () => { + describe('when searching by Id', () => { + const setup = async () => { + const userId = new ObjectId().toHexString(); + + const entity: DeletionRequestEntity = deletionRequestEntityFactory.build({ itemId: userId }); + await em.persistAndFlush(entity); + + const expectedDeletionRequest = { + id: entity.id, + domain: entity.domain, + deleteAfter: entity.deleteAfter, + itemId: entity.itemId, + status: entity.status, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + }; + + return { + entity, + expectedDeletionRequest, + }; + }; + + it('should find the deletionRequest', async () => { + const { entity, expectedDeletionRequest } = await setup(); + + const result: DeletionRequest = await repo.findById(entity.id); + + // Verify explicit fields. + expect(result).toEqual(expect.objectContaining(expectedDeletionRequest)); + }); + }); + }); + + describe('findAllItemsByDeletionDate', () => { + describe('when there is no deletionRequest for execution', () => { + it('should return empty array', async () => { + const result = await repo.findAllItemsByDeletionDate(); + + expect(result).toEqual([]); + }); + }); + + describe('when there are deletionRequests for execution', () => { + const setup = async () => { + const dateInFuture = new Date(); + dateInFuture.setDate(dateInFuture.getDate() + 30); + const deletionRequestEntity1: DeletionRequestEntity = deletionRequestEntityFactory.build({ + deleteAfter: new Date(2023, 9, 1), + }); + const deletionRequestEntity2: DeletionRequestEntity = deletionRequestEntityFactory.build({ + deleteAfter: new Date(2023, 9, 1), + }); + const deletionRequestEntity3: DeletionRequestEntity = deletionRequestEntityFactory.build({ + deleteAfter: dateInFuture, + }); + + await em.persistAndFlush([deletionRequestEntity1, deletionRequestEntity2, deletionRequestEntity3]); + em.clear(); + + return { deletionRequestEntity1, deletionRequestEntity2, deletionRequestEntity3 }; + }; + + it('should find deletionRequests with deleteAfter smaller then today', async () => { + const { deletionRequestEntity1, deletionRequestEntity2, deletionRequestEntity3 } = await setup(); + + const results = await repo.findAllItemsByDeletionDate(); + + const expectedArray = [ + { + id: deletionRequestEntity1.id, + domain: deletionRequestEntity1.domain, + deleteAfter: deletionRequestEntity1.deleteAfter, + itemId: deletionRequestEntity1.itemId, + status: deletionRequestEntity1.status, + createdAt: deletionRequestEntity1.createdAt, + updatedAt: deletionRequestEntity1.updatedAt, + }, + { + id: deletionRequestEntity2.id, + domain: deletionRequestEntity2.domain, + deleteAfter: deletionRequestEntity2.deleteAfter, + itemId: deletionRequestEntity2.itemId, + status: deletionRequestEntity2.status, + createdAt: deletionRequestEntity2.createdAt, + updatedAt: deletionRequestEntity2.updatedAt, + }, + ]; + + expect(results.length).toEqual(2); + + // Verify explicit fields. + expect(results).toEqual( + expect.arrayContaining([expect.objectContaining(expectedArray[0]), expect.objectContaining(expectedArray[1])]) + ); + + const result: DeletionRequest = await repo.findById(deletionRequestEntity3.id); + + expect(result.id).toEqual(deletionRequestEntity3.id); + }); + }); + }); + + describe('deleteById', () => { + describe('when deleting deletionRequest exists', () => { + const setup = async () => { + const userId = new ObjectId().toHexString(); + const entity: DeletionRequestEntity = deletionRequestEntityFactory.build({ itemId: userId }); + const deletionRequestId = entity.id; + await em.persistAndFlush(entity); + em.clear(); + + return { deletionRequestId }; + }; + + it('should delete the deletionRequest with deletionRequestId', async () => { + const { deletionRequestId } = await setup(); + + await repo.deleteById(deletionRequestId); + + expect(await em.findOne(DeletionRequestEntity, { id: deletionRequestId })).toBeNull(); + }); + + it('should return true', async () => { + const { deletionRequestId } = await setup(); + + const result: boolean = await repo.deleteById(deletionRequestId); + + expect(result).toEqual(true); + }); + }); + + describe('when no deletionRequestEntity exists', () => { + const setup = () => { + const deletionRequestId = new ObjectId().toHexString(); + + return { deletionRequestId }; + }; + + it('should return false', async () => { + const { deletionRequestId } = setup(); + + const result: boolean = await repo.deleteById(deletionRequestId); + + expect(result).toEqual(false); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/repo/deletion-request.repo.ts b/apps/server/src/modules/deletion/repo/deletion-request.repo.ts index b386596733c..a2f9a491504 100644 --- a/apps/server/src/modules/deletion/repo/deletion-request.repo.ts +++ b/apps/server/src/modules/deletion/repo/deletion-request.repo.ts @@ -9,27 +9,26 @@ import { DeletionRequestMapper } from './mapper/deletion-request.mapper'; export class DeletionRequestRepo { constructor(private readonly em: EntityManager) {} - async findById(id: EntityId): Promise { - const deletionRequest: DeletionRequestEntity = await this.em.findOneOrFail(DeletionRequestEntity, { id }); + get entityName() { + return DeletionRequestEntity; + } + + async findById(deletionRequestId: EntityId): Promise { + const deletionRequest: DeletionRequestEntity = await this.em.findOneOrFail(DeletionRequestEntity, { + id: deletionRequestId, + }); const mapped: DeletionRequest = DeletionRequestMapper.mapToDO(deletionRequest); return mapped; } - // create async create(deletionRequest: DeletionRequest): Promise { const deletionRequestEntity = DeletionRequestMapper.mapToEntity(deletionRequest); - await this.em.persistAndFlush(deletionRequestEntity); - } - - // update - async update(deletionRequest: DeletionRequest): Promise { - const deletionRequestEntity = DeletionRequestMapper.mapToEntity(deletionRequest); - await this.em.persistAndFlush(deletionRequestEntity); + this.em.persist(deletionRequestEntity); + await this.em.flush(); } - // find async findAllItemsByDeletionDate(): Promise { const currentDate = new Date(); const itemsToDelete: DeletionRequestEntity[] = await this.em.find(DeletionRequestEntity, { @@ -41,5 +40,22 @@ export class DeletionRequestRepo { return mapped; } - // delete + async update(deletionRequest: DeletionRequest): Promise { + const deletionRequestEntity = DeletionRequestMapper.mapToEntity(deletionRequest); + await this.em.persistAndFlush(deletionRequestEntity); + } + + async deleteById(deletionRequestId: EntityId): Promise { + const entity: DeletionRequestEntity | null = await this.em.findOne(DeletionRequestEntity, { + id: deletionRequestId, + }); + + if (!entity) { + return false; + } + + await this.em.removeAndFlush(entity); + + return true; + } } 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 new file mode 100644 index 00000000000..c82acbb2e8e --- /dev/null +++ b/apps/server/src/modules/deletion/repo/mapper/deletion-log.mapper.spec.ts @@ -0,0 +1,139 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { deletionLogEntityFactory } from '../../entity/testing/factory/deletion-log.entity.factory'; +import { DeletionLogMapper } from './deletion-log.mapper'; +import { DeletionLog } from '../../domain/deletion-log.do'; +import { deletionLogFactory } from '../../domain/testing/factory/deletion-log.factory'; +import { DeletionLogEntity } from '../../entity'; + +describe(DeletionLogMapper.name, () => { + describe('mapToDO', () => { + describe('When entity is mapped for domainObject', () => { + it('should properly map the entity to the domain object', () => { + const entity = deletionLogEntityFactory.build(); + + const domainObject = DeletionLogMapper.mapToDO(entity); + + const expectedDomainObject = new DeletionLog({ + id: entity.id, + domain: entity.domain, + operation: entity.operation, + deletionRequestId: entity.deletionRequestId?.toHexString(), + modifiedCounter: entity.modifiedCounter, + deletedCounter: entity.deletedCounter, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + }); + + expect(domainObject).toEqual(expectedDomainObject); + }); + }); + }); + + describe('mapToDOs', () => { + describe('When empty entities array is mapped for an empty domainObjects array', () => { + it('should return empty domain objects array for an empty entities array', () => { + const domainObjects = DeletionLogMapper.mapToDOs([]); + + expect(domainObjects).toEqual([]); + }); + }); + + describe('When entities array is mapped for domainObjects array', () => { + it('should properly map the entities to the domain objects', () => { + const entities = [deletionLogEntityFactory.build()]; + + const domainObjects = DeletionLogMapper.mapToDOs(entities); + + const expectedDomainObjects = entities.map( + (entity) => + new DeletionLog({ + id: entity.id, + domain: entity.domain, + operation: entity.operation, + deletionRequestId: entity.deletionRequestId?.toHexString(), + modifiedCounter: entity.modifiedCounter, + deletedCounter: entity.deletedCounter, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + }) + ); + + expect(domainObjects).toEqual(expectedDomainObjects); + }); + }); + }); + + describe('mapToEntity', () => { + describe('When domainObject is mapped for entity', () => { + beforeAll(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date()); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('should properly map the domainObject to the entity', () => { + const domainObject = deletionLogFactory.build(); + + const entities = DeletionLogMapper.mapToEntity(domainObject); + + const expectedEntities = new DeletionLogEntity({ + id: domainObject.id, + domain: domainObject.domain, + operation: domainObject.operation, + deletionRequestId: new ObjectId(domainObject.deletionRequestId), + modifiedCounter: domainObject.modifiedCounter, + deletedCounter: domainObject.deletedCounter, + createdAt: domainObject.createdAt, + updatedAt: domainObject.updatedAt, + }); + + expect(entities).toEqual(expectedEntities); + }); + }); + }); + + describe('mapToEntities', () => { + describe('When empty domainObjects array is mapped for an entities array', () => { + it('should return empty entities array for an empty domain objects array', () => { + const entities = DeletionLogMapper.mapToEntities([]); + + expect(entities).toEqual([]); + }); + }); + + describe('When domainObjects array is mapped for entities array', () => { + beforeAll(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date()); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('should properly map the domainObjects to the entities', () => { + const domainObjects = [deletionLogFactory.build()]; + + const entities = DeletionLogMapper.mapToEntities(domainObjects); + + const expectedEntities = domainObjects.map( + (domainObject) => + new DeletionLogEntity({ + id: domainObject.id, + domain: domainObject.domain, + operation: domainObject.operation, + deletionRequestId: new ObjectId(domainObject.deletionRequestId), + modifiedCounter: domainObject.modifiedCounter, + deletedCounter: domainObject.deletedCounter, + createdAt: domainObject.createdAt, + updatedAt: domainObject.updatedAt, + }) + ); + expect(entities).toEqual(expectedEntities); + }); + }); + }); +}); 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 9c3656e4d34..506e7369c84 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 @@ -8,9 +8,10 @@ export class DeletionLogMapper { id: entity.id, createdAt: entity.createdAt, updatedAt: entity.updatedAt, - scope: entity.scope, + domain: entity.domain, operation: entity.operation, - docIds: entity.docIds?.map((docId) => docId.toHexString()), + modifiedCounter: entity.modifiedCounter, + deletedCounter: entity.deletedCounter, deletionRequestId: entity.deletionRequestId?.toHexString(), }); } @@ -20,18 +21,19 @@ export class DeletionLogMapper { id: domainObject.id, createdAt: domainObject.createdAt, updatedAt: domainObject.updatedAt, - scope: domainObject.scope, + domain: domainObject.domain, operation: domainObject.operation, - docIds: domainObject.docIds?.map((docId) => new ObjectId(docId)), + modifiedCounter: domainObject.modifiedCounter, + deletedCounter: domainObject.deletedCounter, deletionRequestId: new ObjectId(domainObject.deletionRequestId), }); } - // static mapToDOs(entities: DeletionLogEntity[]): DeletionLog[] { - // return entities.map((entity) => this.mapToDO(entity)); - // } + static mapToDOs(entities: DeletionLogEntity[]): DeletionLog[] { + return entities.map((entity) => this.mapToDO(entity)); + } - // static mapToEntities(domainObjects: DeletionLog[]): DeletionLogEntity[] { - // return domainObjects.map((domainObject) => this.mapToEntity(domainObject)); - // } + static mapToEntities(domainObjects: DeletionLog[]): DeletionLogEntity[] { + return domainObjects.map((domainObject) => this.mapToEntity(domainObject)); + } } diff --git a/apps/server/src/modules/deletion/repo/mapper/deletion-request.mapper.spec.ts b/apps/server/src/modules/deletion/repo/mapper/deletion-request.mapper.spec.ts new file mode 100644 index 00000000000..a4bbedc9946 --- /dev/null +++ b/apps/server/src/modules/deletion/repo/mapper/deletion-request.mapper.spec.ts @@ -0,0 +1,60 @@ +import { DeletionRequest } from '../../domain/deletion-request.do'; +import { deletionRequestFactory } from '../../domain/testing/factory/deletion-request.factory'; +import { DeletionRequestEntity } from '../../entity'; +import { deletionRequestEntityFactory } from '../../entity/testing/factory/deletion-request.entity.factory'; +import { DeletionRequestMapper } from './deletion-request.mapper'; + +describe(DeletionRequestMapper.name, () => { + describe('mapToDO', () => { + describe('When entity is mapped for domainObject', () => { + it('should properly map the entity to the domain object', () => { + const entity = deletionRequestEntityFactory.build(); + + const domainObject = DeletionRequestMapper.mapToDO(entity); + + const expectedDomainObject = new DeletionRequest({ + id: entity.id, + domain: entity.domain, + deleteAfter: entity.deleteAfter, + itemId: entity.itemId, + status: entity.status, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + }); + + expect(domainObject).toEqual(expectedDomainObject); + }); + }); + }); + + describe('mapToEntity', () => { + describe('When domainObject is mapped for entity', () => { + beforeAll(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date()); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('should properly map the domainObject to the entity', () => { + const domainObject = deletionRequestFactory.build(); + + const entity = DeletionRequestMapper.mapToEntity(domainObject); + + const expectedEntity = new DeletionRequestEntity({ + id: domainObject.id, + domain: domainObject.domain, + deleteAfter: domainObject.deleteAfter, + itemId: domainObject.itemId, + status: domainObject.status, + createdAt: domainObject.createdAt, + updatedAt: domainObject.updatedAt, + }); + + expect(entity).toEqual(expectedEntity); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/repo/mapper/deletion-request.mapper.ts b/apps/server/src/modules/deletion/repo/mapper/deletion-request.mapper.ts index 879352117b6..c757606ce69 100644 --- a/apps/server/src/modules/deletion/repo/mapper/deletion-request.mapper.ts +++ b/apps/server/src/modules/deletion/repo/mapper/deletion-request.mapper.ts @@ -8,26 +8,22 @@ export class DeletionRequestMapper { id: entity.id, createdAt: entity.createdAt, updatedAt: entity.updatedAt, - source: entity.source, + domain: entity.domain, deleteAfter: entity.deleteAfter, - userId: entity.userId?.toHexString(), + itemId: entity.itemId, + status: entity.status, }); } static mapToEntity(domainObject: DeletionRequest): DeletionRequestEntity { return new DeletionRequestEntity({ id: domainObject.id, - source: domainObject.source, + domain: domainObject.domain, deleteAfter: domainObject.deleteAfter, - userId: new ObjectId(domainObject.userId), + itemId: new ObjectId(domainObject.itemId).toHexString(), + createdAt: domainObject.createdAt, + updatedAt: domainObject.updatedAt, + status: domainObject.status, }); } - - // static mapToDOs(entities: DeletionRequestEntity[]): DeletionRequest[] { - // return entities.map((entity) => this.mapToDOs(entity)); - // } - - // static mapToEntities(domainObjects: DeletionRequest[]): DeletionRequestEntity[] { - // return domainObjects.map((domainObject) => this.mapToEntities(domainObject)); - // } } diff --git a/apps/server/src/modules/deletion/repo/mapper/index.ts b/apps/server/src/modules/deletion/repo/mapper/index.ts new file mode 100644 index 00000000000..0407135b228 --- /dev/null +++ b/apps/server/src/modules/deletion/repo/mapper/index.ts @@ -0,0 +1,2 @@ +export * from './deletion-request.mapper'; +export * from './deletion-log.mapper'; diff --git a/apps/server/src/modules/deletion/services/deletion-log.service.ts b/apps/server/src/modules/deletion/services/deletion-log.service.ts new file mode 100644 index 00000000000..70290bc0c25 --- /dev/null +++ b/apps/server/src/modules/deletion/services/deletion-log.service.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@nestjs/common'; +import { DeletionLogRepo } from '../repo'; + +@Injectable() +export class DeletionLogService { + constructor(private readonly deletionLogRepo: DeletionLogRepo) {} + + // async createLogRequest(userId: EntityId): Promise { + // const dateInFuture = new Date(); + // dateInFuture.setDate(dateInFuture.getDate() + 30); + + // const newDeletionLog = new DeletionLog({ + // id: new ObjectId().toHexString(), + // scope: 'scope', + // operation: DeletionOperationModel.DELETE, + // deletionRequestId: new ObjectId(), + // docIds: [new ObjectId(), new ObjectId()], + // }); + + // await this.deletionLogRepo.create(newDeletionLog); + + // return newDeletionLof.id; + // } + + // async findById(deletionRequestId: EntityId): Promise { + // const deletionRequest: DeletionRequest = await this.deletionRequestRepo.findById(deletionRequestId); + + // return deletionRequest; + // } + + // async findAllItemsByDeletionDate(): Promise { + // const itemsToDelete: DeletionRequest[] = await this.deletionRequestRepo.findAllItemsByDeletionDate(); + + // return itemsToDelete; + // } + + // async deleteById(deletionRequestId: EntityId): Promise { + // await this.deletionRequestRepo.deleteById(deletionRequestId); + // } +} 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 new file mode 100644 index 00000000000..4013f3adcad --- /dev/null +++ b/apps/server/src/modules/deletion/services/deletion-request.service.spec.ts @@ -0,0 +1,133 @@ +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 { DeletionRequestService } from './deletion-request.service'; +import { DeletionRequestRepo } from '../repo'; +import { deletionRequestFactory } from '../domain/testing/factory/deletion-request.factory'; + +describe(DeletionRequestService.name, () => { + let module: TestingModule; + let service: DeletionRequestService; + let deletionRequestRepo: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + DeletionRequestService, + { + provide: DeletionRequestRepo, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(DeletionRequestService); + deletionRequestRepo = module.get(DeletionRequestRepo); + + await setupEntities(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('defined', () => { + it('should be defined', () => { + expect(service).toBeDefined(); + }); + }); + + // TODO createDeletionRequest + // describe('createDeletionRequest', () => { + // describe('when creating a deletionRequest', () => { + // const setup = () => { + // const deletionRequest = deletionRequestFactory.build(); + + // return { deletionRequest }; + // }; + + // it('should '); + // }); + // }); + + describe('findById', () => { + describe('when finding by deletionRequestId', () => { + const setup = () => { + const deletionRequestId = new ObjectId().toHexString(); + const deletionRequest = deletionRequestFactory.build({ id: deletionRequestId }); + + deletionRequestRepo.findById.mockResolvedValue(deletionRequest); + + return { deletionRequestId, deletionRequest }; + }; + + it('should call deletionRequestRepo.findById', async () => { + const { deletionRequestId } = setup(); + + await service.findById(deletionRequestId); + + expect(deletionRequestRepo.findById).toBeCalledWith(deletionRequestId); + }); + + it('should return deletionRequest', async () => { + const { deletionRequestId, deletionRequest } = setup(); + + const result = await service.findById(deletionRequestId); + + expect(result).toEqual(deletionRequest); + }); + }); + }); + + describe('findAllItemsByDeletionDate', () => { + describe('when finding all deletionRequests for execution', () => { + const setup = () => { + const dateInPast = new Date(); + dateInPast.setDate(dateInPast.getDate() - 1); + const deletionRequest1 = deletionRequestFactory.build({ deleteAfter: dateInPast }); + const deletionRequest2 = deletionRequestFactory.build({ deleteAfter: dateInPast }); + + deletionRequestRepo.findAllItemsByDeletionDate.mockResolvedValue([deletionRequest1, deletionRequest2]); + + const deletionRequests = [deletionRequest1, deletionRequest2]; + return { deletionRequests }; + }; + + it('should call deletionRequestRepo.findAllItemsByDeletionDate', async () => { + await service.findAllItemsByDeletionDate(); + + expect(deletionRequestRepo.findAllItemsByDeletionDate).toBeCalled(); + }); + + it('should return array of two deletionRequests with date smaller than today', async () => { + const { deletionRequests } = setup(); + const result = await service.findAllItemsByDeletionDate(); + + expect(result).toHaveLength(2); + expect(result).toEqual(deletionRequests); + }); + }); + }); + + describe('deleteById', () => { + describe('when deleting deletionRequest', () => { + const setup = () => { + const deletionRequestId = new ObjectId().toHexString(); + + return { deletionRequestId }; + }; + + it('should call deletionRequestRepo.findAllItemsByDeletionDate', async () => { + const { deletionRequestId } = setup(); + await service.deleteById(deletionRequestId); + + expect(deletionRequestRepo.deleteById).toBeCalledWith(deletionRequestId); + }); + }); + }); +}); 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 1f59221ecbe..38889fe37d2 100644 --- a/apps/server/src/modules/deletion/services/deletion-request.service.ts +++ b/apps/server/src/modules/deletion/services/deletion-request.service.ts @@ -3,30 +3,47 @@ import { EntityId } from '@shared/domain'; import { ObjectId } from '@mikro-orm/mongodb'; import { DeletionRequestRepo } from '../repo/deletion-request.repo'; import { DeletionRequest } from '../domain/deletion-request.do'; +import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; @Injectable() export class DeletionRequestService { constructor(private readonly deletionRequestRepo: DeletionRequestRepo) {} - async createDeletionRequest(userId: EntityId): Promise { + async createDeletionRequest( + itemId: EntityId, + domain: DeletionDomainModel, + deleteInMinutes?: number + ): Promise { + deleteInMinutes = deleteInMinutes === undefined ? 43200 : deleteInMinutes; + + const dateOfDeletion = new Date(); + dateOfDeletion.setDate(dateOfDeletion.getDate() + deleteInMinutes * 1000); + const newDeletionRequest = new DeletionRequest({ id: new ObjectId().toHexString(), - source: 'shd', - deleteAfter: new Date(), - userId, + domain, + deleteAfter: dateOfDeletion, + itemId, }); await this.deletionRequestRepo.create(newDeletionRequest); + + return newDeletionRequest.id; } - // updateService - async updateDeletionRequest(deletionRequest: DeletionRequest): Promise { - await this.deletionRequestRepo.update(deletionRequest); + async findById(deletionRequestId: EntityId): Promise { + const deletionRequest: DeletionRequest = await this.deletionRequestRepo.findById(deletionRequestId); + + return deletionRequest; } - // findAll async findAllItemsByDeletionDate(): Promise { const itemsToDelete: DeletionRequest[] = await this.deletionRequestRepo.findAllItemsByDeletionDate(); + return itemsToDelete; } + + async deleteById(deletionRequestId: EntityId): Promise { + await this.deletionRequestRepo.deleteById(deletionRequestId); + } } diff --git a/apps/server/src/modules/deletion/services/index.ts b/apps/server/src/modules/deletion/services/index.ts new file mode 100644 index 00000000000..9661354718c --- /dev/null +++ b/apps/server/src/modules/deletion/services/index.ts @@ -0,0 +1 @@ +export * from './deletion-request.service';