From ca59c48adc5c3efaf8cef24a9f9ef93e05852dd3 Mon Sep 17 00:00:00 2001 From: WojciechGrancow Date: Thu, 19 Oct 2023 23:19:24 +0200 Subject: [PATCH 01/72] first commit --- apps/server/src/apps/deletion.app.ts | 49 ++++++++++++++++ apps/server/src/config/database.config.ts | 3 +- .../src/modules/deletion/deletion.module.ts | 37 ++++++++++++ .../deletion/domain/deletion-log.do.ts | 37 ++++++++++++ .../deletion/domain/deletion-request.do.ts | 32 ++++++++++ .../deletion/entities/deletion-log.entity.ts | 58 +++++++++++++++++++ .../entities/deletion-request.entity.ts | 38 ++++++++++++ .../src/modules/deletion/entities/index.ts | 1 + apps/server/src/modules/deletion/index.ts | 1 + .../deletion/repo/deletion-log.repo.ts | 25 ++++++++ .../deletion/repo/deletion-request.repo.ts | 45 ++++++++++++++ .../server/src/modules/deletion/repo/index.ts | 1 + .../repo/mapper/deletion-log.mapper.ts | 37 ++++++++++++ .../repo/mapper/deletion-request.mapper.ts | 33 +++++++++++ .../services/deletion-request.service.ts | 32 ++++++++++ .../modules/deletion/uc/deletion-worker.us.ts | 16 +++++ 16 files changed, 444 insertions(+), 1 deletion(-) create mode 100644 apps/server/src/apps/deletion.app.ts create mode 100644 apps/server/src/modules/deletion/deletion.module.ts create mode 100644 apps/server/src/modules/deletion/domain/deletion-log.do.ts create mode 100644 apps/server/src/modules/deletion/domain/deletion-request.do.ts create mode 100644 apps/server/src/modules/deletion/entities/deletion-log.entity.ts create mode 100644 apps/server/src/modules/deletion/entities/deletion-request.entity.ts create mode 100644 apps/server/src/modules/deletion/entities/index.ts create mode 100644 apps/server/src/modules/deletion/index.ts create mode 100644 apps/server/src/modules/deletion/repo/deletion-log.repo.ts create mode 100644 apps/server/src/modules/deletion/repo/deletion-request.repo.ts create mode 100644 apps/server/src/modules/deletion/repo/index.ts create mode 100644 apps/server/src/modules/deletion/repo/mapper/deletion-log.mapper.ts create mode 100644 apps/server/src/modules/deletion/repo/mapper/deletion-request.mapper.ts create mode 100644 apps/server/src/modules/deletion/services/deletion-request.service.ts create mode 100644 apps/server/src/modules/deletion/uc/deletion-worker.us.ts diff --git a/apps/server/src/apps/deletion.app.ts b/apps/server/src/apps/deletion.app.ts new file mode 100644 index 00000000000..902831ba391 --- /dev/null +++ b/apps/server/src/apps/deletion.app.ts @@ -0,0 +1,49 @@ +/* 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 ad97e4c3d66..ed0b3767e21 100644 --- a/apps/server/src/config/database.config.ts +++ b/apps/server/src/config/database.config.ts @@ -4,9 +4,10 @@ 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 } = usedGlobals; +export const { DB_URL, DB_PASSWORD, DB_USERNAME, DEL_DB_URL } = usedGlobals; diff --git a/apps/server/src/modules/deletion/deletion.module.ts b/apps/server/src/modules/deletion/deletion.module.ts new file mode 100644 index 00000000000..4481d7ce492 --- /dev/null +++ b/apps/server/src/modules/deletion/deletion.module.ts @@ -0,0 +1,37 @@ +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 { 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.`), +}; + +@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)), + ], + providers: [Logger, DeletionRequestService, DeletionRequestRepo], +}) +export class DeletionModule {} diff --git a/apps/server/src/modules/deletion/domain/deletion-log.do.ts b/apps/server/src/modules/deletion/domain/deletion-log.do.ts new file mode 100644 index 00000000000..a5ae8db4b89 --- /dev/null +++ b/apps/server/src/modules/deletion/domain/deletion-log.do.ts @@ -0,0 +1,37 @@ +import { EntityId } from '@shared/domain/types'; +import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; + +export interface DeletionLogProps extends AuthorizableObject { + createdAt: Date; + updatedAt: Date; + scope?: string; + operation?: string; + docIds?: EntityId[]; + deletionRequestId?: EntityId; +} + +export class DeletionLog extends DomainObject { + get createdAt(): Date { + return this.props.createdAt; + } + + get updatedAt(): Date { + return this.props.updatedAt; + } + + get scope(): string | undefined { + return this.props.scope; + } + + get operation(): string | undefined { + return this.props.operation; + } + + get deletionRequestId(): EntityId | undefined { + return this.props.deletionRequestId; + } + + get docIds(): EntityId[] | undefined { + return this.props.docIds; + } +} diff --git a/apps/server/src/modules/deletion/domain/deletion-request.do.ts b/apps/server/src/modules/deletion/domain/deletion-request.do.ts new file mode 100644 index 00000000000..3a85ecd345c --- /dev/null +++ b/apps/server/src/modules/deletion/domain/deletion-request.do.ts @@ -0,0 +1,32 @@ +import { EntityId } from '@shared/domain/types'; +import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; + +export interface DeletionRequestProps extends AuthorizableObject { + createdAt?: Date; + updatedAt?: Date; + source?: string; + deleteAfter?: Date; + userId?: EntityId; +} + +export class DeletionRequest extends DomainObject { + get createdAt(): Date | undefined { + return this.props.createdAt; + } + + get updatedAt(): Date | undefined { + return this.props.updatedAt; + } + + get source(): string | undefined { + return this.props.source; + } + + get deleteAfter(): Date | undefined { + return this.props.deleteAfter; + } + + get userId(): EntityId | undefined { + return this.props.userId; + } +} diff --git a/apps/server/src/modules/deletion/entities/deletion-log.entity.ts b/apps/server/src/modules/deletion/entities/deletion-log.entity.ts new file mode 100644 index 00000000000..839655016fa --- /dev/null +++ b/apps/server/src/modules/deletion/entities/deletion-log.entity.ts @@ -0,0 +1,58 @@ +import { Entity, Property } from '@mikro-orm/core'; +import { BaseEntityWithTimestamps, EntityId } from '@shared/domain'; +import { ObjectId } from '@mikro-orm/mongodb'; + +export interface DeletionLogEntityProps { + id: EntityId; + 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; + + @Property({ nullable: true }) + operation?: string; + + @Property({ nullable: true }) + docIds?: ObjectId[]; + + @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.operation !== undefined) { + this.operation = props.operation; + } + + if (props.updatedAt !== undefined) { + this.updatedAt = props.updatedAt; + } + + if (props.deletionRequestId !== undefined) { + this.deletionRequestId = props.deletionRequestId; + } + + this.docIds = props.docIds; + } +} diff --git a/apps/server/src/modules/deletion/entities/deletion-request.entity.ts b/apps/server/src/modules/deletion/entities/deletion-request.entity.ts new file mode 100644 index 00000000000..dd4ac0a2258 --- /dev/null +++ b/apps/server/src/modules/deletion/entities/deletion-request.entity.ts @@ -0,0 +1,38 @@ +import { Entity, Property } from '@mikro-orm/core'; +import { BaseEntityWithTimestamps, EntityId } from '@shared/domain'; +import { ObjectId } from '@mikro-orm/mongodb'; + +export interface DeletionRequestEntityProps { + id?: EntityId; + source?: string; + deleteAfter?: Date; + userId?: ObjectId; +} + +@Entity({ tableName: 'deletionrequests' }) +export class DeletionRequestEntity extends BaseEntityWithTimestamps { + @Property({ nullable: true }) + deleteAfter?: Date; + + @Property({ fieldName: 'userToDeletion', nullable: true }) + userId?: ObjectId; + + @Property({ nullable: true }) + source?: string; + + constructor(props: DeletionRequestEntityProps) { + super(); + + if (props.source !== undefined) { + this.source = props.source; + } + + if (props.deleteAfter !== undefined) { + this.deleteAfter = props.deleteAfter; + } + + if (props.userId !== undefined) { + this.userId = props.userId; + } + } +} diff --git a/apps/server/src/modules/deletion/entities/index.ts b/apps/server/src/modules/deletion/entities/index.ts new file mode 100644 index 00000000000..221c543f2a2 --- /dev/null +++ b/apps/server/src/modules/deletion/entities/index.ts @@ -0,0 +1 @@ +export * from './deletion-request.entity'; \ No newline at end of file diff --git a/apps/server/src/modules/deletion/index.ts b/apps/server/src/modules/deletion/index.ts new file mode 100644 index 00000000000..a2fbd9ee46b --- /dev/null +++ b/apps/server/src/modules/deletion/index.ts @@ -0,0 +1 @@ +export * from './deletion.module'; diff --git a/apps/server/src/modules/deletion/repo/deletion-log.repo.ts b/apps/server/src/modules/deletion/repo/deletion-log.repo.ts new file mode 100644 index 00000000000..5d412f5ee67 --- /dev/null +++ b/apps/server/src/modules/deletion/repo/deletion-log.repo.ts @@ -0,0 +1,25 @@ +import { EntityManager } from '@mikro-orm/mongodb'; +import { Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain'; +import { DeletionLog } from '../domain/deletion-log.do'; +import { DeletionLogEntity } from '../entities/deletion-log.entity'; +import { DeletionLogMapper } from './mapper/deletion-log.mapper'; + +@Injectable() +export class DeletionLogRepo { + constructor(private readonly em: EntityManager) {} + + async findById(id: EntityId): Promise { + const deletionRequest: DeletionLogEntity = await this.em.findOneOrFail(DeletionLogEntity, { id }); + + const mapped: DeletionLog = DeletionLogMapper.mapToDO(deletionRequest); + + return mapped; + } + + // create + + // update + + // delete +} diff --git a/apps/server/src/modules/deletion/repo/deletion-request.repo.ts b/apps/server/src/modules/deletion/repo/deletion-request.repo.ts new file mode 100644 index 00000000000..fdd9729b1d2 --- /dev/null +++ b/apps/server/src/modules/deletion/repo/deletion-request.repo.ts @@ -0,0 +1,45 @@ +import { EntityManager } from '@mikro-orm/mongodb'; +import { Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain'; +import { DeletionRequest } from '../domain/deletion-request.do'; +import { DeletionRequestEntity } from '../entities'; +import { DeletionRequestMapper } from './mapper/deletion-request.mapper'; + +@Injectable() +export class DeletionRequestRepo { + constructor(private readonly em: EntityManager) {} + + async findById(id: EntityId): Promise { + const deletionRequest: DeletionRequestEntity = await this.em.findOneOrFail(DeletionRequestEntity, { id }); + + 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); + } + + // find + async findAllItemsByDeletionDate(): Promise { + const currentDate = new Date(); + const itemsToDelete: DeletionRequestEntity[] = await this.em.find(DeletionRequestEntity, { + deleteAfter: { $lt: currentDate }, + }); + + const mapped: DeletionRequest[] = itemsToDelete.map((entity) => DeletionRequestMapper.mapToDO(entity)); + + return mapped; + } + + // delete +} diff --git a/apps/server/src/modules/deletion/repo/index.ts b/apps/server/src/modules/deletion/repo/index.ts new file mode 100644 index 00000000000..d93472feca7 --- /dev/null +++ b/apps/server/src/modules/deletion/repo/index.ts @@ -0,0 +1 @@ +export * from './classes.repo'; 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 new file mode 100644 index 00000000000..681497d9e43 --- /dev/null +++ b/apps/server/src/modules/deletion/repo/mapper/deletion-log.mapper.ts @@ -0,0 +1,37 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { DeletionLogEntity } from '../../entities/deletion-log.entity'; +import { DeletionLog } from '../../domain/deletion-log.do'; + +export class DeletionLogMapper { + static mapToDO(entity: DeletionLogEntity): DeletionLog { + return new DeletionLog({ + id: entity.id, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + scope: entity.scope, + operation: entity.operation, + docIds: entity.docIds?.map((docId) => docId.toHexString()), + deletionRequestId: entity.deletionRequestId?.toHexString(), + }); + } + + static mapToEntity(domainObject: DeletionLog): DeletionLogEntity { + return new DeletionLogEntity({ + id: domainObject.id, + createdAt: domainObject.createdAt, + updatedAt: domainObject.updatedAt, + scope: domainObject.scope, + operation: domainObject.operation, + docIds: domainObject.docIds?.map((docId) => new ObjectId(docId)), + deletionRequestId: new ObjectId(domainObject.deletionRequestId), + }); + } + + // static mapToDOs(entities: DeletionLogEntity[]): DeletionLog[] { + // return entities.map((entity) => this.mapToDO(entity)); + // } + + // 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.ts b/apps/server/src/modules/deletion/repo/mapper/deletion-request.mapper.ts new file mode 100644 index 00000000000..6262ff7a6df --- /dev/null +++ b/apps/server/src/modules/deletion/repo/mapper/deletion-request.mapper.ts @@ -0,0 +1,33 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { DeletionRequest } from '../../domain/deletion-request.do'; +import { DeletionRequestEntity } from '../../entities'; + +export class DeletionRequestMapper { + static mapToDO(entity: DeletionRequestEntity): DeletionRequest { + return new DeletionRequest({ + id: entity.id, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + source: entity.source, + deleteAfter: entity.deleteAfter, + userId: entity.userId?.toHexString(), + }); + } + + static mapToEntity(domainObject: DeletionRequest): DeletionRequestEntity { + return new DeletionRequestEntity({ + id: domainObject.id, + source: domainObject.source, + deleteAfter: domainObject.deleteAfter, + userId: new ObjectId(domainObject.userId), + }); + } + + // 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/services/deletion-request.service.ts b/apps/server/src/modules/deletion/services/deletion-request.service.ts new file mode 100644 index 00000000000..1f59221ecbe --- /dev/null +++ b/apps/server/src/modules/deletion/services/deletion-request.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@nestjs/common'; +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'; + +@Injectable() +export class DeletionRequestService { + constructor(private readonly deletionRequestRepo: DeletionRequestRepo) {} + + async createDeletionRequest(userId: EntityId): Promise { + const newDeletionRequest = new DeletionRequest({ + id: new ObjectId().toHexString(), + source: 'shd', + deleteAfter: new Date(), + userId, + }); + + await this.deletionRequestRepo.create(newDeletionRequest); + } + + // updateService + async updateDeletionRequest(deletionRequest: DeletionRequest): Promise { + await this.deletionRequestRepo.update(deletionRequest); + } + + // findAll + async findAllItemsByDeletionDate(): Promise { + const itemsToDelete: DeletionRequest[] = await this.deletionRequestRepo.findAllItemsByDeletionDate(); + return itemsToDelete; + } +} diff --git a/apps/server/src/modules/deletion/uc/deletion-worker.us.ts b/apps/server/src/modules/deletion/uc/deletion-worker.us.ts new file mode 100644 index 00000000000..9dbb19b41aa --- /dev/null +++ b/apps/server/src/modules/deletion/uc/deletion-worker.us.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@nestjs/common'; +import { AuthorizationService } from '@src/modules/authorization'; +import { DeletionRequestService } from '../services/deletion-request.service'; +import { DeletionRequest } from '../domain/deletion-request.do'; + +@Injectable() +export class DeletionWorkerUc { + constructor( + private readonly deletionRequestService: DeletionRequestService, + private readonly authorizationService: AuthorizationService + ) {} + + async findAllItemsByDeletionDate(): Promise { + return this.deletionRequestService.findAllItemsByDeletionDate(); + } +} From 5838b81e9a45ea4191f865000cba03385d228fca Mon Sep 17 00:00:00 2001 From: WojciechGrancow Date: Fri, 20 Oct 2023 22:31:08 +0200 Subject: [PATCH 02/72] add some tests --- .../src/modules/deletion/entities/index.ts | 1 - .../deletion-log.entity.ts | 0 .../deletion-request.entity.ts | 0 .../src/modules/deletion/entity/index.ts | 2 ++ .../testing/deletion-request.entity.spec.ts | 19 +++++++++++++++++++ .../deletion-request.entity.factory.ts | 14 ++++++++++++++ .../deletion/repo/deletion-log.repo.ts | 2 +- .../deletion/repo/deletion-request.repo.ts | 2 +- .../server/src/modules/deletion/repo/index.ts | 3 ++- .../repo/mapper/deletion-log.mapper.ts | 2 +- .../repo/mapper/deletion-request.mapper.ts | 2 +- 11 files changed, 41 insertions(+), 6 deletions(-) delete mode 100644 apps/server/src/modules/deletion/entities/index.ts rename apps/server/src/modules/deletion/{entities => entity}/deletion-log.entity.ts (100%) rename apps/server/src/modules/deletion/{entities => entity}/deletion-request.entity.ts (100%) create mode 100644 apps/server/src/modules/deletion/entity/index.ts create mode 100644 apps/server/src/modules/deletion/entity/testing/deletion-request.entity.spec.ts create mode 100644 apps/server/src/modules/deletion/entity/testing/factory/deletion-request.entity.factory.ts diff --git a/apps/server/src/modules/deletion/entities/index.ts b/apps/server/src/modules/deletion/entities/index.ts deleted file mode 100644 index 221c543f2a2..00000000000 --- a/apps/server/src/modules/deletion/entities/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './deletion-request.entity'; \ No newline at end of file diff --git a/apps/server/src/modules/deletion/entities/deletion-log.entity.ts b/apps/server/src/modules/deletion/entity/deletion-log.entity.ts similarity index 100% rename from apps/server/src/modules/deletion/entities/deletion-log.entity.ts rename to apps/server/src/modules/deletion/entity/deletion-log.entity.ts diff --git a/apps/server/src/modules/deletion/entities/deletion-request.entity.ts b/apps/server/src/modules/deletion/entity/deletion-request.entity.ts similarity index 100% rename from apps/server/src/modules/deletion/entities/deletion-request.entity.ts rename to apps/server/src/modules/deletion/entity/deletion-request.entity.ts diff --git a/apps/server/src/modules/deletion/entity/index.ts b/apps/server/src/modules/deletion/entity/index.ts new file mode 100644 index 00000000000..7e3e31dcd19 --- /dev/null +++ b/apps/server/src/modules/deletion/entity/index.ts @@ -0,0 +1,2 @@ +export * from './deletion-request.entity'; +export * from './deletion-log.entity'; 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 new file mode 100644 index 00000000000..ef279ee6a08 --- /dev/null +++ b/apps/server/src/modules/deletion/entity/testing/deletion-request.entity.spec.ts @@ -0,0 +1,19 @@ +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-request.entity.factory.ts b/apps/server/src/modules/deletion/entity/testing/factory/deletion-request.entity.factory.ts new file mode 100644 index 00000000000..82cc7290c54 --- /dev/null +++ b/apps/server/src/modules/deletion/entity/testing/factory/deletion-request.entity.factory.ts @@ -0,0 +1,14 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BaseFactory } from '@shared/testing'; +import { DeletionRequestEntity, DeletionRequestEntityProps } from '../..'; + +export const deletionRequestEntityFactory = BaseFactory.define( + DeletionRequestEntity, + () => { + return { + source: 'shd', + deleteAfter: new Date(), + userId: new ObjectId(), + }; + } +); 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 5d412f5ee67..0c0ace43c6d 100644 --- a/apps/server/src/modules/deletion/repo/deletion-log.repo.ts +++ b/apps/server/src/modules/deletion/repo/deletion-log.repo.ts @@ -2,7 +2,7 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain'; import { DeletionLog } from '../domain/deletion-log.do'; -import { DeletionLogEntity } from '../entities/deletion-log.entity'; +import { DeletionLogEntity } from '../entity/deletion-log.entity'; import { DeletionLogMapper } from './mapper/deletion-log.mapper'; @Injectable() 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 fdd9729b1d2..b386596733c 100644 --- a/apps/server/src/modules/deletion/repo/deletion-request.repo.ts +++ b/apps/server/src/modules/deletion/repo/deletion-request.repo.ts @@ -2,7 +2,7 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain'; import { DeletionRequest } from '../domain/deletion-request.do'; -import { DeletionRequestEntity } from '../entities'; +import { DeletionRequestEntity } from '../entity'; import { DeletionRequestMapper } from './mapper/deletion-request.mapper'; @Injectable() diff --git a/apps/server/src/modules/deletion/repo/index.ts b/apps/server/src/modules/deletion/repo/index.ts index d93472feca7..68860c00a79 100644 --- a/apps/server/src/modules/deletion/repo/index.ts +++ b/apps/server/src/modules/deletion/repo/index.ts @@ -1 +1,2 @@ -export * from './classes.repo'; +export * from './deletion-log.repo'; +export * from './deletion-request.repo'; 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 681497d9e43..9c3656e4d34 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 @@ -1,5 +1,5 @@ import { ObjectId } from '@mikro-orm/mongodb'; -import { DeletionLogEntity } from '../../entities/deletion-log.entity'; +import { DeletionLogEntity } from '../../entity/deletion-log.entity'; import { DeletionLog } from '../../domain/deletion-log.do'; export class DeletionLogMapper { 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 6262ff7a6df..879352117b6 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 @@ -1,6 +1,6 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { DeletionRequest } from '../../domain/deletion-request.do'; -import { DeletionRequestEntity } from '../../entities'; +import { DeletionRequestEntity } from '../../entity'; export class DeletionRequestMapper { static mapToDO(entity: DeletionRequestEntity): DeletionRequest { From 7df7c23f85f0752d8ef6fad4d52becb031f855e2 Mon Sep 17 00:00:00 2001 From: WojciechGrancow Date: Fri, 27 Oct 2023 08:51:31 +0200 Subject: [PATCH 03/72] add test cases and services --- apps/server/src/apps/deletion.app.ts | 49 ---- apps/server/src/config/database.config.ts | 3 +- .../src/modules/deletion/deletion.module.ts | 57 ++--- .../deletion/domain/deletion-log.do.spec.ts | 64 +++++ .../deletion/domain/deletion-log.do.ts | 27 ++- .../domain/deletion-request.do.spec.ts | 62 +++++ .../deletion/domain/deletion-request.do.ts | 19 +- .../testing/factory/deletion-log.factory.ts | 18 ++ .../factory/deletion-request.factory.ts | 28 +++ .../types/deletion-domain-model.enum.ts | 7 + .../types/deletion-operation-model.enum.ts | 4 + .../types/deletion-status-model.enum.ts | 4 + .../entity/deletion-log.entity.spec.ts | 60 +++++ .../deletion/entity/deletion-log.entity.ts | 47 ++-- .../entity/deletion-request.entity.spec.ts | 59 +++++ .../entity/deletion-request.entity.ts | 51 +++- .../testing/deletion-request.entity.spec.ts | 19 -- .../factory/deletion-log.entity.factory.ts | 21 ++ .../deletion-request.entity.factory.ts | 12 +- .../deletion/repo/deletion-log.repo.spec.ts | 186 +++++++++++++++ .../deletion/repo/deletion-log.repo.ts | 30 ++- .../repo/deletion-request.repo.spec.ts | 218 ++++++++++++++++++ .../deletion/repo/deletion-request.repo.ts | 40 +++- .../repo/mapper/deletion-log.mapper.spec.ts | 139 +++++++++++ .../repo/mapper/deletion-log.mapper.ts | 22 +- .../mapper/deletion-request.mapper.spec.ts | 60 +++++ .../repo/mapper/deletion-request.mapper.ts | 20 +- .../src/modules/deletion/repo/mapper/index.ts | 2 + .../deletion/services/deletion-log.service.ts | 40 ++++ .../services/deletion-request.service.spec.ts | 133 +++++++++++ .../services/deletion-request.service.ts | 33 ++- .../src/modules/deletion/services/index.ts | 1 + 32 files changed, 1340 insertions(+), 195 deletions(-) delete mode 100644 apps/server/src/apps/deletion.app.ts create mode 100644 apps/server/src/modules/deletion/domain/deletion-log.do.spec.ts create mode 100644 apps/server/src/modules/deletion/domain/deletion-request.do.spec.ts create mode 100644 apps/server/src/modules/deletion/domain/testing/factory/deletion-log.factory.ts create mode 100644 apps/server/src/modules/deletion/domain/testing/factory/deletion-request.factory.ts create mode 100644 apps/server/src/modules/deletion/domain/types/deletion-domain-model.enum.ts create mode 100644 apps/server/src/modules/deletion/domain/types/deletion-operation-model.enum.ts create mode 100644 apps/server/src/modules/deletion/domain/types/deletion-status-model.enum.ts create mode 100644 apps/server/src/modules/deletion/entity/deletion-log.entity.spec.ts create mode 100644 apps/server/src/modules/deletion/entity/deletion-request.entity.spec.ts delete mode 100644 apps/server/src/modules/deletion/entity/testing/deletion-request.entity.spec.ts create mode 100644 apps/server/src/modules/deletion/entity/testing/factory/deletion-log.entity.factory.ts create mode 100644 apps/server/src/modules/deletion/repo/deletion-log.repo.spec.ts create mode 100644 apps/server/src/modules/deletion/repo/deletion-request.repo.spec.ts create mode 100644 apps/server/src/modules/deletion/repo/mapper/deletion-log.mapper.spec.ts create mode 100644 apps/server/src/modules/deletion/repo/mapper/deletion-request.mapper.spec.ts create mode 100644 apps/server/src/modules/deletion/repo/mapper/index.ts create mode 100644 apps/server/src/modules/deletion/services/deletion-log.service.ts create mode 100644 apps/server/src/modules/deletion/services/deletion-request.service.spec.ts create mode 100644 apps/server/src/modules/deletion/services/index.ts 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'; From 1b07d3f2ca6d2e1039fe3b9f13d481fb59634a2a Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Mon, 30 Oct 2023 23:57:15 +0100 Subject: [PATCH 04/72] add new (almost empty for now) batch deletion app --- apps/server/src/apps/batch-deletion.app.ts | 18 ++++++++++++++++++ .../batch-deletion-app-startup.loggable.ts | 18 ++++++++++++++++++ .../batch-deletion/batch-deletion-config.ts | 8 ++++++++ .../batch-deletion/batch-deletion.module.ts | 11 +++++++++++ .../server/src/modules/batch-deletion/index.ts | 0 config/default.schema.json | 5 +++++ nest-cli.json | 9 +++++++++ package.json | 4 ++++ 8 files changed, 73 insertions(+) create mode 100644 apps/server/src/apps/batch-deletion.app.ts create mode 100644 apps/server/src/apps/loggables/batch-deletion-app-startup.loggable.ts create mode 100644 apps/server/src/modules/batch-deletion/batch-deletion-config.ts create mode 100644 apps/server/src/modules/batch-deletion/batch-deletion.module.ts create mode 100644 apps/server/src/modules/batch-deletion/index.ts diff --git a/apps/server/src/apps/batch-deletion.app.ts b/apps/server/src/apps/batch-deletion.app.ts new file mode 100644 index 00000000000..2255db82228 --- /dev/null +++ b/apps/server/src/apps/batch-deletion.app.ts @@ -0,0 +1,18 @@ +import { NestFactory } from '@nestjs/core'; +import { ConfigService } from '@nestjs/config'; +import { Logger } from '@src/core/logger'; +import { BatchDeletionModule } from '@modules/batch-deletion/batch-deletion.module'; +import { BatchDeletionAppStartupLoggable } from './loggables/batch-deletion-app-startup.loggable'; + +async function bootstrap() { + const app = await NestFactory.createApplicationContext(BatchDeletionModule); + + const logger = await app.resolve(Logger); + const configService = app.get(ConfigService); + + const inputFilePath = configService.get('DELETION_INPUT_FILE_PATH') as string; + + logger.info(new BatchDeletionAppStartupLoggable({ inputFilePath })); +} + +void bootstrap(); diff --git a/apps/server/src/apps/loggables/batch-deletion-app-startup.loggable.ts b/apps/server/src/apps/loggables/batch-deletion-app-startup.loggable.ts new file mode 100644 index 00000000000..6d09f6814b3 --- /dev/null +++ b/apps/server/src/apps/loggables/batch-deletion-app-startup.loggable.ts @@ -0,0 +1,18 @@ +import { Loggable, LogMessage } from '@src/core/logger'; + +interface BatchDeletionAppStartupInfo { + inputFilePath: string; +} + +export class BatchDeletionAppStartupLoggable implements Loggable { + constructor(private readonly info: BatchDeletionAppStartupInfo) {} + + getLogMessage(): LogMessage { + return { + message: 'Successfully started batch deletion app...', + data: { + inputFilePath: this.info.inputFilePath, + }, + }; + } +} diff --git a/apps/server/src/modules/batch-deletion/batch-deletion-config.ts b/apps/server/src/modules/batch-deletion/batch-deletion-config.ts new file mode 100644 index 00000000000..9e63880fb29 --- /dev/null +++ b/apps/server/src/modules/batch-deletion/batch-deletion-config.ts @@ -0,0 +1,8 @@ +import { Configuration } from '@hpi-schul-cloud/commons'; + +const batchDeletionConfig = { + NEST_LOG_LEVEL: Configuration.get('NEST_LOG_LEVEL') as string, + DELETION_INPUT_FILE_PATH: Configuration.get('DELETION_INPUT_FILE_PATH') as string, +}; + +export const config = () => batchDeletionConfig; diff --git a/apps/server/src/modules/batch-deletion/batch-deletion.module.ts b/apps/server/src/modules/batch-deletion/batch-deletion.module.ts new file mode 100644 index 00000000000..49b4bbbc235 --- /dev/null +++ b/apps/server/src/modules/batch-deletion/batch-deletion.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { LoggerModule } from '@src/core/logger'; +import { createConfigModuleOptions } from '@src/config'; +import { config } from './batch-deletion-config'; + +@Module({ + imports: [LoggerModule, ConfigModule.forRoot(createConfigModuleOptions(config))], + providers: [], +}) +export class BatchDeletionModule {} diff --git a/apps/server/src/modules/batch-deletion/index.ts b/apps/server/src/modules/batch-deletion/index.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/config/default.schema.json b/config/default.schema.json index a34d8e899ad..355c33f3704 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -1342,6 +1342,11 @@ "description": "Number of simultaneously synchronized students, teachers and classes" } } + }, + "DELETION_INPUT_FILE_PATH": { + "type": "string", + "default": "/data/data-to-delete.csv", + "description": "Path of an input file that contains all the references to the data that should be deleted." } }, "required": [], diff --git a/nest-cli.json b/nest-cli.json index 73dea03c093..c8cd9726936 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -89,6 +89,15 @@ "compilerOptions": { "tsConfigPath": "apps/server/tsconfig.app.json" } + }, + "batch-deletion": { + "type": "application", + "root": "apps/server", + "entryFile": "apps/batch-deletion.app", + "sourceRoot": "apps/server/src", + "compilerOptions": { + "tsConfigPath": "apps/server/tsconfig.app.json" + } } } } diff --git a/package.json b/package.json index 45e150f6668..abef31027a9 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,10 @@ "nest:start:console": "nest start console --", "nest:start:console:dev": "nest start console --watch --", "nest:start:console:debug": "nest start console --debug --watch --", + "nest:start:batch-deletion": "nest start batch-deletion", + "nest:start:batch-deletion:dev": "nest start batch-deletion --watch", + "nest:start:batch-deletion:debug": "nest start batch-deletion --watch --debug", + "nest:start:batch-deletion:prod": "node dist/apps/server/apps/batch-deletion.app", "nest:test": "npm run nest:test:cov && npm run nest:lint", "nest:test:all": "jest", "nest:test:unit": "jest \"^((?!\\.api\\.spec\\.ts).)*\\.spec\\.ts$\"", From dec5292bfdcf769b295ba83a3c5b05ec8f5829b0 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Tue, 31 Oct 2023 16:28:00 +0100 Subject: [PATCH 05/72] refactor config vars --- apps/server/src/apps/batch-deletion.app.ts | 6 +++-- .../batch-deletion-app-startup.loggable.ts | 8 ++++-- .../batch-deletion/batch-deletion-config.ts | 4 ++- config/default.json | 5 ++++ config/default.schema.json | 25 ++++++++++++++++--- 5 files changed, 39 insertions(+), 9 deletions(-) diff --git a/apps/server/src/apps/batch-deletion.app.ts b/apps/server/src/apps/batch-deletion.app.ts index 2255db82228..4ac70f8e8e5 100644 --- a/apps/server/src/apps/batch-deletion.app.ts +++ b/apps/server/src/apps/batch-deletion.app.ts @@ -10,9 +10,11 @@ async function bootstrap() { const logger = await app.resolve(Logger); const configService = app.get(ConfigService); - const inputFilePath = configService.get('DELETION_INPUT_FILE_PATH') as string; + const targetRefDomain = configService.get('TARGET_REF_DOMAIN') as string; + const targetRefsFilePath = configService.get('TARGET_REFS_FILE_PATH') as string; + const deleteInMinutes = configService.get('DELETE_IN_MINUTES') as number; - logger.info(new BatchDeletionAppStartupLoggable({ inputFilePath })); + logger.info(new BatchDeletionAppStartupLoggable({ targetRefDomain, targetRefsFilePath, deleteInMinutes })); } void bootstrap(); diff --git a/apps/server/src/apps/loggables/batch-deletion-app-startup.loggable.ts b/apps/server/src/apps/loggables/batch-deletion-app-startup.loggable.ts index 6d09f6814b3..b16ab3678a9 100644 --- a/apps/server/src/apps/loggables/batch-deletion-app-startup.loggable.ts +++ b/apps/server/src/apps/loggables/batch-deletion-app-startup.loggable.ts @@ -1,7 +1,9 @@ import { Loggable, LogMessage } from '@src/core/logger'; interface BatchDeletionAppStartupInfo { - inputFilePath: string; + targetRefDomain: string; + targetRefsFilePath: string; + deleteInMinutes: number; } export class BatchDeletionAppStartupLoggable implements Loggable { @@ -11,7 +13,9 @@ export class BatchDeletionAppStartupLoggable implements Loggable { return { message: 'Successfully started batch deletion app...', data: { - inputFilePath: this.info.inputFilePath, + targetRefDomain: this.info.targetRefDomain, + targetRefsFilePath: this.info.targetRefsFilePath, + deleteInMinutes: this.info.deleteInMinutes, }, }; } diff --git a/apps/server/src/modules/batch-deletion/batch-deletion-config.ts b/apps/server/src/modules/batch-deletion/batch-deletion-config.ts index 9e63880fb29..4b6029f06b3 100644 --- a/apps/server/src/modules/batch-deletion/batch-deletion-config.ts +++ b/apps/server/src/modules/batch-deletion/batch-deletion-config.ts @@ -2,7 +2,9 @@ import { Configuration } from '@hpi-schul-cloud/commons'; const batchDeletionConfig = { NEST_LOG_LEVEL: Configuration.get('NEST_LOG_LEVEL') as string, - DELETION_INPUT_FILE_PATH: Configuration.get('DELETION_INPUT_FILE_PATH') as string, + TARGET_REF_DOMAIN: Configuration.get('BATCH_DELETION__TARGET_REF_DOMAIN') as string, + TARGET_REFS_FILE_PATH: Configuration.get('BATCH_DELETION__TARGET_REFS_FILE_PATH') as string, + DELETE_IN_MINUTES: Configuration.get('BATCH_DELETION__DELETE_IN_MINUTES') as number, }; export const config = () => batchDeletionConfig; diff --git a/config/default.json b/config/default.json index 7dd1802a037..bebd2a98be7 100644 --- a/config/default.json +++ b/config/default.json @@ -41,5 +41,10 @@ }, "CTL_TOOLS": { "EXTERNAL_TOOL_MAX_LOGO_SIZE_IN_BYTES": 300000 + }, + "BATCH_DELETION": { + "TARGET_REF_DOMAIN": "user", + "TARGET_REFS_FILE_PATH": "/data/ids-to-delete.txt", + "DELETE_IN_MINUTES": 43200 } } diff --git a/config/default.schema.json b/config/default.schema.json index 355c33f3704..1911b729328 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -1343,10 +1343,27 @@ } } }, - "DELETION_INPUT_FILE_PATH": { - "type": "string", - "default": "/data/data-to-delete.csv", - "description": "Path of an input file that contains all the references to the data that should be deleted." + "BATCH_DELETION": { + "type": "object", + "description": "Configuration for the \"batch deletion\" application.", + "required": [], + "properties": { + "TARGET_REF_DOMAIN": { + "type": "string", + "default": "user", + "description": "Domain of the provided references (ids)." + }, + "TARGET_REFS_FILE_PATH": { + "type": "string", + "default": "/data/ids-to-delete.txt", + "description": "Path to the file containing all the references (ids) that should be deleted." + }, + "DELETE_IN_MINUTES": { + "type": "integer", + "default": 43200, + "description": "The number of minutes after which the data should be deleted." + } + } } }, "required": [], From 29c58a0a607b50dbaed81227b1c0c32a24fcd2e8 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Tue, 31 Oct 2023 17:43:40 +0100 Subject: [PATCH 06/72] add optional env var for specifying delay between the API calls --- .../apps/loggables/batch-deletion-app-startup.loggable.ts | 2 ++ .../src/modules/batch-deletion/batch-deletion-config.ts | 1 + .../src/modules/batch-deletion/batch-deletion.module.ts | 3 ++- config/default.json | 6 +----- config/default.schema.json | 5 +++++ 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/apps/server/src/apps/loggables/batch-deletion-app-startup.loggable.ts b/apps/server/src/apps/loggables/batch-deletion-app-startup.loggable.ts index b16ab3678a9..152998607f9 100644 --- a/apps/server/src/apps/loggables/batch-deletion-app-startup.loggable.ts +++ b/apps/server/src/apps/loggables/batch-deletion-app-startup.loggable.ts @@ -4,6 +4,7 @@ interface BatchDeletionAppStartupInfo { targetRefDomain: string; targetRefsFilePath: string; deleteInMinutes: number; + callsDelayMilliseconds: number; } export class BatchDeletionAppStartupLoggable implements Loggable { @@ -16,6 +17,7 @@ export class BatchDeletionAppStartupLoggable implements Loggable { targetRefDomain: this.info.targetRefDomain, targetRefsFilePath: this.info.targetRefsFilePath, deleteInMinutes: this.info.deleteInMinutes, + callsDelayMilliseconds: this.info.callsDelayMilliseconds, }, }; } diff --git a/apps/server/src/modules/batch-deletion/batch-deletion-config.ts b/apps/server/src/modules/batch-deletion/batch-deletion-config.ts index 4b6029f06b3..8b573558014 100644 --- a/apps/server/src/modules/batch-deletion/batch-deletion-config.ts +++ b/apps/server/src/modules/batch-deletion/batch-deletion-config.ts @@ -5,6 +5,7 @@ const batchDeletionConfig = { TARGET_REF_DOMAIN: Configuration.get('BATCH_DELETION__TARGET_REF_DOMAIN') as string, TARGET_REFS_FILE_PATH: Configuration.get('BATCH_DELETION__TARGET_REFS_FILE_PATH') as string, DELETE_IN_MINUTES: Configuration.get('BATCH_DELETION__DELETE_IN_MINUTES') as number, + CALLS_DELAY_MILLISECONDS: Configuration.get('BATCH_DELETION__CALLS_DELAY_MILLISECONDS') as number, }; export const config = () => batchDeletionConfig; diff --git a/apps/server/src/modules/batch-deletion/batch-deletion.module.ts b/apps/server/src/modules/batch-deletion/batch-deletion.module.ts index 49b4bbbc235..893fb2c9703 100644 --- a/apps/server/src/modules/batch-deletion/batch-deletion.module.ts +++ b/apps/server/src/modules/batch-deletion/batch-deletion.module.ts @@ -1,11 +1,12 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; +import { HttpModule } from '@nestjs/axios'; import { LoggerModule } from '@src/core/logger'; import { createConfigModuleOptions } from '@src/config'; import { config } from './batch-deletion-config'; @Module({ - imports: [LoggerModule, ConfigModule.forRoot(createConfigModuleOptions(config))], + imports: [HttpModule, LoggerModule, ConfigModule.forRoot(createConfigModuleOptions(config))], providers: [], }) export class BatchDeletionModule {} diff --git a/config/default.json b/config/default.json index bebd2a98be7..9278a7b9e57 100644 --- a/config/default.json +++ b/config/default.json @@ -42,9 +42,5 @@ "CTL_TOOLS": { "EXTERNAL_TOOL_MAX_LOGO_SIZE_IN_BYTES": 300000 }, - "BATCH_DELETION": { - "TARGET_REF_DOMAIN": "user", - "TARGET_REFS_FILE_PATH": "/data/ids-to-delete.txt", - "DELETE_IN_MINUTES": 43200 - } + "BATCH_DELETION": {} } diff --git a/config/default.schema.json b/config/default.schema.json index 1911b729328..2f61736b187 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -1362,6 +1362,11 @@ "type": "integer", "default": 43200, "description": "The number of minutes after which the data should be deleted." + }, + "CALLS_DELAY_MILLISECONDS": { + "type": "integer", + "default": 0, + "description": "Delay between the HTTP calls in milliseconds - 0 (or less) indicates no delay." } } } From a853ea748f390f2b646f44d0cf24c55915fc2b41 Mon Sep 17 00:00:00 2001 From: WojciechGrancow Date: Wed, 1 Nov 2023 23:01:22 +0100 Subject: [PATCH 07/72] add usecases and test cases --- .../src/modules/deletion/deletion.module.ts | 30 +- .../deletion/domain/deletion-log.do.spec.ts | 8 +- .../deletion/domain/deletion-log.do.ts | 24 +- .../deletion/domain/deletion-request.do.ts | 16 +- .../testing/factory/deletion-log.factory.ts | 4 +- .../types/deletion-domain-model.enum.ts | 10 +- .../entity/deletion-log.entity.spec.ts | 8 +- .../deletion/entity/deletion-log.entity.ts | 26 +- .../entity/deletion-request.entity.spec.ts | 42 +- .../entity/deletion-request.entity.ts | 53 +-- .../factory/deletion-log.entity.factory.ts | 4 +- apps/server/src/modules/deletion/index.ts | 1 + .../deletion/repo/deletion-log.repo.spec.ts | 16 +- .../repo/deletion-request.repo.spec.ts | 61 +++ .../deletion/repo/deletion-request.repo.ts | 15 +- .../repo/mapper/deletion-log.mapper.spec.ts | 16 +- .../repo/mapper/deletion-log.mapper.ts | 8 +- .../services/deletion-log.service.spec.ts | 110 +++++ .../deletion/services/deletion-log.service.ts | 61 ++- .../services/deletion-request.service.spec.ts | 70 +++- .../services/deletion-request.service.ts | 16 +- .../deletion/uc/deletion-request.uc.spec.ts | 396 ++++++++++++++++++ .../deletion/uc/deletion-request.uc.ts | 219 ++++++++++ .../modules/deletion/uc/deletion-worker.us.ts | 16 - 24 files changed, 1026 insertions(+), 204 deletions(-) create mode 100644 apps/server/src/modules/deletion/services/deletion-log.service.spec.ts create mode 100644 apps/server/src/modules/deletion/uc/deletion-request.uc.spec.ts create mode 100644 apps/server/src/modules/deletion/uc/deletion-request.uc.ts delete mode 100644 apps/server/src/modules/deletion/uc/deletion-worker.us.ts diff --git a/apps/server/src/modules/deletion/deletion.module.ts b/apps/server/src/modules/deletion/deletion.module.ts index 7d508b1650a..a32f57e2db3 100644 --- a/apps/server/src/modules/deletion/deletion.module.ts +++ b/apps/server/src/modules/deletion/deletion.module.ts @@ -1,38 +1,10 @@ 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 { 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.`), -// }; - @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)), - // ], providers: [Logger, DeletionRequestService, DeletionRequestRepo], + exports: [DeletionRequestService], }) 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 index b18e4759af5..860e07f56de 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 @@ -38,8 +38,8 @@ describe(DeletionLog.name, () => { id: new ObjectId().toHexString(), domain: DeletionDomainModel.USER, operation: DeletionOperationModel.DELETE, - modifiedCounter: 0, - deletedCounter: 1, + modifiedCount: 0, + deletedCount: 1, deletionRequestId: new ObjectId().toHexString(), createdAt: new Date(), updatedAt: new Date(), @@ -50,8 +50,8 @@ describe(DeletionLog.name, () => { id: deletionLogDo.id, domain: deletionLogDo.domain, operation: deletionLogDo.operation, - modifiedCounter: deletionLogDo.modifiedCounter, - deletedCounter: deletionLogDo.deletedCounter, + modifiedCount: deletionLogDo.modifiedCount, + deletedCount: deletionLogDo.deletedCount, deletionRequestId: deletionLogDo.deletionRequestId, createdAt: deletionLogDo.createdAt, updatedAt: deletionLogDo.updatedAt, 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 89391ca65a8..73e62b46055 100644 --- a/apps/server/src/modules/deletion/domain/deletion-log.do.ts +++ b/apps/server/src/modules/deletion/domain/deletion-log.do.ts @@ -4,25 +4,25 @@ 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; - domain?: DeletionDomainModel; + createdAt?: Date; + updatedAt?: Date; + domain: DeletionDomainModel; operation?: DeletionOperationModel; - modifiedCounter?: number; - deletedCounter?: number; + modifiedCount?: number; + deletedCount?: number; deletionRequestId?: EntityId; } export class DeletionLog extends DomainObject { - get createdAt(): Date { + get createdAt(): Date | undefined { return this.props.createdAt; } - get updatedAt(): Date { + get updatedAt(): Date | undefined { return this.props.updatedAt; } - get domain(): DeletionDomainModel | undefined { + get domain(): DeletionDomainModel { return this.props.domain; } @@ -30,12 +30,12 @@ export class DeletionLog extends DomainObject { return this.props.operation; } - get modifiedCounter(): number | undefined { - return this.props.modifiedCounter; + get modifiedCount(): number | undefined { + return this.props.modifiedCount; } - get deletedCounter(): number | undefined { - return this.props.deletedCounter; + get deletedCount(): number | undefined { + return this.props.deletedCount; } get deletionRequestId(): EntityId | undefined { 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 881560359bc..6d1349a65f2 100644 --- a/apps/server/src/modules/deletion/domain/deletion-request.do.ts +++ b/apps/server/src/modules/deletion/domain/deletion-request.do.ts @@ -6,10 +6,10 @@ import { DeletionStatusModel } from './types/deletion-status-model.enum'; export interface DeletionRequestProps extends AuthorizableObject { createdAt?: Date; updatedAt?: Date; - domain?: DeletionDomainModel; - deleteAfter?: Date; - itemId?: EntityId; - status?: DeletionStatusModel; + domain: DeletionDomainModel; + deleteAfter: Date; + itemId: EntityId; + status: DeletionStatusModel; } export class DeletionRequest extends DomainObject { @@ -21,19 +21,19 @@ export class DeletionRequest extends DomainObject { return this.props.updatedAt; } - get domain(): DeletionDomainModel | undefined { + get domain(): DeletionDomainModel { return this.props.domain; } - get deleteAfter(): Date | undefined { + get deleteAfter(): Date { return this.props.deleteAfter; } - get itemId(): EntityId | undefined { + get itemId(): EntityId { return this.props.itemId; } - get status(): DeletionStatusModel | undefined { + get status(): DeletionStatusModel { 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 index 25249fa3518..d83b2f44c8a 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 @@ -9,8 +9,8 @@ export const deletionLogFactory = DoBaseFactory.define { id: new ObjectId().toHexString(), domain: DeletionDomainModel.USER, operation: DeletionOperationModel.DELETE, - modifiedCounter: 0, - deletedCounter: 1, + modifiedCount: 0, + deletedCount: 1, deletionRequestId: new ObjectId(), createdAt: new Date(), updatedAt: new Date(), @@ -46,8 +46,8 @@ describe(DeletionLogEntity.name, () => { id: entity.id, domain: entity.domain, operation: entity.operation, - modifiedCounter: entity.modifiedCounter, - deletedCounter: entity.deletedCounter, + modifiedCount: entity.modifiedCount, + deletedCount: entity.deletedCount, deletionRequestId: entity.deletionRequestId, createdAt: entity.createdAt, updatedAt: entity.updatedAt, 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 f2139ca6aaa..8a9d2bab025 100644 --- a/apps/server/src/modules/deletion/entity/deletion-log.entity.ts +++ b/apps/server/src/modules/deletion/entity/deletion-log.entity.ts @@ -6,10 +6,10 @@ import { DeletionOperationModel } from '../domain/types/deletion-operation-model export interface DeletionLogEntityProps { id?: EntityId; - domain?: DeletionDomainModel; + domain: DeletionDomainModel; operation?: DeletionOperationModel; - modifiedCounter?: number; - deletedCounter?: number; + modifiedCount?: number; + deletedCount?: number; deletionRequestId?: ObjectId; createdAt?: Date; updatedAt?: Date; @@ -17,17 +17,17 @@ export interface DeletionLogEntityProps { @Entity({ tableName: 'deletionlogs' }) export class DeletionLogEntity extends BaseEntityWithTimestamps { - @Property({ nullable: true }) - domain?: DeletionDomainModel; + @Property() + domain: DeletionDomainModel; @Property({ nullable: true }) operation?: DeletionOperationModel; @Property({ nullable: true }) - modifiedCounter?: number; + modifiedCount?: number; @Property({ nullable: true }) - deletedCounter?: number; + deletedCount?: number; @Property({ nullable: true }) deletionRequestId?: ObjectId; @@ -38,20 +38,18 @@ export class DeletionLogEntity extends BaseEntityWithTimestamps { this.id = props.id; } - if (props.domain !== undefined) { - this.domain = props.domain; - } + this.domain = props.domain; if (props.operation !== undefined) { this.operation = props.operation; } - if (props.modifiedCounter !== undefined) { - this.modifiedCounter = props.modifiedCounter; + if (props.modifiedCount !== undefined) { + this.modifiedCount = props.modifiedCount; } - if (props.deletedCounter !== undefined) { - this.deletedCounter = props.deletedCounter; + if (props.deletedCount !== undefined) { + this.deletedCount = props.deletedCount; } if (props.deletionRequestId !== undefined) { 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 fca7bb87b57..1552360e2fd 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 @@ -3,28 +3,31 @@ 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'; +// import { deletionRequestEntityFactory } from './testing/factory/deletion-request.entity.factory'; 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(), - }; + const setup = () => { + jest.clearAllMocks(); - return { props }; - }; + 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 }; + }; + + describe('constructor', () => { + describe('When constructor is called', () => { it('should throw an error by empty constructor', () => { // @ts-expect-error: Test case const test = () => new DeletionRequestEntity(); @@ -56,4 +59,15 @@ describe(DeletionRequestEntity.name, () => { }); }); }); + + describe('executed', () => { + it('should update status', () => { + const { props } = setup(); + const entity: DeletionRequestEntity = new DeletionRequestEntity(props); + + entity.executed(); + + expect(entity.status).toEqual(DeletionStatusModel.SUCCESS); + }); + }); }); 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 f76dbedcb8b..6494edca17f 100644 --- a/apps/server/src/modules/deletion/entity/deletion-request.entity.ts +++ b/apps/server/src/modules/deletion/entity/deletion-request.entity.ts @@ -7,32 +7,32 @@ import { DeletionStatusModel } from '../domain/types/deletion-status-model.enum' export interface DeletionRequestEntityProps { id?: EntityId; - domain?: DeletionDomainModel; - deleteAfter?: Date; - itemId?: EntityId; - status?: DeletionStatusModel; + domain: DeletionDomainModel; + deleteAfter: Date; + itemId: EntityId; + status: DeletionStatusModel; createdAt?: Date; updatedAt?: Date; } @Entity({ tableName: 'deletionrequests' }) +@Index({ properties: ['_itemId', 'domain'] }) export class DeletionRequestEntity extends BaseEntityWithTimestamps { - @Property({ nullable: true }) - deleteAfter?: Date; + @Property() + deleteAfter: Date; - @Property({ fieldName: 'itemToDeletion', nullable: true }) - @Index() - _itemId?: ObjectId; + @Property() + _itemId: ObjectId; - get itemId(): EntityId | undefined { - return this._itemId?.toHexString(); + get itemId(): EntityId { + return this._itemId.toHexString(); } - @Property({ nullable: true }) - domain?: DeletionDomainModel; + @Property() + domain: DeletionDomainModel; - @Property({ nullable: true }) - status?: DeletionStatusModel; + @Property() + status: DeletionStatusModel; constructor(props: DeletionRequestEntityProps) { super(); @@ -40,21 +40,10 @@ export class DeletionRequestEntity extends BaseEntityWithTimestamps { this.id = props.id; } - if (props.domain !== undefined) { - this.domain = props.domain; - } - - if (props.deleteAfter !== undefined) { - this.deleteAfter = props.deleteAfter; - } - - if (props.itemId !== undefined) { - this._itemId = new ObjectId(props.itemId); - } - - if (props.status !== undefined) { - this.status = props.status; - } + this.domain = props.domain; + this.deleteAfter = props.deleteAfter; + this._itemId = new ObjectId(props.itemId); + this.status = props.status; if (props.createdAt !== undefined) { this.createdAt = props.createdAt; @@ -64,4 +53,8 @@ export class DeletionRequestEntity extends BaseEntityWithTimestamps { this.updatedAt = props.updatedAt; } } + + public executed(): void { + this.status = DeletionStatusModel.SUCCESS; + } } 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 ed7f975cc0f..a8b40c381db 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 @@ -11,8 +11,8 @@ export const deletionLogEntityFactory = BaseFactory.define { id: domainObject.id, domain: domainObject.domain, operation: domainObject.operation, - modifiedCounter: domainObject.modifiedCounter, - deletedCounter: domainObject.deletedCounter, + modifiedCount: domainObject.modifiedCount, + deletedCount: domainObject.deletedCount, deletionRequestId: domainObject.deletionRequestId, createdAt: domainObject.createdAt, updatedAt: domainObject.updatedAt, @@ -88,8 +88,8 @@ describe(DeletionLogRepo.name, () => { id: entity.id, domain: entity.domain, operation: entity.operation, - modifiedCounter: entity.modifiedCounter, - deletedCounter: entity.deletedCounter, + modifiedCount: entity.modifiedCount, + deletedCount: entity.deletedCount, deletionRequestId: entity.deletionRequestId?.toHexString(), createdAt: entity.createdAt, updatedAt: entity.updatedAt, @@ -153,8 +153,8 @@ describe(DeletionLogRepo.name, () => { domain: deletionLogEntity1.domain, operation: deletionLogEntity1.operation, deletionRequestId: deletionLogEntity1.deletionRequestId?.toHexString(), - modifiedCounter: deletionLogEntity1.modifiedCounter, - deletedCounter: deletionLogEntity1.deletedCounter, + modifiedCount: deletionLogEntity1.modifiedCount, + deletedCount: deletionLogEntity1.deletedCount, createdAt: deletionLogEntity1.createdAt, updatedAt: deletionLogEntity1.updatedAt, }, @@ -163,8 +163,8 @@ describe(DeletionLogRepo.name, () => { domain: deletionLogEntity2.domain, operation: deletionLogEntity2.operation, deletionRequestId: deletionLogEntity2.deletionRequestId?.toHexString(), - modifiedCounter: deletionLogEntity2.modifiedCounter, - deletedCounter: deletionLogEntity2.deletedCounter, + modifiedCount: deletionLogEntity2.modifiedCount, + deletedCount: deletionLogEntity2.deletedCount, createdAt: deletionLogEntity2.createdAt, updatedAt: deletionLogEntity2.updatedAt, }, 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 index d52b43407aa..fd28755a6b1 100644 --- a/apps/server/src/modules/deletion/repo/deletion-request.repo.spec.ts +++ b/apps/server/src/modules/deletion/repo/deletion-request.repo.spec.ts @@ -170,6 +170,67 @@ describe(DeletionRequestRepo.name, () => { }); }); + describe('update', () => { + describe('when updating deletionRequest', () => { + const setup = async () => { + const userId = new ObjectId().toHexString(); + + const entity: DeletionRequestEntity = deletionRequestEntityFactory.build({ itemId: userId }); + await em.persistAndFlush(entity); + + // Arrange expected DeletionRequestEntity after changing status + entity.status = DeletionStatusModel.SUCCESS; + const deletionRequestToUpdate = DeletionRequestMapper.mapToDO(entity); + + return { + entity, + deletionRequestToUpdate, + }; + }; + + it('should update the deletionRequest', async () => { + const { entity, deletionRequestToUpdate } = await setup(); + + await repo.update(deletionRequestToUpdate); + + const result: DeletionRequest = await repo.findById(entity.id); + + expect(result.status).toEqual(entity.status); + }); + }); + }); + + describe('markDeletionRequestAsExecuted', () => { + describe('when mark deletionRequest as executed', () => { + const setup = async () => { + const userId = new ObjectId().toHexString(); + + const entity: DeletionRequestEntity = deletionRequestEntityFactory.build({ itemId: userId }); + await em.persistAndFlush(entity); + + return { entity }; + }; + + it('should update the deletionRequest', async () => { + const { entity } = await setup(); + + const result = await repo.markDeletionRequestAsExecuted(entity.id); + + expect(result).toBe(true); + }); + + it('should update the deletionRequest', async () => { + const { entity } = await setup(); + + await repo.markDeletionRequestAsExecuted(entity.id); + + const result: DeletionRequest = await repo.findById(entity.id); + + expect(result.status).toEqual(DeletionStatusModel.SUCCESS); + }); + }); + }); + describe('deleteById', () => { describe('when deleting deletionRequest exists', () => { const setup = async () => { 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 a2f9a491504..8ab31672f60 100644 --- a/apps/server/src/modules/deletion/repo/deletion-request.repo.ts +++ b/apps/server/src/modules/deletion/repo/deletion-request.repo.ts @@ -42,7 +42,20 @@ export class DeletionRequestRepo { async update(deletionRequest: DeletionRequest): Promise { const deletionRequestEntity = DeletionRequestMapper.mapToEntity(deletionRequest); - await this.em.persistAndFlush(deletionRequestEntity); + const referencedEntity = this.em.getReference(DeletionRequestEntity, deletionRequestEntity.id); + + await this.em.persistAndFlush(referencedEntity); + } + + async markDeletionRequestAsExecuted(deletionRequestId: EntityId): Promise { + const deletionRequest: DeletionRequestEntity = await this.em.findOneOrFail(DeletionRequestEntity, { + id: deletionRequestId, + }); + + deletionRequest.executed(); + await this.em.persistAndFlush(deletionRequest); + + return true; } async deleteById(deletionRequestId: EntityId): Promise { 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 c82acbb2e8e..008c54502c2 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 @@ -18,8 +18,8 @@ describe(DeletionLogMapper.name, () => { domain: entity.domain, operation: entity.operation, deletionRequestId: entity.deletionRequestId?.toHexString(), - modifiedCounter: entity.modifiedCounter, - deletedCounter: entity.deletedCounter, + modifiedCount: entity.modifiedCount, + deletedCount: entity.deletedCount, createdAt: entity.createdAt, updatedAt: entity.updatedAt, }); @@ -51,8 +51,8 @@ describe(DeletionLogMapper.name, () => { domain: entity.domain, operation: entity.operation, deletionRequestId: entity.deletionRequestId?.toHexString(), - modifiedCounter: entity.modifiedCounter, - deletedCounter: entity.deletedCounter, + modifiedCount: entity.modifiedCount, + deletedCount: entity.deletedCount, createdAt: entity.createdAt, updatedAt: entity.updatedAt, }) @@ -84,8 +84,8 @@ describe(DeletionLogMapper.name, () => { domain: domainObject.domain, operation: domainObject.operation, deletionRequestId: new ObjectId(domainObject.deletionRequestId), - modifiedCounter: domainObject.modifiedCounter, - deletedCounter: domainObject.deletedCounter, + modifiedCount: domainObject.modifiedCount, + deletedCount: domainObject.deletedCount, createdAt: domainObject.createdAt, updatedAt: domainObject.updatedAt, }); @@ -126,8 +126,8 @@ describe(DeletionLogMapper.name, () => { domain: domainObject.domain, operation: domainObject.operation, deletionRequestId: new ObjectId(domainObject.deletionRequestId), - modifiedCounter: domainObject.modifiedCounter, - deletedCounter: domainObject.deletedCounter, + modifiedCount: domainObject.modifiedCount, + deletedCount: domainObject.deletedCount, 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 506e7369c84..820cd9d87c0 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, - modifiedCounter: entity.modifiedCounter, - deletedCounter: entity.deletedCounter, + modifiedCount: entity.modifiedCount, + deletedCount: entity.deletedCount, deletionRequestId: entity.deletionRequestId?.toHexString(), }); } @@ -23,8 +23,8 @@ export class DeletionLogMapper { updatedAt: domainObject.updatedAt, domain: domainObject.domain, operation: domainObject.operation, - modifiedCounter: domainObject.modifiedCounter, - deletedCounter: domainObject.deletedCounter, + modifiedCount: domainObject.modifiedCount, + deletedCount: domainObject.deletedCount, deletionRequestId: new ObjectId(domainObject.deletionRequestId), }); } 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 new file mode 100644 index 00000000000..21522e5e924 --- /dev/null +++ b/apps/server/src/modules/deletion/services/deletion-log.service.spec.ts @@ -0,0 +1,110 @@ +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 { DeletionLogRepo } from '../repo'; +import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; +import { DeletionLogService } from './deletion-log.service'; +import { DeletionOperationModel } from '../domain/types/deletion-operation-model.enum'; +import { deletionLogFactory } from '../domain/testing/factory/deletion-log.factory'; + +describe(DeletionLogService.name, () => { + let module: TestingModule; + let service: DeletionLogService; + let deletionLogRepo: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + DeletionLogService, + { + provide: DeletionLogRepo, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(DeletionLogService); + deletionLogRepo = module.get(DeletionLogRepo); + + await setupEntities(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('defined', () => { + it('should be defined', () => { + expect(service).toBeDefined(); + }); + }); + + describe('createDeletionRequest', () => { + describe('when creating a deletionRequest', () => { + const setup = () => { + const deletionRequestId = '653e4833cc39e5907a1e18d2'; + const domain = DeletionDomainModel.USER; + const operation = DeletionOperationModel.DELETE; + const modifiedCount = 0; + const deletedCount = 1; + + return { deletionRequestId, domain, operation, modifiedCount, deletedCount }; + }; + + it('should call deletionRequestRepo.create', async () => { + const { deletionRequestId, domain, operation, modifiedCount, deletedCount } = setup(); + + await service.createDeletionLog(deletionRequestId, domain, operation, modifiedCount, deletedCount); + + expect(deletionLogRepo.create).toHaveBeenCalledWith( + expect.objectContaining({ + id: expect.any(String), + deletionRequestId, + domain, + operation, + modifiedCount, + deletedCount, + }) + ); + }); + }); + }); + + describe('findByDeletionRequestId', () => { + describe('when finding all logs for deletionRequestId', () => { + const setup = () => { + const deletionRequestId = new ObjectId().toHexString(); + const deletionLog1 = deletionLogFactory.build({ deletionRequestId }); + const deletionLog2 = deletionLogFactory.build({ + deletionRequestId, + domain: DeletionDomainModel.PSEUDONYMS, + }); + const deletionLogs = [deletionLog1, deletionLog2]; + + deletionLogRepo.findAllByDeletionRequestId.mockResolvedValue(deletionLogs); + + return { deletionRequestId, deletionLogs }; + }; + + it('should call deletionLogRepo.findAllByDeletionRequestId', async () => { + const { deletionRequestId } = setup(); + await service.findByDeletionRequestId(deletionRequestId); + + expect(deletionLogRepo.findAllByDeletionRequestId).toBeCalledWith(deletionRequestId); + }); + + it('should return array of two deletionLogs with deletionRequestId', async () => { + const { deletionRequestId, deletionLogs } = setup(); + const result = await service.findByDeletionRequestId(deletionRequestId); + + expect(result).toHaveLength(2); + expect(result).toEqual(deletionLogs); + }); + }); + }); +}); 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 70290bc0c25..d37f9b9c4a9 100644 --- a/apps/server/src/modules/deletion/services/deletion-log.service.ts +++ b/apps/server/src/modules/deletion/services/deletion-log.service.ts @@ -1,40 +1,37 @@ import { Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain'; +import { ObjectId } from '@mikro-orm/mongodb'; import { DeletionLogRepo } from '../repo'; +import { DeletionLog } from '../domain/deletion-log.do'; +import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; +import { DeletionOperationModel } from '../domain/types/deletion-operation-model.enum'; @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); - // } + async createDeletionLog( + deletionRequestId: EntityId, + domain: DeletionDomainModel, + operation: DeletionOperationModel, + modifiedCounter: number, + deletedCounter: number + ): Promise { + const newDeletionLog = new DeletionLog({ + id: new ObjectId().toHexString(), + domain, + deletionRequestId, + operation, + modifiedCount: modifiedCounter, + deletedCount: deletedCounter, + }); + + await this.deletionLogRepo.create(newDeletionLog); + } + + async findByDeletionRequestId(deletionRequestId: EntityId): Promise { + const deletionLogs: DeletionLog[] = await this.deletionLogRepo.findAllByDeletionRequestId(deletionRequestId); + + return deletionLogs; + } } 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 4013f3adcad..e0396d6e81b 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 @@ -5,6 +5,8 @@ import { setupEntities } from '@shared/testing'; import { DeletionRequestService } from './deletion-request.service'; import { DeletionRequestRepo } from '../repo'; import { deletionRequestFactory } from '../domain/testing/factory/deletion-request.factory'; +import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; +import { DeletionStatusModel } from '../domain/types/deletion-status-model.enum'; describe(DeletionRequestService.name, () => { let module: TestingModule; @@ -42,18 +44,32 @@ describe(DeletionRequestService.name, () => { }); }); - // TODO createDeletionRequest - // describe('createDeletionRequest', () => { - // describe('when creating a deletionRequest', () => { - // const setup = () => { - // const deletionRequest = deletionRequestFactory.build(); + describe('createDeletionRequest', () => { + describe('when creating a deletionRequest', () => { + const setup = () => { + const itemId = '653e4833cc39e5907a1e18d2'; + const domain = DeletionDomainModel.USER; + + return { itemId, domain }; + }; + + it('should call deletionRequestRepo.create', async () => { + const { itemId, domain } = setup(); - // return { deletionRequest }; - // }; + await service.createDeletionRequest(itemId, domain); - // it('should '); - // }); - // }); + expect(deletionRequestRepo.create).toHaveBeenCalledWith( + expect.objectContaining({ + id: expect.any(String), + domain, + deleteAfter: expect.any(Date), + itemId, + status: DeletionStatusModel.REGISTERED, + }) + ); + }); + }); + }); describe('findById', () => { describe('when finding by deletionRequestId', () => { @@ -114,6 +130,40 @@ describe(DeletionRequestService.name, () => { }); }); + describe('update', () => { + describe('when updating deletionRequest', () => { + const setup = () => { + const deletionRequest = deletionRequestFactory.buildWithId(); + + return { deletionRequest }; + }; + + it('should call deletionRequestRepo.update', async () => { + const { deletionRequest } = setup(); + await service.update(deletionRequest); + + expect(deletionRequestRepo.update).toBeCalledWith(deletionRequest); + }); + }); + }); + + describe('markDeletionRequestAsExecuted', () => { + describe('when mark deletionRequest as executed', () => { + const setup = () => { + const deletionRequestId = new ObjectId().toHexString(); + + return { deletionRequestId }; + }; + + it('should call deletionRequestRepo.markDeletionRequestAsExecuted', async () => { + const { deletionRequestId } = setup(); + await service.markDeletionRequestAsExecuted(deletionRequestId); + + expect(deletionRequestRepo.markDeletionRequestAsExecuted).toBeCalledWith(deletionRequestId); + }); + }); + }); + describe('deleteById', () => { describe('when deleting deletionRequest', () => { const setup = () => { 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 38889fe37d2..570661d4f92 100644 --- a/apps/server/src/modules/deletion/services/deletion-request.service.ts +++ b/apps/server/src/modules/deletion/services/deletion-request.service.ts @@ -4,6 +4,7 @@ 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'; +import { DeletionStatusModel } from '../domain/types/deletion-status-model.enum'; @Injectable() export class DeletionRequestService { @@ -13,22 +14,23 @@ export class DeletionRequestService { itemId: EntityId, domain: DeletionDomainModel, deleteInMinutes?: number - ): Promise { + ): Promise<{ requestId: EntityId; deletionPlannedAt: Date }> { deleteInMinutes = deleteInMinutes === undefined ? 43200 : deleteInMinutes; const dateOfDeletion = new Date(); - dateOfDeletion.setDate(dateOfDeletion.getDate() + deleteInMinutes * 1000); + dateOfDeletion.setMinutes(dateOfDeletion.getMinutes() + deleteInMinutes); const newDeletionRequest = new DeletionRequest({ id: new ObjectId().toHexString(), domain, deleteAfter: dateOfDeletion, itemId, + status: DeletionStatusModel.REGISTERED, }); await this.deletionRequestRepo.create(newDeletionRequest); - return newDeletionRequest.id; + return { requestId: newDeletionRequest.id, deletionPlannedAt: newDeletionRequest.deleteAfter }; } async findById(deletionRequestId: EntityId): Promise { @@ -43,6 +45,14 @@ export class DeletionRequestService { return itemsToDelete; } + async update(deletionRequestToUpdate: DeletionRequest): Promise { + await this.deletionRequestRepo.update(deletionRequestToUpdate); + } + + async markDeletionRequestAsExecuted(deletionRequestId: EntityId): Promise { + return this.deletionRequestRepo.markDeletionRequestAsExecuted(deletionRequestId); + } + async deleteById(deletionRequestId: EntityId): Promise { await this.deletionRequestRepo.deleteById(deletionRequestId); } 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 new file mode 100644 index 00000000000..6525e71e97f --- /dev/null +++ b/apps/server/src/modules/deletion/uc/deletion-request.uc.spec.ts @@ -0,0 +1,396 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { setupEntities } from '@shared/testing'; +import { AccountService } from '@src/modules/account/services/account.service'; +import { ClassService } from '@src/modules/class'; +import { CourseGroupService } from '@src/modules/learnroom/service/coursegroup.service'; +import { CourseService } from '@src/modules/learnroom/service'; +import { FilesService } from '@src/modules/files/service'; +import { LessonService } from '@src/modules/lesson/service'; +import { PseudonymService } from '@src/modules/pseudonym'; +import { TeamService } from '@src/modules/teams'; +import { UserService } from '@src/modules'; +import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; +import { DeletionLogService } from '../services/deletion-log.service'; +import { DeletionRequestService } from '../services'; +import { DeletionRequestLog, DeletionRequestProps, DeletionRequestUc } from './deletion-request.uc'; +import { deletionRequestFactory } from '../domain/testing/factory/deletion-request.factory'; +import { DeletionStatusModel } from '../domain/types/deletion-status-model.enum'; +import { deletionLogFactory } from '../domain/testing/factory/deletion-log.factory'; + +describe(DeletionRequestUc.name, () => { + let module: TestingModule; + let uc: DeletionRequestUc; + let deletionRequestService: DeepMocked; + let deletionLogService: DeepMocked; + let accountService: DeepMocked; + let classService: DeepMocked; + let courseGroupService: DeepMocked; + let courseService: DeepMocked; + let filesService: DeepMocked; + let lessonService: DeepMocked; + let pseudonymService: DeepMocked; + let teamService: DeepMocked; + let userService: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + DeletionRequestUc, + { + provide: DeletionRequestService, + useValue: createMock(), + }, + { + provide: DeletionLogService, + useValue: createMock(), + }, + { + provide: AccountService, + useValue: createMock(), + }, + { + provide: ClassService, + useValue: createMock(), + }, + { + provide: CourseGroupService, + useValue: createMock(), + }, + { + provide: CourseService, + useValue: createMock(), + }, + { + provide: FilesService, + useValue: createMock(), + }, + { + provide: LessonService, + useValue: createMock(), + }, + { + provide: PseudonymService, + useValue: createMock(), + }, + { + provide: TeamService, + useValue: createMock(), + }, + { + provide: UserService, + useValue: createMock(), + }, + ], + }).compile(); + + uc = module.get(DeletionRequestUc); + deletionRequestService = module.get(DeletionRequestService); + deletionLogService = module.get(DeletionLogService); + accountService = module.get(AccountService); + classService = module.get(ClassService); + courseGroupService = module.get(CourseGroupService); + courseService = module.get(CourseService); + filesService = module.get(FilesService); + lessonService = module.get(LessonService); + pseudonymService = module.get(PseudonymService); + teamService = module.get(TeamService); + userService = module.get(UserService); + await setupEntities(); + }); + + const setup = () => { + jest.clearAllMocks(); + const deletionRequestToCreate: DeletionRequestProps = { + targetRef: { + domain: DeletionDomainModel.USER, + itemId: '653e4833cc39e5907a1e18d2', + }, + deleteInMinutes: 1440, + }; + + const deletionRequest = deletionRequestFactory.build(); + const deletionRequestToExecute = deletionRequestFactory.build({ deleteAfter: new Date('2023-01-01') }); + const deletionRequestExecuted = deletionRequestFactory.build({ status: DeletionStatusModel.SUCCESS }); + const deletionLogExecuted1 = deletionLogFactory.build({ deletionRequestId: deletionRequestExecuted.id }); + const deletionLogExecuted2 = deletionLogFactory.build({ + deletionRequestId: deletionRequestExecuted.id, + domain: DeletionDomainModel.ACCOUNT, + modifiedCount: 0, + deletedCount: 1, + }); + + const executedDeletionRequestSummary: DeletionRequestLog = { + targetRef: { + domain: deletionRequestExecuted.domain, + itemId: deletionRequestExecuted.itemId, + }, + deletionPlannedAt: deletionRequestExecuted.deleteAfter, + statistics: [ + { + domain: deletionLogExecuted1.domain, + modifiedCount: deletionLogExecuted1.modifiedCount, + deletedCount: deletionLogExecuted1.deletedCount, + }, + { + domain: deletionLogExecuted2.domain, + modifiedCount: deletionLogExecuted2.modifiedCount, + deletedCount: deletionLogExecuted2.deletedCount, + }, + ], + }; + + const notExecutedDeletionRequestSummary: DeletionRequestLog = { + targetRef: { + domain: deletionRequest.domain, + itemId: deletionRequest.itemId, + }, + deletionPlannedAt: deletionRequest.deleteAfter, + }; + + classService.deleteUserDataFromClasses.mockResolvedValueOnce(1); + courseGroupService.deleteUserDataFromCourseGroup.mockResolvedValueOnce(2); + courseService.deleteUserDataFromCourse.mockResolvedValueOnce(2); + filesService.markFilesOwnedByUserForDeletion.mockResolvedValueOnce(2); + filesService.removeUserPermissionsToAnyFiles.mockResolvedValueOnce(2); + lessonService.deleteUserDataFromLessons.mockResolvedValueOnce(2); + pseudonymService.deleteByUserId.mockResolvedValueOnce(2); + teamService.deleteUserDataFromTeams.mockResolvedValueOnce(2); + userService.deleteUser.mockResolvedValueOnce(1); + + return { + deletionRequestToCreate, + deletionRequest, + deletionRequestToExecute, + deletionRequestExecuted, + notExecutedDeletionRequestSummary, + executedDeletionRequestSummary, + deletionLogExecuted1, + deletionLogExecuted2, + }; + }; + + describe('createDeletionRequest', () => { + describe('when creating a deletionRequest', () => { + it('should call the service to create the deletionRequest', async () => { + const { deletionRequestToCreate } = setup(); + + await uc.createDeletionRequest(deletionRequestToCreate); + + expect(deletionRequestService.createDeletionRequest).toHaveBeenCalledWith( + deletionRequestToCreate.targetRef.itemId, + deletionRequestToCreate.targetRef.domain, + deletionRequestToCreate.deleteInMinutes + ); + }); + + it('should return the deletionRequestID and deletionPlannedAt', async () => { + const { deletionRequestToCreate, deletionRequest } = setup(); + + deletionRequestService.createDeletionRequest.mockResolvedValueOnce({ + requestId: deletionRequest.id, + deletionPlannedAt: deletionRequest.deleteAfter, + }); + + const result = await uc.createDeletionRequest(deletionRequestToCreate); + + expect(result).toEqual({ + requestId: deletionRequest.id, + deletionPlannedAt: deletionRequest.deleteAfter, + }); + }); + }); + }); + + describe('executeDeletionRequests', () => { + describe('when executing deletionRequests', () => { + it('should call deletionRequestService.findAllItemsByDeletionDate', async () => { + await uc.executeDeletionRequests(); + + expect(deletionRequestService.findAllItemsByDeletionDate).toHaveBeenCalled(); + }); + + it('should call deletionRequestService.markDeletionRequestAsExecuted to update status of deletionRequests', async () => { + const { deletionRequestToExecute } = setup(); + + deletionRequestService.findAllItemsByDeletionDate.mockResolvedValueOnce([deletionRequestToExecute]); + + await uc.executeDeletionRequests(); + + expect(deletionRequestService.markDeletionRequestAsExecuted).toHaveBeenCalledWith(deletionRequestToExecute.id); + }); + + it('should call accountService.deleteByUserId to delete user data in account module', async () => { + const { deletionRequestToExecute } = setup(); + + deletionRequestService.findAllItemsByDeletionDate.mockResolvedValueOnce([deletionRequestToExecute]); + + await uc.executeDeletionRequests(); + + expect(accountService.deleteByUserId).toHaveBeenCalled(); + }); + + it('should call classService.deleteUserDataFromClasses to delete user data in class module', async () => { + const { deletionRequestToExecute } = setup(); + + deletionRequestService.findAllItemsByDeletionDate.mockResolvedValueOnce([deletionRequestToExecute]); + + await uc.executeDeletionRequests(); + + expect(classService.deleteUserDataFromClasses).toHaveBeenCalledWith(deletionRequestToExecute.itemId); + }); + + it('should call courseGroupService.deleteUserDataFromCourseGroup to delete user data in courseGroup module', async () => { + const { deletionRequestToExecute } = setup(); + + deletionRequestService.findAllItemsByDeletionDate.mockResolvedValueOnce([deletionRequestToExecute]); + + await uc.executeDeletionRequests(); + + expect(courseGroupService.deleteUserDataFromCourseGroup).toHaveBeenCalledWith(deletionRequestToExecute.itemId); + }); + + it('should call courseService.deleteUserDataFromCourse to delete user data in course module', async () => { + const { deletionRequestToExecute } = setup(); + + deletionRequestService.findAllItemsByDeletionDate.mockResolvedValueOnce([deletionRequestToExecute]); + + await uc.executeDeletionRequests(); + + expect(courseService.deleteUserDataFromCourse).toHaveBeenCalledWith(deletionRequestToExecute.itemId); + }); + + it('should call filesService.markFilesOwnedByUserForDeletion to mark users files to delete in file module', async () => { + const { deletionRequestToExecute } = setup(); + + deletionRequestService.findAllItemsByDeletionDate.mockResolvedValueOnce([deletionRequestToExecute]); + + await uc.executeDeletionRequests(); + + expect(filesService.markFilesOwnedByUserForDeletion).toHaveBeenCalledWith(deletionRequestToExecute.itemId); + }); + + it('should call filesService.removeUserPermissionsToAnyFiles to remove users permissions to any files in file module', async () => { + const { deletionRequestToExecute } = setup(); + + deletionRequestService.findAllItemsByDeletionDate.mockResolvedValueOnce([deletionRequestToExecute]); + + await uc.executeDeletionRequests(); + + expect(filesService.removeUserPermissionsToAnyFiles).toHaveBeenCalledWith(deletionRequestToExecute.itemId); + }); + + it('should call lessonService.deleteUserDataFromLessons to delete users data in lesson module', async () => { + const { deletionRequestToExecute } = setup(); + + deletionRequestService.findAllItemsByDeletionDate.mockResolvedValueOnce([deletionRequestToExecute]); + + await uc.executeDeletionRequests(); + + expect(lessonService.deleteUserDataFromLessons).toHaveBeenCalledWith(deletionRequestToExecute.itemId); + }); + + it('should call pseudonymService.deleteByUserId to delete users data in pseudonym module', async () => { + const { deletionRequestToExecute } = setup(); + + deletionRequestService.findAllItemsByDeletionDate.mockResolvedValueOnce([deletionRequestToExecute]); + + await uc.executeDeletionRequests(); + + expect(pseudonymService.deleteByUserId).toHaveBeenCalledWith(deletionRequestToExecute.itemId); + }); + + it('should call teamService.deleteUserDataFromTeams to delete users data in teams module', async () => { + const { deletionRequestToExecute } = setup(); + + deletionRequestService.findAllItemsByDeletionDate.mockResolvedValueOnce([deletionRequestToExecute]); + + await uc.executeDeletionRequests(); + + expect(teamService.deleteUserDataFromTeams).toHaveBeenCalledWith(deletionRequestToExecute.itemId); + }); + + it('should call userService.deleteUsers to delete user in user module', async () => { + const { deletionRequestToExecute } = setup(); + + deletionRequestService.findAllItemsByDeletionDate.mockResolvedValueOnce([deletionRequestToExecute]); + + await uc.executeDeletionRequests(); + + expect(userService.deleteUser).toHaveBeenCalledWith(deletionRequestToExecute.itemId); + }); + + it('should call deletionLogService.createDeletionLog to create logs for deletionRequest', async () => { + const { deletionRequestToExecute } = setup(); + + deletionRequestService.findAllItemsByDeletionDate.mockResolvedValueOnce([deletionRequestToExecute]); + + await uc.executeDeletionRequests(); + + expect(deletionLogService.createDeletionLog).toHaveBeenCalledTimes(9); + }); + }); + }); + + describe('findById', () => { + describe('when searching for logs for deletionRequest which was executed', () => { + it('should call to deletionRequestService and deletionLogService', async () => { + const { deletionRequestExecuted } = setup(); + + deletionRequestService.findById.mockResolvedValueOnce(deletionRequestExecuted); + + await uc.findById(deletionRequestExecuted.id); + + expect(deletionRequestService.findById).toHaveBeenCalledWith(deletionRequestExecuted.id); + expect(deletionLogService.findByDeletionRequestId).toHaveBeenCalledWith(deletionRequestExecuted.id); + }); + + it('should return object with summary of deletionRequest', async () => { + const { deletionRequestExecuted, deletionLogExecuted1, deletionLogExecuted2, executedDeletionRequestSummary } = + setup(); + + deletionRequestService.findById.mockResolvedValueOnce(deletionRequestExecuted); + deletionLogService.findByDeletionRequestId.mockResolvedValueOnce([deletionLogExecuted1, deletionLogExecuted2]); + + const result = await uc.findById(deletionRequestExecuted.id); + + expect(result).toEqual(executedDeletionRequestSummary); + }); + }); + + describe('when searching for logs for deletionRequest which was not executed', () => { + it('should call to deletionRequestService', async () => { + const { deletionRequest } = setup(); + + deletionRequestService.findById.mockResolvedValueOnce(deletionRequest); + + await uc.findById(deletionRequest.id); + + expect(deletionRequestService.findById).toHaveBeenCalledWith(deletionRequest.id); + expect(deletionLogService.findByDeletionRequestId).not.toHaveBeenCalled(); + }); + + it('should return object with summary of deletionRequest', async () => { + const { deletionRequest, notExecutedDeletionRequestSummary } = setup(); + + deletionRequestService.findById.mockResolvedValueOnce(deletionRequest); + + const result = await uc.findById(deletionRequest.id); + + expect(result).toEqual(notExecutedDeletionRequestSummary); + }); + }); + }); + + describe('deleteDeletionRequestById', () => { + describe('when deleting a deletionRequestId', () => { + it('should call the service deletionRequestService.deleteById', async () => { + const { deletionRequest } = setup(); + + await uc.deleteDeletionRequestById(deletionRequest.id); + + expect(deletionRequestService.deleteById).toHaveBeenCalledWith(deletionRequest.id); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/uc/deletion-request.uc.ts b/apps/server/src/modules/deletion/uc/deletion-request.uc.ts new file mode 100644 index 00000000000..982c80e88ae --- /dev/null +++ b/apps/server/src/modules/deletion/uc/deletion-request.uc.ts @@ -0,0 +1,219 @@ +import { Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain'; +import { PseudonymService } from '@src/modules/pseudonym'; +import { UserService } from '@src/modules'; +import { TeamService } from '@src/modules/teams'; +import { ClassService } from '@src/modules/class'; +import { LessonService } from '@src/modules/lesson/service'; +import { CourseService } from '@src/modules/learnroom/service'; +import { CourseGroupService } from '@src/modules/learnroom/service/coursegroup.service'; +import { FilesService } from '@src/modules/files/service'; +import { AccountService } from '@src/modules/account/services/account.service'; +import { DeletionRequestService } from '../services/deletion-request.service'; +import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; +import { DeletionLogService } from '../services/deletion-log.service'; +import { DeletionRequest } from '../domain/deletion-request.do'; +import { DeletionOperationModel } from '../domain/types/deletion-operation-model.enum'; +import { DeletionStatusModel } from '../domain/types/deletion-status-model.enum'; +import { DeletionLog } from '../domain/deletion-log.do'; + +export interface DeletionRequestLog { + targetRef: { domain: DeletionDomainModel; itemId: EntityId }; + deletionPlannedAt: Date; + statistics?: DeletionLogStatistic[]; +} + +export interface DeletionLogStatistic { + domain: DeletionDomainModel; + modifiedCount?: number; + deletedCount?: number; +} + +export interface DeletionRequestProps { + targetRef: { domain: DeletionDomainModel; itemId: EntityId }; + deleteInMinutes?: number; +} + +@Injectable() +export class DeletionRequestUc { + constructor( + private readonly deletionRequestService: DeletionRequestService, + private readonly deletionLogService: DeletionLogService, + private readonly accountService: AccountService, + private readonly classService: ClassService, + private readonly courseGroupService: CourseGroupService, + private readonly courseService: CourseService, + private readonly filesService: FilesService, + private readonly lessonService: LessonService, + private readonly pseudonymService: PseudonymService, + private readonly teamService: TeamService, + private readonly userService: UserService + ) {} + + async createDeletionRequest( + deletionRequest: DeletionRequestProps + ): Promise<{ requestId: EntityId; deletionPlannedAt: Date }> { + const result = await this.deletionRequestService.createDeletionRequest( + deletionRequest.targetRef.itemId, + deletionRequest.targetRef.domain, + deletionRequest.deleteInMinutes + ); + + return result; + } + + async executeDeletionRequests(): Promise { + const deletionRequestToExecution: DeletionRequest[] = + await this.deletionRequestService.findAllItemsByDeletionDate(); + + for (const req of deletionRequestToExecution) { + // eslint-disable-next-line no-await-in-loop + await this.executeDeletionRequest(req); + } + } + + async findById(deletionRequestId: EntityId): Promise { + const deletionRequest = await this.deletionRequestService.findById(deletionRequestId); + let response: DeletionRequestLog = { + targetRef: { + domain: deletionRequest.domain, + itemId: deletionRequest.itemId, + }, + deletionPlannedAt: deletionRequest.deleteAfter, + }; + + if (deletionRequest.status === DeletionStatusModel.SUCCESS) { + const deletionLog: DeletionLog[] = await this.deletionLogService.findByDeletionRequestId(deletionRequestId); + const deletionLogStatistic: DeletionLogStatistic[] = deletionLog.map((log) => { + return { + domain: log.domain, + modifiedCount: log.modifiedCount, + deletedCount: log.deletedCount, + }; + }); + response = { ...response, statistics: deletionLogStatistic }; + } + + return response; + } + + async deleteDeletionRequestById(deletionRequestId: EntityId): Promise { + await this.deletionRequestService.deleteById(deletionRequestId); + } + + private async executeDeletionRequest(deletionRequest: DeletionRequest): Promise { + await this.deletionRequestService.markDeletionRequestAsExecuted(deletionRequest.id); + + const [ + , + classesUpdated, + courseGroupUpdated, + courseUpdated, + fileDeleted, + filesPermisionUpdated, + lessonUpdated, + pseudonymDeleted, + teamsUpdated, + userDeleted, + ] = await Promise.all([ + this.accountService.deleteByUserId(deletionRequest.itemId), + this.classService.deleteUserDataFromClasses(deletionRequest.itemId), + this.courseGroupService.deleteUserDataFromCourseGroup(deletionRequest.itemId), + this.courseService.deleteUserDataFromCourse(deletionRequest.itemId), + this.filesService.markFilesOwnedByUserForDeletion(deletionRequest.itemId), + this.filesService.removeUserPermissionsToAnyFiles(deletionRequest.itemId), + this.lessonService.deleteUserDataFromLessons(deletionRequest.itemId), + this.pseudonymService.deleteByUserId(deletionRequest.itemId), + this.teamService.deleteUserDataFromTeams(deletionRequest.itemId), + this.userService.deleteUser(deletionRequest.itemId), + ]); + + await this.deletionLogService.createDeletionLog( + deletionRequest.id, + DeletionDomainModel.ACCOUNT, + DeletionOperationModel.DELETE, + 0, + 1 + ); + + if (classesUpdated > 0) { + await this.deletionLogService.createDeletionLog( + deletionRequest.id, + DeletionDomainModel.CLASS, + DeletionOperationModel.UPDATE, + classesUpdated, + 0 + ); + } + + if (courseGroupUpdated > 0) { + await this.deletionLogService.createDeletionLog( + deletionRequest.id, + DeletionDomainModel.COURSEGROUP, + DeletionOperationModel.UPDATE, + courseGroupUpdated, + 0 + ); + } + + if (courseUpdated > 0) { + await this.deletionLogService.createDeletionLog( + deletionRequest.id, + DeletionDomainModel.COURSE, + DeletionOperationModel.UPDATE, + courseUpdated, + 0 + ); + } + + if (fileDeleted > 0 || filesPermisionUpdated > 0) { + await this.deletionLogService.createDeletionLog( + deletionRequest.id, + DeletionDomainModel.FILE, + DeletionOperationModel.UPDATE, + fileDeleted + filesPermisionUpdated, + 0 + ); + } + + if (lessonUpdated > 0) { + await this.deletionLogService.createDeletionLog( + deletionRequest.id, + DeletionDomainModel.LESSONS, + DeletionOperationModel.UPDATE, + lessonUpdated, + 0 + ); + } + + if (pseudonymDeleted > 0) { + await this.deletionLogService.createDeletionLog( + deletionRequest.id, + DeletionDomainModel.PSEUDONYMS, + DeletionOperationModel.DELETE, + 0, + pseudonymDeleted + ); + } + + if (teamsUpdated > 0) { + await this.deletionLogService.createDeletionLog( + deletionRequest.id, + DeletionDomainModel.TEAMS, + DeletionOperationModel.UPDATE, + teamsUpdated, + 0 + ); + } + + if (userDeleted > 0) { + await this.deletionLogService.createDeletionLog( + deletionRequest.id, + DeletionDomainModel.USER, + DeletionOperationModel.DELETE, + 0, + userDeleted + ); + } + } +} diff --git a/apps/server/src/modules/deletion/uc/deletion-worker.us.ts b/apps/server/src/modules/deletion/uc/deletion-worker.us.ts deleted file mode 100644 index 9dbb19b41aa..00000000000 --- a/apps/server/src/modules/deletion/uc/deletion-worker.us.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { AuthorizationService } from '@src/modules/authorization'; -import { DeletionRequestService } from '../services/deletion-request.service'; -import { DeletionRequest } from '../domain/deletion-request.do'; - -@Injectable() -export class DeletionWorkerUc { - constructor( - private readonly deletionRequestService: DeletionRequestService, - private readonly authorizationService: AuthorizationService - ) {} - - async findAllItemsByDeletionDate(): Promise { - return this.deletionRequestService.findAllItemsByDeletionDate(); - } -} From c56ae3d7dbca30bea3d1b237a9fee7fa8b061fe1 Mon Sep 17 00:00:00 2001 From: WojciechGrancow Date: Wed, 1 Nov 2023 23:18:55 +0100 Subject: [PATCH 08/72] fix importing --- apps/server/src/modules/deletion/uc/deletion-request.uc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 982c80e88ae..4cfeb6f9c30 100644 --- a/apps/server/src/modules/deletion/uc/deletion-request.uc.ts +++ b/apps/server/src/modules/deletion/uc/deletion-request.uc.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain'; import { PseudonymService } from '@src/modules/pseudonym'; -import { UserService } from '@src/modules'; +import { UserService } from '@src/modules/user'; import { TeamService } from '@src/modules/teams'; import { ClassService } from '@src/modules/class'; import { LessonService } from '@src/modules/lesson/service'; From cad449129d004edf2f5914a0f385e82a7a61686c Mon Sep 17 00:00:00 2001 From: WojciechGrancow Date: Thu, 2 Nov 2023 07:21:28 +0100 Subject: [PATCH 09/72] add type in uc --- apps/server/src/modules/deletion/uc/deletion-request.uc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4cfeb6f9c30..8cd1d3d0025 100644 --- a/apps/server/src/modules/deletion/uc/deletion-request.uc.ts +++ b/apps/server/src/modules/deletion/uc/deletion-request.uc.ts @@ -73,7 +73,7 @@ export class DeletionRequestUc { } async findById(deletionRequestId: EntityId): Promise { - const deletionRequest = await this.deletionRequestService.findById(deletionRequestId); + const deletionRequest: DeletionRequest = await this.deletionRequestService.findById(deletionRequestId); let response: DeletionRequestLog = { targetRef: { domain: deletionRequest.domain, From b80ff0e7a98475dd26a0c1c6a6dac834a77d3e08 Mon Sep 17 00:00:00 2001 From: WojciechGrancow Date: Thu, 2 Nov 2023 08:00:26 +0100 Subject: [PATCH 10/72] fix import --- apps/server/src/modules/deletion/uc/deletion-request.uc.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6525e71e97f..87f2cf0db39 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 @@ -9,7 +9,7 @@ import { FilesService } from '@src/modules/files/service'; import { LessonService } from '@src/modules/lesson/service'; import { PseudonymService } from '@src/modules/pseudonym'; import { TeamService } from '@src/modules/teams'; -import { UserService } from '@src/modules'; +import { UserService } from '@src/modules/user'; import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; import { DeletionLogService } from '../services/deletion-log.service'; import { DeletionRequestService } from '../services'; From 5d764d00fbd3dc6460fc7f91b4bf3064bc5e53ea Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Thu, 2 Nov 2023 16:32:44 +0100 Subject: [PATCH 11/72] add references service that'll load all the references to the data we want to delete --- .../modules/batch-deletion/service/index.ts | 1 + .../service/references.service.spec.ts | 62 +++++++++++++++++++ .../service/references.service.ts | 29 +++++++++ 3 files changed, 92 insertions(+) create mode 100644 apps/server/src/modules/batch-deletion/service/index.ts create mode 100644 apps/server/src/modules/batch-deletion/service/references.service.spec.ts create mode 100644 apps/server/src/modules/batch-deletion/service/references.service.ts diff --git a/apps/server/src/modules/batch-deletion/service/index.ts b/apps/server/src/modules/batch-deletion/service/index.ts new file mode 100644 index 00000000000..18d874794c5 --- /dev/null +++ b/apps/server/src/modules/batch-deletion/service/index.ts @@ -0,0 +1 @@ +export * from './references.service'; diff --git a/apps/server/src/modules/batch-deletion/service/references.service.spec.ts b/apps/server/src/modules/batch-deletion/service/references.service.spec.ts new file mode 100644 index 00000000000..02ed9b53068 --- /dev/null +++ b/apps/server/src/modules/batch-deletion/service/references.service.spec.ts @@ -0,0 +1,62 @@ +import fs from 'fs'; +import { ReferencesService } from './references.service'; + +describe(ReferencesService.name, () => { + describe(ReferencesService.loadFromTxtFile.name, () => { + const setup = (mockedFileContent: string) => { + jest.spyOn(fs, 'readFileSync').mockReturnValueOnce(mockedFileContent); + }; + + describe('when passed a completely empty file (without any content)', () => { + it('should return an empty references array', () => { + setup(''); + + const references = ReferencesService.loadFromTxtFile('references.txt'); + + expect(references).toEqual([]); + }); + }); + + describe('when passed a file without any references (just some empty lines)', () => { + it('should return an empty references array', () => { + setup('\n\n \n \n\n\n \n\n\n'); + + const references = ReferencesService.loadFromTxtFile('references.txt'); + + expect(references).toEqual([]); + }); + }); + + describe('when passed a file with 3 references on a few separate lines', () => { + describe('split with LFs', () => { + it('should return an array with all the references present in a file', () => { + setup('653fd3b784ca851b17e98579\n' + '653fd3b784ca851b17e9857a\n' + '653fd3b784ca851b17e9857b\n\n\n'); + + const references = ReferencesService.loadFromTxtFile('references.txt'); + + expect(references).toEqual([ + '653fd3b784ca851b17e98579', + '653fd3b784ca851b17e9857a', + '653fd3b784ca851b17e9857b', + ]); + }); + }); + + describe('split with CRLFs', () => { + it('should return an array with all the references present in a file', () => { + setup( + '653fd3b784ca851b17e98579\r\n' + '653fd3b784ca851b17e9857a\r\n' + '653fd3b784ca851b17e9857b\r\n\r\n\r\n' + ); + + const references = ReferencesService.loadFromTxtFile('references.txt'); + + expect(references).toEqual([ + '653fd3b784ca851b17e98579', + '653fd3b784ca851b17e9857a', + '653fd3b784ca851b17e9857b', + ]); + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/batch-deletion/service/references.service.ts b/apps/server/src/modules/batch-deletion/service/references.service.ts new file mode 100644 index 00000000000..0224283a28a --- /dev/null +++ b/apps/server/src/modules/batch-deletion/service/references.service.ts @@ -0,0 +1,29 @@ +import fs from 'fs'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class ReferencesService { + static loadFromTxtFile(filePath: string): string[] { + let fileContent = fs.readFileSync(filePath).toString(); + + // Replace all the CRLF occurrences with just a LF. + fileContent = fileContent.replace(/\r\n/g, '\n'); + + // Split the whole file content by a line feed (LF) char (\n). + const fileLines = fileContent.split('\n'); + + const references: string[] = []; + + // Iterate over all the file lines and if it contains a valid id (which is + // basically any non-empty string), add it to the loaded references array. + fileLines.forEach((fileLine) => { + const reference = fileLine.trim(); + + if (reference && reference.length > 0) { + references.push(reference); + } + }); + + return references; + } +} From c226c6df042013069919ae619232783cd1033fba Mon Sep 17 00:00:00 2001 From: WojciechGrancow Date: Fri, 3 Nov 2023 09:40:39 +0100 Subject: [PATCH 12/72] fix most of issue form review --- .../deletion/domain/deletion-log.do.spec.ts | 8 +++- .../domain/deletion-request.do.spec.ts | 17 +++++-- .../deletion/domain/deletion-request.do.ts | 12 ++--- .../factory/deletion-request.factory.ts | 8 ++-- .../types/deletion-status-model.enum.ts | 6 +-- .../entity/deletion-request.entity.spec.ts | 9 ++-- .../entity/deletion-request.entity.ts | 19 +++----- .../factory/deletion-log.entity.factory.ts | 6 +-- .../deletion-request.entity.factory.ts | 10 ++-- .../deletion/repo/deletion-log.repo.spec.ts | 26 +++++----- .../repo/deletion-request.repo.spec.ts | 36 +++++++------- .../repo/mapper/deletion-log.mapper.spec.ts | 47 ++++++++++++++----- .../mapper/deletion-request.mapper.spec.ts | 33 ++++++++----- .../repo/mapper/deletion-request.mapper.ts | 9 ++-- .../deletion/services/deletion-log.service.ts | 8 ++-- .../services/deletion-request.service.spec.ts | 14 +++--- .../services/deletion-request.service.ts | 8 ++-- .../deletion/uc/deletion-request.uc.spec.ts | 36 +++++++------- .../deletion/uc/deletion-request.uc.ts | 32 ++++++------- 19 files changed, 195 insertions(+), 149 deletions(-) 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 860e07f56de..9117ded29c5 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 @@ -33,7 +33,7 @@ describe(DeletionLog.name, () => { describe('getters', () => { describe('When getters are used', () => { - it('getters should return proper values', () => { + const setup = () => { const props = { id: new ObjectId().toHexString(), domain: DeletionDomainModel.USER, @@ -46,6 +46,12 @@ describe(DeletionLog.name, () => { }; const deletionLogDo = new DeletionLog(props); + + return { props, deletionLogDo }; + }; + it('getters should return proper values', () => { + const { props, deletionLogDo } = setup(); + const gettersValues = { id: deletionLogDo.id, domain: deletionLogDo.domain, 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 a2262f0b614..3c0eb608c87 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 @@ -33,23 +33,30 @@ describe(DeletionRequest.name, () => { describe('getters', () => { describe('When getters are used', () => { - it('getters should return proper values', () => { + const setup = () => { const props = { id: new ObjectId().toHexString(), - domain: DeletionDomainModel.USER, + targetRefDomain: DeletionDomainModel.USER, deleteAfter: new Date(), - itemId: new ObjectId().toHexString(), + targetRefId: new ObjectId().toHexString(), status: DeletionStatusModel.REGISTERED, createdAt: new Date(), updatedAt: new Date(), }; const deletionRequestDo = new DeletionRequest(props); + + return { props, deletionRequestDo }; + }; + + it('getters should return proper values', () => { + const { props, deletionRequestDo } = setup(); + const gettersValues = { id: deletionRequestDo.id, - domain: deletionRequestDo.domain, + targetRefDomain: deletionRequestDo.targetRefDomain, deleteAfter: deletionRequestDo.deleteAfter, - itemId: deletionRequestDo.itemId, + targetRefId: deletionRequestDo.targetRefId, status: deletionRequestDo.status, createdAt: deletionRequestDo.createdAt, updatedAt: deletionRequestDo.updatedAt, 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 6d1349a65f2..e1a8b289ef0 100644 --- a/apps/server/src/modules/deletion/domain/deletion-request.do.ts +++ b/apps/server/src/modules/deletion/domain/deletion-request.do.ts @@ -6,9 +6,9 @@ import { DeletionStatusModel } from './types/deletion-status-model.enum'; export interface DeletionRequestProps extends AuthorizableObject { createdAt?: Date; updatedAt?: Date; - domain: DeletionDomainModel; + targetRefDomain: DeletionDomainModel; deleteAfter: Date; - itemId: EntityId; + targetRefId: EntityId; status: DeletionStatusModel; } @@ -21,16 +21,16 @@ export class DeletionRequest extends DomainObject { return this.props.updatedAt; } - get domain(): DeletionDomainModel { - return this.props.domain; + get targetRefDomain(): DeletionDomainModel { + return this.props.targetRefDomain; } get deleteAfter(): Date { return this.props.deleteAfter; } - get itemId(): EntityId { - return this.props.itemId; + get targetRefId(): EntityId { + return this.props.targetRefId; } get status(): DeletionStatusModel { 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 36cad2e8aa0..9f87bbc1cbf 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 @@ -6,9 +6,9 @@ import { DeletionDomainModel } from '../../types/deletion-domain-model.enum'; import { DeletionStatusModel } from '../../types/deletion-status-model.enum'; class DeletionRequestFactory extends DoBaseFactory { - withUserIds(itemId: string): this { + withUserIds(id: string): this { const params: DeepPartial = { - itemId, + targetRefId: id, }; return this.params(params); @@ -18,9 +18,9 @@ class DeletionRequestFactory extends DoBaseFactory { return { id: new ObjectId().toHexString(), - domain: DeletionDomainModel.USER, + targetRefDomain: DeletionDomainModel.USER, deleteAfter: new Date(), - itemId: new ObjectId().toHexString(), + targetRefId: new ObjectId().toHexString(), status: DeletionStatusModel.REGISTERED, createdAt: new Date(), updatedAt: new Date(), 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 index 884cccdf68f..4bce8af628c 100644 --- 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 @@ -1,4 +1,4 @@ -export enum DeletionStatusModel { - 'REGISTERED' = 'registered', - 'SUCCESS' = 'success', +export const enum DeletionStatusModel { + REGISTERED = 'registered', + SUCCESS = 'success', } 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 1552360e2fd..2097f53a9c2 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 @@ -3,7 +3,6 @@ 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'; -// import { deletionRequestEntityFactory } from './testing/factory/deletion-request.entity.factory'; describe(DeletionRequestEntity.name, () => { beforeAll(async () => { @@ -15,9 +14,9 @@ describe(DeletionRequestEntity.name, () => { const props = { id: new ObjectId().toHexString(), - domain: DeletionDomainModel.USER, + targetRefDomain: DeletionDomainModel.USER, deleteAfter: new Date(), - itemId: new ObjectId().toHexString(), + targetRefId: new ObjectId().toHexString(), status: DeletionStatusModel.REGISTERED, createdAt: new Date(), updatedAt: new Date(), @@ -47,9 +46,9 @@ describe(DeletionRequestEntity.name, () => { const entityProps = { id: entity.id, - domain: entity.domain, + targetRefDomain: entity.targetRefDomain, deleteAfter: entity.deleteAfter, - itemId: entity.itemId, + targetRefId: entity.targetRefId, status: entity.status, createdAt: entity.createdAt, updatedAt: entity.updatedAt, 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 6494edca17f..5a37d1b71b7 100644 --- a/apps/server/src/modules/deletion/entity/deletion-request.entity.ts +++ b/apps/server/src/modules/deletion/entity/deletion-request.entity.ts @@ -1,35 +1,30 @@ 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; - domain: DeletionDomainModel; + targetRefDomain: DeletionDomainModel; deleteAfter: Date; - itemId: EntityId; + targetRefId: EntityId; status: DeletionStatusModel; createdAt?: Date; updatedAt?: Date; } @Entity({ tableName: 'deletionrequests' }) -@Index({ properties: ['_itemId', 'domain'] }) +@Index({ properties: ['targetRefId', 'targetRefDomain'] }) export class DeletionRequestEntity extends BaseEntityWithTimestamps { @Property() deleteAfter: Date; @Property() - _itemId: ObjectId; - - get itemId(): EntityId { - return this._itemId.toHexString(); - } + targetRefId: EntityId; @Property() - domain: DeletionDomainModel; + targetRefDomain: DeletionDomainModel; @Property() status: DeletionStatusModel; @@ -40,9 +35,9 @@ export class DeletionRequestEntity extends BaseEntityWithTimestamps { this.id = props.id; } - this.domain = props.domain; + this.targetRefDomain = props.targetRefDomain; this.deleteAfter = props.deleteAfter; - this._itemId = new ObjectId(props.itemId); + this.targetRefId = props.targetRefId; this.status = props.status; if (props.createdAt !== undefined) { 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 a8b40c381db..897fba6820a 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,8 +1,8 @@ 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'; +import { DeletionLogEntity, DeletionLogEntityProps } from '../../deletion-log.entity'; +import { DeletionOperationModel } from '../../../domain/types/deletion-operation-model.enum'; +import { DeletionDomainModel } from '../../../domain/types/deletion-domain-model.enum'; export const deletionLogEntityFactory = BaseFactory.define( DeletionLogEntity, 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 6e1a0f326ec..3ccba779e3e 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,17 +1,17 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { BaseFactory } from '@shared/testing'; -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'; +import { DeletionStatusModel } from '../../../domain/types/deletion-status-model.enum'; +import { DeletionRequestEntity, DeletionRequestEntityProps } from '../../deletion-request.entity'; +import { DeletionDomainModel } from '../../../domain/types/deletion-domain-model.enum'; export const deletionRequestEntityFactory = BaseFactory.define( DeletionRequestEntity, () => { return { id: new ObjectId().toHexString(), - domain: DeletionDomainModel.USER, + targetRefDomain: DeletionDomainModel.USER, deleteAfter: new Date(), - itemId: new ObjectId().toHexString(), + targetRefId: 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 index 94d451023b3..5bc151c3541 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 @@ -53,10 +53,9 @@ describe(DeletionLogRepo.name, () => { describe('create deletionLog', () => { describe('when deletionLog is new', () => { - it('should create a new deletionLog', async () => { + const setup = () => { const domainObject: DeletionLog = deletionLogFactory.build(); const deletionLogId = domainObject.id; - await repo.create(domainObject); const expectedDomainObject = { id: domainObject.id, @@ -69,9 +68,14 @@ describe(DeletionLogRepo.name, () => { updatedAt: domainObject.updatedAt, }; + return { domainObject, deletionLogId, expectedDomainObject }; + }; + it('should create a new deletionLog', async () => { + const { domainObject, deletionLogId, expectedDomainObject } = setup(); + await repo.create(domainObject); + const result = await repo.findById(deletionLogId); - // expect(result).toEqual(domainObject); expect(result).toEqual(expect.objectContaining(expectedDomainObject)); }); }); @@ -139,14 +143,6 @@ describe(DeletionLogRepo.name, () => { 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, @@ -170,6 +166,14 @@ describe(DeletionLogRepo.name, () => { }, ]; + return { deletionLogEntity3, deletionRequest1Id, expectedArray }; + }; + + it('should find deletionRequests with deleteAfter smaller then today', async () => { + const { deletionLogEntity3, deletionRequest1Id, expectedArray } = await setup(); + + const results = await repo.findAllByDeletionRequestId(deletionRequest1Id.toHexString()); + expect(results.length).toEqual(2); // Verify explicit fields. 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 index fd28755a6b1..4abb324dece 100644 --- a/apps/server/src/modules/deletion/repo/deletion-request.repo.spec.ts +++ b/apps/server/src/modules/deletion/repo/deletion-request.repo.spec.ts @@ -71,14 +71,14 @@ describe(DeletionRequestRepo.name, () => { const setup = async () => { const userId = new ObjectId().toHexString(); - const entity: DeletionRequestEntity = deletionRequestEntityFactory.build({ itemId: userId }); + const entity: DeletionRequestEntity = deletionRequestEntityFactory.build({ targetRefId: userId }); await em.persistAndFlush(entity); const expectedDeletionRequest = { id: entity.id, - domain: entity.domain, + targetRefDomain: entity.targetRefDomain, deleteAfter: entity.deleteAfter, - itemId: entity.itemId, + targetRefId: entity.targetRefId, status: entity.status, createdAt: entity.createdAt, updatedAt: entity.updatedAt, @@ -127,35 +127,35 @@ describe(DeletionRequestRepo.name, () => { 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, + targetRefDomain: deletionRequestEntity1.targetRefDomain, deleteAfter: deletionRequestEntity1.deleteAfter, - itemId: deletionRequestEntity1.itemId, + targetRefId: deletionRequestEntity1.targetRefId, status: deletionRequestEntity1.status, createdAt: deletionRequestEntity1.createdAt, updatedAt: deletionRequestEntity1.updatedAt, }, { id: deletionRequestEntity2.id, - domain: deletionRequestEntity2.domain, + targetRefDomain: deletionRequestEntity2.targetRefDomain, deleteAfter: deletionRequestEntity2.deleteAfter, - itemId: deletionRequestEntity2.itemId, + targetRefId: deletionRequestEntity2.targetRefId, status: deletionRequestEntity2.status, createdAt: deletionRequestEntity2.createdAt, updatedAt: deletionRequestEntity2.updatedAt, }, ]; + return { deletionRequestEntity3, expectedArray }; + }; + + it('should find deletionRequests with deleteAfter smaller then today', async () => { + const { deletionRequestEntity3, expectedArray } = await setup(); + + const results = await repo.findAllItemsByDeletionDate(); + expect(results.length).toEqual(2); // Verify explicit fields. @@ -175,7 +175,7 @@ describe(DeletionRequestRepo.name, () => { const setup = async () => { const userId = new ObjectId().toHexString(); - const entity: DeletionRequestEntity = deletionRequestEntityFactory.build({ itemId: userId }); + const entity: DeletionRequestEntity = deletionRequestEntityFactory.build({ targetRefId: userId }); await em.persistAndFlush(entity); // Arrange expected DeletionRequestEntity after changing status @@ -205,7 +205,7 @@ describe(DeletionRequestRepo.name, () => { const setup = async () => { const userId = new ObjectId().toHexString(); - const entity: DeletionRequestEntity = deletionRequestEntityFactory.build({ itemId: userId }); + const entity: DeletionRequestEntity = deletionRequestEntityFactory.build({ targetRefId: userId }); await em.persistAndFlush(entity); return { entity }; @@ -235,7 +235,7 @@ describe(DeletionRequestRepo.name, () => { describe('when deleting deletionRequest exists', () => { const setup = async () => { const userId = new ObjectId().toHexString(); - const entity: DeletionRequestEntity = deletionRequestEntityFactory.build({ itemId: userId }); + const entity: DeletionRequestEntity = deletionRequestEntityFactory.build({ targetRefId: userId }); const deletionRequestId = entity.id; await em.persistAndFlush(entity); em.clear(); 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 008c54502c2..a5823f5ce32 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 @@ -8,11 +8,9 @@ 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 setup = () => { const entity = deletionLogEntityFactory.build(); - const domainObject = DeletionLogMapper.mapToDO(entity); - const expectedDomainObject = new DeletionLog({ id: entity.id, domain: entity.domain, @@ -24,6 +22,13 @@ describe(DeletionLogMapper.name, () => { updatedAt: entity.updatedAt, }); + return { entity, expectedDomainObject }; + }; + it('should properly map the entity to the domain object', () => { + const { entity, expectedDomainObject } = setup(); + + const domainObject = DeletionLogMapper.mapToDO(entity); + expect(domainObject).toEqual(expectedDomainObject); }); }); @@ -39,11 +44,9 @@ describe(DeletionLogMapper.name, () => { }); describe('When entities array is mapped for domainObjects array', () => { - it('should properly map the entities to the domain objects', () => { + const setup = () => { const entities = [deletionLogEntityFactory.build()]; - const domainObjects = DeletionLogMapper.mapToDOs(entities); - const expectedDomainObjects = entities.map( (entity) => new DeletionLog({ @@ -58,6 +61,13 @@ describe(DeletionLogMapper.name, () => { }) ); + return { entities, expectedDomainObjects }; + }; + it('should properly map the entities to the domain objects', () => { + const { entities, expectedDomainObjects } = setup(); + + const domainObjects = DeletionLogMapper.mapToDOs(entities); + expect(domainObjects).toEqual(expectedDomainObjects); }); }); @@ -74,11 +84,9 @@ describe(DeletionLogMapper.name, () => { jest.useRealTimers(); }); - it('should properly map the domainObject to the entity', () => { + const setup = () => { const domainObject = deletionLogFactory.build(); - const entities = DeletionLogMapper.mapToEntity(domainObject); - const expectedEntities = new DeletionLogEntity({ id: domainObject.id, domain: domainObject.domain, @@ -90,6 +98,14 @@ describe(DeletionLogMapper.name, () => { updatedAt: domainObject.updatedAt, }); + return { domainObject, expectedEntities }; + }; + + it('should properly map the domainObject to the entity', () => { + const { domainObject, expectedEntities } = setup(); + + const entities = DeletionLogMapper.mapToEntity(domainObject); + expect(entities).toEqual(expectedEntities); }); }); @@ -114,11 +130,9 @@ describe(DeletionLogMapper.name, () => { jest.useRealTimers(); }); - it('should properly map the domainObjects to the entities', () => { + const setup = () => { const domainObjects = [deletionLogFactory.build()]; - const entities = DeletionLogMapper.mapToEntities(domainObjects); - const expectedEntities = domainObjects.map( (domainObject) => new DeletionLogEntity({ @@ -132,6 +146,15 @@ describe(DeletionLogMapper.name, () => { updatedAt: domainObject.updatedAt, }) ); + + return { domainObjects, expectedEntities }; + }; + + it('should properly map the domainObjects to the entities', () => { + const { domainObjects, expectedEntities } = setup(); + + const entities = DeletionLogMapper.mapToEntities(domainObjects); + expect(entities).toEqual(expectedEntities); }); }); 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 index a4bbedc9946..4e880aab54e 100644 --- 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 @@ -7,21 +7,27 @@ 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 setup = () => { const entity = deletionRequestEntityFactory.build(); - const domainObject = DeletionRequestMapper.mapToDO(entity); - const expectedDomainObject = new DeletionRequest({ id: entity.id, - domain: entity.domain, + targetRefDomain: entity.targetRefDomain, deleteAfter: entity.deleteAfter, - itemId: entity.itemId, + targetRefId: entity.targetRefId, status: entity.status, createdAt: entity.createdAt, updatedAt: entity.updatedAt, }); + return { entity, expectedDomainObject }; + }; + + it('should properly map the entity to the domain object', () => { + const { entity, expectedDomainObject } = setup(); + + const domainObject = DeletionRequestMapper.mapToDO(entity); + expect(domainObject).toEqual(expectedDomainObject); }); }); @@ -37,22 +43,27 @@ describe(DeletionRequestMapper.name, () => { afterAll(() => { jest.useRealTimers(); }); - - it('should properly map the domainObject to the entity', () => { + const setup = () => { const domainObject = deletionRequestFactory.build(); - const entity = DeletionRequestMapper.mapToEntity(domainObject); - const expectedEntity = new DeletionRequestEntity({ id: domainObject.id, - domain: domainObject.domain, + targetRefDomain: domainObject.targetRefDomain, deleteAfter: domainObject.deleteAfter, - itemId: domainObject.itemId, + targetRefId: domainObject.targetRefId, status: domainObject.status, createdAt: domainObject.createdAt, updatedAt: domainObject.updatedAt, }); + return { domainObject, expectedEntity }; + }; + + it('should properly map the domainObject to the entity', () => { + const { domainObject, expectedEntity } = setup(); + + const entity = DeletionRequestMapper.mapToEntity(domainObject); + 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 c757606ce69..fd6c273011f 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 @@ -1,4 +1,3 @@ -import { ObjectId } from '@mikro-orm/mongodb'; import { DeletionRequest } from '../../domain/deletion-request.do'; import { DeletionRequestEntity } from '../../entity'; @@ -8,9 +7,9 @@ export class DeletionRequestMapper { id: entity.id, createdAt: entity.createdAt, updatedAt: entity.updatedAt, - domain: entity.domain, + targetRefDomain: entity.targetRefDomain, deleteAfter: entity.deleteAfter, - itemId: entity.itemId, + targetRefId: entity.targetRefId, status: entity.status, }); } @@ -18,9 +17,9 @@ export class DeletionRequestMapper { static mapToEntity(domainObject: DeletionRequest): DeletionRequestEntity { return new DeletionRequestEntity({ id: domainObject.id, - domain: domainObject.domain, + targetRefDomain: domainObject.targetRefDomain, deleteAfter: domainObject.deleteAfter, - itemId: new ObjectId(domainObject.itemId).toHexString(), + targetRefId: domainObject.targetRefId, createdAt: domainObject.createdAt, updatedAt: domainObject.updatedAt, status: domainObject.status, 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 d37f9b9c4a9..937d422ebb3 100644 --- a/apps/server/src/modules/deletion/services/deletion-log.service.ts +++ b/apps/server/src/modules/deletion/services/deletion-log.service.ts @@ -14,16 +14,16 @@ export class DeletionLogService { deletionRequestId: EntityId, domain: DeletionDomainModel, operation: DeletionOperationModel, - modifiedCounter: number, - deletedCounter: number + modifiedCount: number, + deletedCount: number ): Promise { const newDeletionLog = new DeletionLog({ id: new ObjectId().toHexString(), domain, deletionRequestId, operation, - modifiedCount: modifiedCounter, - deletedCount: deletedCounter, + modifiedCount, + deletedCount, }); 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 e0396d6e81b..bbe46aef49f 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 @@ -47,23 +47,23 @@ describe(DeletionRequestService.name, () => { describe('createDeletionRequest', () => { describe('when creating a deletionRequest', () => { const setup = () => { - const itemId = '653e4833cc39e5907a1e18d2'; - const domain = DeletionDomainModel.USER; + const targetRefId = '653e4833cc39e5907a1e18d2'; + const targetRefDomain = DeletionDomainModel.USER; - return { itemId, domain }; + return { targetRefId, targetRefDomain }; }; it('should call deletionRequestRepo.create', async () => { - const { itemId, domain } = setup(); + const { targetRefId, targetRefDomain } = setup(); - await service.createDeletionRequest(itemId, domain); + await service.createDeletionRequest(targetRefId, targetRefDomain); expect(deletionRequestRepo.create).toHaveBeenCalledWith( expect.objectContaining({ id: expect.any(String), - domain, + targetRefDomain, deleteAfter: expect.any(Date), - itemId, + targetRefId, status: DeletionStatusModel.REGISTERED, }) ); 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 570661d4f92..51b1a266e4e 100644 --- a/apps/server/src/modules/deletion/services/deletion-request.service.ts +++ b/apps/server/src/modules/deletion/services/deletion-request.service.ts @@ -11,8 +11,8 @@ export class DeletionRequestService { constructor(private readonly deletionRequestRepo: DeletionRequestRepo) {} async createDeletionRequest( - itemId: EntityId, - domain: DeletionDomainModel, + targetRefId: EntityId, + targetRefDomain: DeletionDomainModel, deleteInMinutes?: number ): Promise<{ requestId: EntityId; deletionPlannedAt: Date }> { deleteInMinutes = deleteInMinutes === undefined ? 43200 : deleteInMinutes; @@ -22,9 +22,9 @@ export class DeletionRequestService { const newDeletionRequest = new DeletionRequest({ id: new ObjectId().toHexString(), - domain, + targetRefDomain, deleteAfter: dateOfDeletion, - itemId, + targetRefId, status: DeletionStatusModel.REGISTERED, }); 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 87f2cf0db39..902e5d079a8 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 @@ -103,8 +103,8 @@ describe(DeletionRequestUc.name, () => { jest.clearAllMocks(); const deletionRequestToCreate: DeletionRequestProps = { targetRef: { - domain: DeletionDomainModel.USER, - itemId: '653e4833cc39e5907a1e18d2', + targetRefDoamin: DeletionDomainModel.USER, + targetRefId: '653e4833cc39e5907a1e18d2', }, deleteInMinutes: 1440, }; @@ -122,8 +122,8 @@ describe(DeletionRequestUc.name, () => { const executedDeletionRequestSummary: DeletionRequestLog = { targetRef: { - domain: deletionRequestExecuted.domain, - itemId: deletionRequestExecuted.itemId, + targetRefDoamin: deletionRequestExecuted.targetRefDomain, + targetRefId: deletionRequestExecuted.targetRefId, }, deletionPlannedAt: deletionRequestExecuted.deleteAfter, statistics: [ @@ -142,8 +142,8 @@ describe(DeletionRequestUc.name, () => { const notExecutedDeletionRequestSummary: DeletionRequestLog = { targetRef: { - domain: deletionRequest.domain, - itemId: deletionRequest.itemId, + targetRefDoamin: deletionRequest.targetRefDomain, + targetRefId: deletionRequest.targetRefId, }, deletionPlannedAt: deletionRequest.deleteAfter, }; @@ -178,8 +178,8 @@ describe(DeletionRequestUc.name, () => { await uc.createDeletionRequest(deletionRequestToCreate); expect(deletionRequestService.createDeletionRequest).toHaveBeenCalledWith( - deletionRequestToCreate.targetRef.itemId, - deletionRequestToCreate.targetRef.domain, + deletionRequestToCreate.targetRef.targetRefId, + deletionRequestToCreate.targetRef.targetRefDoamin, deletionRequestToCreate.deleteInMinutes ); }); @@ -237,7 +237,7 @@ describe(DeletionRequestUc.name, () => { await uc.executeDeletionRequests(); - expect(classService.deleteUserDataFromClasses).toHaveBeenCalledWith(deletionRequestToExecute.itemId); + expect(classService.deleteUserDataFromClasses).toHaveBeenCalledWith(deletionRequestToExecute.targetRefId); }); it('should call courseGroupService.deleteUserDataFromCourseGroup to delete user data in courseGroup module', async () => { @@ -247,7 +247,9 @@ describe(DeletionRequestUc.name, () => { await uc.executeDeletionRequests(); - expect(courseGroupService.deleteUserDataFromCourseGroup).toHaveBeenCalledWith(deletionRequestToExecute.itemId); + expect(courseGroupService.deleteUserDataFromCourseGroup).toHaveBeenCalledWith( + deletionRequestToExecute.targetRefId + ); }); it('should call courseService.deleteUserDataFromCourse to delete user data in course module', async () => { @@ -257,7 +259,7 @@ describe(DeletionRequestUc.name, () => { await uc.executeDeletionRequests(); - expect(courseService.deleteUserDataFromCourse).toHaveBeenCalledWith(deletionRequestToExecute.itemId); + expect(courseService.deleteUserDataFromCourse).toHaveBeenCalledWith(deletionRequestToExecute.targetRefId); }); it('should call filesService.markFilesOwnedByUserForDeletion to mark users files to delete in file module', async () => { @@ -267,7 +269,7 @@ describe(DeletionRequestUc.name, () => { await uc.executeDeletionRequests(); - expect(filesService.markFilesOwnedByUserForDeletion).toHaveBeenCalledWith(deletionRequestToExecute.itemId); + expect(filesService.markFilesOwnedByUserForDeletion).toHaveBeenCalledWith(deletionRequestToExecute.targetRefId); }); it('should call filesService.removeUserPermissionsToAnyFiles to remove users permissions to any files in file module', async () => { @@ -277,7 +279,7 @@ describe(DeletionRequestUc.name, () => { await uc.executeDeletionRequests(); - expect(filesService.removeUserPermissionsToAnyFiles).toHaveBeenCalledWith(deletionRequestToExecute.itemId); + expect(filesService.removeUserPermissionsToAnyFiles).toHaveBeenCalledWith(deletionRequestToExecute.targetRefId); }); it('should call lessonService.deleteUserDataFromLessons to delete users data in lesson module', async () => { @@ -287,7 +289,7 @@ describe(DeletionRequestUc.name, () => { await uc.executeDeletionRequests(); - expect(lessonService.deleteUserDataFromLessons).toHaveBeenCalledWith(deletionRequestToExecute.itemId); + expect(lessonService.deleteUserDataFromLessons).toHaveBeenCalledWith(deletionRequestToExecute.targetRefId); }); it('should call pseudonymService.deleteByUserId to delete users data in pseudonym module', async () => { @@ -297,7 +299,7 @@ describe(DeletionRequestUc.name, () => { await uc.executeDeletionRequests(); - expect(pseudonymService.deleteByUserId).toHaveBeenCalledWith(deletionRequestToExecute.itemId); + expect(pseudonymService.deleteByUserId).toHaveBeenCalledWith(deletionRequestToExecute.targetRefId); }); it('should call teamService.deleteUserDataFromTeams to delete users data in teams module', async () => { @@ -307,7 +309,7 @@ describe(DeletionRequestUc.name, () => { await uc.executeDeletionRequests(); - expect(teamService.deleteUserDataFromTeams).toHaveBeenCalledWith(deletionRequestToExecute.itemId); + expect(teamService.deleteUserDataFromTeams).toHaveBeenCalledWith(deletionRequestToExecute.targetRefId); }); it('should call userService.deleteUsers to delete user in user module', async () => { @@ -317,7 +319,7 @@ describe(DeletionRequestUc.name, () => { await uc.executeDeletionRequests(); - expect(userService.deleteUser).toHaveBeenCalledWith(deletionRequestToExecute.itemId); + expect(userService.deleteUser).toHaveBeenCalledWith(deletionRequestToExecute.targetRefId); }); it('should call deletionLogService.createDeletionLog to create logs for deletionRequest', async () => { 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 8cd1d3d0025..be201054f79 100644 --- a/apps/server/src/modules/deletion/uc/deletion-request.uc.ts +++ b/apps/server/src/modules/deletion/uc/deletion-request.uc.ts @@ -18,7 +18,7 @@ import { DeletionStatusModel } from '../domain/types/deletion-status-model.enum' import { DeletionLog } from '../domain/deletion-log.do'; export interface DeletionRequestLog { - targetRef: { domain: DeletionDomainModel; itemId: EntityId }; + targetRef: { targetRefDoamin: DeletionDomainModel; targetRefId: EntityId }; deletionPlannedAt: Date; statistics?: DeletionLogStatistic[]; } @@ -30,7 +30,7 @@ export interface DeletionLogStatistic { } export interface DeletionRequestProps { - targetRef: { domain: DeletionDomainModel; itemId: EntityId }; + targetRef: { targetRefDoamin: DeletionDomainModel; targetRefId: EntityId }; deleteInMinutes?: number; } @@ -54,8 +54,8 @@ export class DeletionRequestUc { deletionRequest: DeletionRequestProps ): Promise<{ requestId: EntityId; deletionPlannedAt: Date }> { const result = await this.deletionRequestService.createDeletionRequest( - deletionRequest.targetRef.itemId, - deletionRequest.targetRef.domain, + deletionRequest.targetRef.targetRefId, + deletionRequest.targetRef.targetRefDoamin, deletionRequest.deleteInMinutes ); @@ -76,8 +76,8 @@ export class DeletionRequestUc { const deletionRequest: DeletionRequest = await this.deletionRequestService.findById(deletionRequestId); let response: DeletionRequestLog = { targetRef: { - domain: deletionRequest.domain, - itemId: deletionRequest.itemId, + targetRefDoamin: deletionRequest.targetRefDomain, + targetRefId: deletionRequest.targetRefId, }, deletionPlannedAt: deletionRequest.deleteAfter, }; @@ -116,16 +116,16 @@ export class DeletionRequestUc { teamsUpdated, userDeleted, ] = await Promise.all([ - this.accountService.deleteByUserId(deletionRequest.itemId), - this.classService.deleteUserDataFromClasses(deletionRequest.itemId), - this.courseGroupService.deleteUserDataFromCourseGroup(deletionRequest.itemId), - this.courseService.deleteUserDataFromCourse(deletionRequest.itemId), - this.filesService.markFilesOwnedByUserForDeletion(deletionRequest.itemId), - this.filesService.removeUserPermissionsToAnyFiles(deletionRequest.itemId), - this.lessonService.deleteUserDataFromLessons(deletionRequest.itemId), - this.pseudonymService.deleteByUserId(deletionRequest.itemId), - this.teamService.deleteUserDataFromTeams(deletionRequest.itemId), - this.userService.deleteUser(deletionRequest.itemId), + this.accountService.deleteByUserId(deletionRequest.targetRefId), + this.classService.deleteUserDataFromClasses(deletionRequest.targetRefId), + this.courseGroupService.deleteUserDataFromCourseGroup(deletionRequest.targetRefId), + this.courseService.deleteUserDataFromCourse(deletionRequest.targetRefId), + this.filesService.markFilesOwnedByUserForDeletion(deletionRequest.targetRefId), + this.filesService.removeUserPermissionsToAnyFiles(deletionRequest.targetRefId), + this.lessonService.deleteUserDataFromLessons(deletionRequest.targetRefId), + this.pseudonymService.deleteByUserId(deletionRequest.targetRefId), + this.teamService.deleteUserDataFromTeams(deletionRequest.targetRefId), + this.userService.deleteUser(deletionRequest.targetRefId), ]); await this.deletionLogService.createDeletionLog( From 78ed3b930a53a5f041ae54d425acb73d068c1261 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Sat, 4 Nov 2023 14:16:42 +0100 Subject: [PATCH 13/72] add deletion API client with just a single method for now that allows for sending a deletion request --- .../deletion/client/deletion-client.config.ts | 13 ++ .../deletion/client/deletion.client.spec.ts | 113 ++++++++++++++++++ .../deletion/client/deletion.client.ts | 49 ++++++++ .../src/modules/deletion/client/index.ts | 3 + .../deletion-request-input.interface.ts | 9 ++ .../deletion-request-output.interface.ts | 4 + .../deletion/client/interface/index.ts | 2 + config/default.schema.json | 10 ++ 8 files changed, 203 insertions(+) create mode 100644 apps/server/src/modules/deletion/client/deletion-client.config.ts create mode 100644 apps/server/src/modules/deletion/client/deletion.client.spec.ts create mode 100644 apps/server/src/modules/deletion/client/deletion.client.ts create mode 100644 apps/server/src/modules/deletion/client/index.ts create mode 100644 apps/server/src/modules/deletion/client/interface/deletion-request-input.interface.ts create mode 100644 apps/server/src/modules/deletion/client/interface/deletion-request-output.interface.ts create mode 100644 apps/server/src/modules/deletion/client/interface/index.ts diff --git a/apps/server/src/modules/deletion/client/deletion-client.config.ts b/apps/server/src/modules/deletion/client/deletion-client.config.ts new file mode 100644 index 00000000000..366f53dc875 --- /dev/null +++ b/apps/server/src/modules/deletion/client/deletion-client.config.ts @@ -0,0 +1,13 @@ +import { Configuration } from '@hpi-schul-cloud/commons/lib'; + +export interface DeletionClientConfig { + ADMIN_API_BASE_URL: string; + ADMIN_API_KEY: string; +} + +const deletionClientConfig = { + ADMIN_API_BASE_URL: Configuration.get('ADMIN_API_BASE_URL') as string, + ADMIN_API_KEY: Configuration.get('ADMIN_API_KEY') as string, +}; + +export const config = () => {}; diff --git a/apps/server/src/modules/deletion/client/deletion.client.spec.ts b/apps/server/src/modules/deletion/client/deletion.client.spec.ts new file mode 100644 index 00000000000..ff1f8e1ebbb --- /dev/null +++ b/apps/server/src/modules/deletion/client/deletion.client.spec.ts @@ -0,0 +1,113 @@ +import { of } from 'rxjs'; +import { AxiosResponse } from 'axios'; +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { axiosResponseFactory } from '@shared/testing'; +import { DeletionRequestOutput } from './interface'; +import { DeletionClient } from './deletion.client'; + +describe(DeletionClient.name, () => { + let module: TestingModule; + let client: DeletionClient; + let httpService: DeepMocked; + + beforeEach(async () => { + module = await Test.createTestingModule({ + providers: [ + DeletionClient, + { + provide: ConfigService, + useValue: createMock({ + get: jest.fn((key: string) => { + if (key === 'ADMIN_API_BASE_URL') { + return 'http://localhost:8080'; + } + + return '6b3df003-61e9-467c-9e6b-579634801896'; + }), + }), + }, + { + provide: HttpService, + useValue: createMock(), + }, + ], + }).compile(); + + client = module.get(DeletionClient); + httpService = module.get(HttpService); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('queueDeletionRequest', () => { + describe('when received valid response with expected HTTP status code', () => { + const setup = () => { + const input = { + targetRef: { + domain: 'user', + id: '652f1625e9bc1a13bdaae48b', + }, + }; + + const output: DeletionRequestOutput = { + requestId: '6536ce29b595d7c8e5faf200', + deletionPlannedAt: new Date('2024-10-15T12:42:50.521Z'), + }; + + const response: AxiosResponse = axiosResponseFactory.build({ + data: output, + status: 202, + }); + + httpService.post.mockReturnValueOnce(of(response)); + + return { input, output }; + }; + + it('should return proper output', async () => { + const { input, output } = setup(); + + const result = await client.queueDeletionRequest(input); + + expect(result).toEqual(output); + }); + }); + + describe('when received invalid HTTP status code in a response', () => { + const setup = () => { + const input = { + targetRef: { + domain: 'user', + id: '652f1625e9bc1a13bdaae48b', + }, + }; + + const output: DeletionRequestOutput = { requestId: '', deletionPlannedAt: new Date() }; + + const response: AxiosResponse = axiosResponseFactory.build({ + data: output, + status: 200, + }); + + httpService.post.mockReturnValueOnce(of(response)); + + return { input }; + }; + + it('should throw an exception', async () => { + const { input } = setup(); + + await expect(client.queueDeletionRequest(input)).rejects.toThrow(Error); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/client/deletion.client.ts b/apps/server/src/modules/deletion/client/deletion.client.ts new file mode 100644 index 00000000000..d4b59b740a1 --- /dev/null +++ b/apps/server/src/modules/deletion/client/deletion.client.ts @@ -0,0 +1,49 @@ +import { Injectable } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { firstValueFrom } from 'rxjs'; +import { AxiosResponse } from 'axios'; +import { DeletionRequestInput, DeletionRequestOutput } from './interface'; +import { ConfigService } from '@nestjs/config'; +import { DeletionClientConfig } from './deletion-client.config'; + +@Injectable() +export class DeletionClient { + constructor( + private readonly httpService: HttpService, + private readonly configService: ConfigService + ) {} + + async queueDeletionRequest(input: DeletionRequestInput): Promise { + const url = this.endpointUrl('/admin/api/v1/deletionRequests'); + + const request = this.httpService.post(url.toString(), input, { + headers: this.apiKeyHeader(), + }); + + const expectedStatus = 202; + + return firstValueFrom(request) + .then((resp: AxiosResponse) => { + if (resp.status !== expectedStatus) { + throw new Error( + `invalid HTTP status code in a response from the server - ${resp.status} instead of ${expectedStatus}` + ); + } + + return resp.data; + }) + .catch((err) => { + throw new Error(`failed to send a deletion request: ${err}`); + }); + } + + private endpointUrl(endpoint: string): URL { + const baseUrl = this.configService.get('ADMIN_API_BASE_URL'); + + return new URL(endpoint, baseUrl); + } + + private apiKeyHeader() { + return { 'X-Api-Key': this.configService.get('ADMIN_API_KEY') }; + } +} diff --git a/apps/server/src/modules/deletion/client/index.ts b/apps/server/src/modules/deletion/client/index.ts new file mode 100644 index 00000000000..eae21ec4de4 --- /dev/null +++ b/apps/server/src/modules/deletion/client/index.ts @@ -0,0 +1,3 @@ +export * from './interface/deletion-request-input.interface'; +export * from './interface/deletion-request-output.interface'; +export * from './deletion.client'; diff --git a/apps/server/src/modules/deletion/client/interface/deletion-request-input.interface.ts b/apps/server/src/modules/deletion/client/interface/deletion-request-input.interface.ts new file mode 100644 index 00000000000..f8fc9a7eca1 --- /dev/null +++ b/apps/server/src/modules/deletion/client/interface/deletion-request-input.interface.ts @@ -0,0 +1,9 @@ +export interface DeletionRequestTargetRefInput { + domain: string; + id: string; +} + +export interface DeletionRequestInput { + targetRef: DeletionRequestTargetRefInput; + deleteInMinutes?: number; +} diff --git a/apps/server/src/modules/deletion/client/interface/deletion-request-output.interface.ts b/apps/server/src/modules/deletion/client/interface/deletion-request-output.interface.ts new file mode 100644 index 00000000000..b61a372d5a5 --- /dev/null +++ b/apps/server/src/modules/deletion/client/interface/deletion-request-output.interface.ts @@ -0,0 +1,4 @@ +export interface DeletionRequestOutput { + requestId: string; + deletionPlannedAt: Date; +} diff --git a/apps/server/src/modules/deletion/client/interface/index.ts b/apps/server/src/modules/deletion/client/interface/index.ts new file mode 100644 index 00000000000..2005003d049 --- /dev/null +++ b/apps/server/src/modules/deletion/client/interface/index.ts @@ -0,0 +1,2 @@ +export * from './deletion-request-input.interface'; +export * from './deletion-request-output.interface'; diff --git a/config/default.schema.json b/config/default.schema.json index 2f61736b187..8f963023ea2 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -1343,6 +1343,16 @@ } } }, + "ADMIN_API_BASE_URL": { + "type": "string", + "default": "", + "description": "Admin API base URL." + }, + "ADMIN_API_KEY": { + "type": "string", + "default": "", + "description": "Admin API key." + }, "BATCH_DELETION": { "type": "object", "description": "Configuration for the \"batch deletion\" application.", From 3acf266976ab6011b98c5717ad3f5bb9b16d7443 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Sat, 4 Nov 2023 14:32:00 +0100 Subject: [PATCH 14/72] refactor the env vars for configurting the Admin API --- .../deletion/client/deletion-client.config.ts | 6 ++--- .../deletion/client/deletion.client.spec.ts | 1 + .../deletion/client/deletion.client.ts | 2 +- config/default.schema.json | 24 ++++++++++++------- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/apps/server/src/modules/deletion/client/deletion-client.config.ts b/apps/server/src/modules/deletion/client/deletion-client.config.ts index 366f53dc875..4438f1f7fe4 100644 --- a/apps/server/src/modules/deletion/client/deletion-client.config.ts +++ b/apps/server/src/modules/deletion/client/deletion-client.config.ts @@ -2,12 +2,12 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; export interface DeletionClientConfig { ADMIN_API_BASE_URL: string; - ADMIN_API_KEY: string; + ADMIN_API_API_KEY: string; } const deletionClientConfig = { - ADMIN_API_BASE_URL: Configuration.get('ADMIN_API_BASE_URL') as string, - ADMIN_API_KEY: Configuration.get('ADMIN_API_KEY') as string, + ADMIN_API_BASE_URL: Configuration.get('ADMIN_API__BASE_URL') as string, + ADMIN_API_API_KEY: Configuration.get('ADMIN_API__API_KEY') as string, }; export const config = () => {}; diff --git a/apps/server/src/modules/deletion/client/deletion.client.spec.ts b/apps/server/src/modules/deletion/client/deletion.client.spec.ts index ff1f8e1ebbb..a01e3d698fd 100644 --- a/apps/server/src/modules/deletion/client/deletion.client.spec.ts +++ b/apps/server/src/modules/deletion/client/deletion.client.spec.ts @@ -25,6 +25,7 @@ describe(DeletionClient.name, () => { return 'http://localhost:8080'; } + // Default is for the Admin APIs API Key. return '6b3df003-61e9-467c-9e6b-579634801896'; }), }), diff --git a/apps/server/src/modules/deletion/client/deletion.client.ts b/apps/server/src/modules/deletion/client/deletion.client.ts index d4b59b740a1..1d20eecab80 100644 --- a/apps/server/src/modules/deletion/client/deletion.client.ts +++ b/apps/server/src/modules/deletion/client/deletion.client.ts @@ -44,6 +44,6 @@ export class DeletionClient { } private apiKeyHeader() { - return { 'X-Api-Key': this.configService.get('ADMIN_API_KEY') }; + return { 'X-Api-Key': this.configService.get('ADMIN_API_API_KEY') }; } } diff --git a/config/default.schema.json b/config/default.schema.json index 8f963023ea2..0f3c6562e4d 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -1343,15 +1343,21 @@ } } }, - "ADMIN_API_BASE_URL": { - "type": "string", - "default": "", - "description": "Admin API base URL." - }, - "ADMIN_API_KEY": { - "type": "string", - "default": "", - "description": "Admin API key." + "ADMIN_API": { + "type": "object", + "description": "Configuration of the schulcloud-server's admin API.", + "required": ["API_KEY"], + "properties": { + "BASE_URL": { + "type": "string", + "default": "http://localhost:4030", + "description": "Base URL of the Admin API." + }, + "API_KEY": { + "type": "string", + "description": "API key for accessing the Admin API." + } + } }, "BATCH_DELETION": { "type": "object", From f2ac1bc88f431f8dfaee9281f4c133feceb1af4d Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Sat, 4 Nov 2023 15:13:26 +0100 Subject: [PATCH 15/72] add exporting DeletionClientConfig --- apps/server/src/modules/deletion/client/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/server/src/modules/deletion/client/index.ts b/apps/server/src/modules/deletion/client/index.ts index eae21ec4de4..9aa334f0034 100644 --- a/apps/server/src/modules/deletion/client/index.ts +++ b/apps/server/src/modules/deletion/client/index.ts @@ -1,3 +1,4 @@ export * from './interface/deletion-request-input.interface'; export * from './interface/deletion-request-output.interface'; +export * from './deletion-client.config'; export * from './deletion.client'; From b69741a2382a3a77fefd7cb91f3dc5cda646cfb3 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Sat, 4 Nov 2023 15:15:11 +0100 Subject: [PATCH 16/72] move references service to the deletion module --- apps/server/src/modules/deletion/services/index.ts | 1 + .../service => deletion/services}/references.service.spec.ts | 0 .../service => deletion/services}/references.service.ts | 0 3 files changed, 1 insertion(+) rename apps/server/src/modules/{batch-deletion/service => deletion/services}/references.service.spec.ts (100%) rename apps/server/src/modules/{batch-deletion/service => deletion/services}/references.service.ts (100%) diff --git a/apps/server/src/modules/deletion/services/index.ts b/apps/server/src/modules/deletion/services/index.ts index 9661354718c..374704ca311 100644 --- a/apps/server/src/modules/deletion/services/index.ts +++ b/apps/server/src/modules/deletion/services/index.ts @@ -1 +1,2 @@ export * from './deletion-request.service'; +export * from './references.service'; diff --git a/apps/server/src/modules/batch-deletion/service/references.service.spec.ts b/apps/server/src/modules/deletion/services/references.service.spec.ts similarity index 100% rename from apps/server/src/modules/batch-deletion/service/references.service.spec.ts rename to apps/server/src/modules/deletion/services/references.service.spec.ts diff --git a/apps/server/src/modules/batch-deletion/service/references.service.ts b/apps/server/src/modules/deletion/services/references.service.ts similarity index 100% rename from apps/server/src/modules/batch-deletion/service/references.service.ts rename to apps/server/src/modules/deletion/services/references.service.ts From 0c02d6de5deadbf781435260856c473f8f084995 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Sat, 4 Nov 2023 15:16:33 +0100 Subject: [PATCH 17/72] delete unused code --- apps/server/src/modules/batch-deletion/service/index.ts | 1 - 1 file changed, 1 deletion(-) delete mode 100644 apps/server/src/modules/batch-deletion/service/index.ts diff --git a/apps/server/src/modules/batch-deletion/service/index.ts b/apps/server/src/modules/batch-deletion/service/index.ts deleted file mode 100644 index 18d874794c5..00000000000 --- a/apps/server/src/modules/batch-deletion/service/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './references.service'; From 30412f9954980cae5051ad77a297d2a11103dccf Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Sat, 4 Nov 2023 15:35:37 +0100 Subject: [PATCH 18/72] add batch deletion service that makes it possible ot queue deletion for many references at once --- .../services/batch-deletion.service.ts | 46 +++++++++++++++++++ .../src/modules/deletion/services/index.ts | 1 + 2 files changed, 47 insertions(+) create mode 100644 apps/server/src/modules/deletion/services/batch-deletion.service.ts diff --git a/apps/server/src/modules/deletion/services/batch-deletion.service.ts b/apps/server/src/modules/deletion/services/batch-deletion.service.ts new file mode 100644 index 00000000000..a42b86a9145 --- /dev/null +++ b/apps/server/src/modules/deletion/services/batch-deletion.service.ts @@ -0,0 +1,46 @@ +import { Injectable } from '@nestjs/common'; +import { DeletionClient } from '@modules/deletion/client'; + +export interface QueueDeletionRequestInput { + targetRefDomain: string; + targetRefId: string; + deleteInMinutes: number; +} + +export interface QueueDeletionRequestOutput { + requestId?: string; + deletionPlannedAt?: Date; + error?: string; +} + +@Injectable() +export class BatchDeletionService { + constructor(private readonly deletionClient: DeletionClient) {} + + queueDeletionRequests(inputs: QueueDeletionRequestInput[]): QueueDeletionRequestOutput[] { + const outputs: QueueDeletionRequestOutput[] = []; + + inputs.forEach(async (input) => { + const deletionRequestInput = { + targetRef: { + domain: input.targetRefDomain, + id: input.targetRefId, + }, + deleteInMinutes: input.deleteInMinutes, + }; + + try { + const deletionRequestOutput = await this.deletionClient.queueDeletionRequest(deletionRequestInput); + + outputs.push({ + requestId: deletionRequestOutput.requestId, + deletionPlannedAt: deletionRequestOutput.deletionPlannedAt, + }); + } catch (err) { + outputs.push({ error: err }); + } + }); + + return outputs; + } +} diff --git a/apps/server/src/modules/deletion/services/index.ts b/apps/server/src/modules/deletion/services/index.ts index 374704ca311..799f31615b5 100644 --- a/apps/server/src/modules/deletion/services/index.ts +++ b/apps/server/src/modules/deletion/services/index.ts @@ -1,2 +1,3 @@ export * from './deletion-request.service'; export * from './references.service'; +export * from './batch-deletion.service'; From 13f7b576a0ec7f8b5c6a2a3a19f1a3ff1ff6246f Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Sat, 4 Nov 2023 15:38:57 +0100 Subject: [PATCH 19/72] move some parts of the interface to the interface subdir --- apps/server/src/modules/deletion/services/index.ts | 1 + apps/server/src/modules/deletion/services/interface/index.ts | 2 ++ .../interface/queue-deletion-request-input.interface.ts | 5 +++++ .../interface/queue-deletion-request-output.interface.ts | 5 +++++ 4 files changed, 13 insertions(+) create mode 100644 apps/server/src/modules/deletion/services/interface/index.ts create mode 100644 apps/server/src/modules/deletion/services/interface/queue-deletion-request-input.interface.ts create mode 100644 apps/server/src/modules/deletion/services/interface/queue-deletion-request-output.interface.ts diff --git a/apps/server/src/modules/deletion/services/index.ts b/apps/server/src/modules/deletion/services/index.ts index 799f31615b5..dce7ed21fee 100644 --- a/apps/server/src/modules/deletion/services/index.ts +++ b/apps/server/src/modules/deletion/services/index.ts @@ -1,3 +1,4 @@ +export * from './interface'; export * from './deletion-request.service'; export * from './references.service'; export * from './batch-deletion.service'; diff --git a/apps/server/src/modules/deletion/services/interface/index.ts b/apps/server/src/modules/deletion/services/interface/index.ts new file mode 100644 index 00000000000..8a455440798 --- /dev/null +++ b/apps/server/src/modules/deletion/services/interface/index.ts @@ -0,0 +1,2 @@ +export * from './queue-deletion-request-input.interface'; +export * from './queue-deletion-request-output.interface'; diff --git a/apps/server/src/modules/deletion/services/interface/queue-deletion-request-input.interface.ts b/apps/server/src/modules/deletion/services/interface/queue-deletion-request-input.interface.ts new file mode 100644 index 00000000000..b421943bce9 --- /dev/null +++ b/apps/server/src/modules/deletion/services/interface/queue-deletion-request-input.interface.ts @@ -0,0 +1,5 @@ +export interface QueueDeletionRequestInput { + targetRefDomain: string; + targetRefId: string; + deleteInMinutes: number; +} diff --git a/apps/server/src/modules/deletion/services/interface/queue-deletion-request-output.interface.ts b/apps/server/src/modules/deletion/services/interface/queue-deletion-request-output.interface.ts new file mode 100644 index 00000000000..375ff811857 --- /dev/null +++ b/apps/server/src/modules/deletion/services/interface/queue-deletion-request-output.interface.ts @@ -0,0 +1,5 @@ +export interface QueueDeletionRequestOutput { + requestId?: string; + deletionPlannedAt?: Date; + error?: string; +} From 61a08149b128369738141a6e36d8294c79d10333 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Sat, 4 Nov 2023 15:40:19 +0100 Subject: [PATCH 20/72] add an interface for the batch deletion summary --- .../uc/interface/batch-deletion-summary.interface.ts | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 apps/server/src/modules/deletion/uc/interface/batch-deletion-summary.interface.ts diff --git a/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary.interface.ts b/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary.interface.ts new file mode 100644 index 00000000000..11faf68fdf2 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary.interface.ts @@ -0,0 +1,7 @@ +import { QueueDeletionRequestOutput } from '../../services'; + +export interface BatchDeletionSummary { + SuccessCount: number; + FailureCount: number; + Details: QueueDeletionRequestOutput[]; +} From 6c72715a244d2d4c657c450e8a71688d22e73038 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Sat, 4 Nov 2023 16:06:17 +0100 Subject: [PATCH 21/72] move some interfaces to a separate subdir --- .../deletion/services/batch-deletion.service.ts | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/apps/server/src/modules/deletion/services/batch-deletion.service.ts b/apps/server/src/modules/deletion/services/batch-deletion.service.ts index a42b86a9145..612432ffbb5 100644 --- a/apps/server/src/modules/deletion/services/batch-deletion.service.ts +++ b/apps/server/src/modules/deletion/services/batch-deletion.service.ts @@ -1,17 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { DeletionClient } from '@modules/deletion/client'; - -export interface QueueDeletionRequestInput { - targetRefDomain: string; - targetRefId: string; - deleteInMinutes: number; -} - -export interface QueueDeletionRequestOutput { - requestId?: string; - deletionPlannedAt?: Date; - error?: string; -} +import { DeletionClient } from '../client'; +import { QueueDeletionRequestInput, QueueDeletionRequestOutput } from './interface'; @Injectable() export class BatchDeletionService { From 46395c083db4ecf904241bed9d46744aaa9e8350 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Sat, 4 Nov 2023 16:06:50 +0100 Subject: [PATCH 22/72] refactor the batch deletion summary interface --- .../uc/interface/batch-deletion-summary.interface.ts | 6 +++--- apps/server/src/modules/deletion/uc/interface/index.ts | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 apps/server/src/modules/deletion/uc/interface/index.ts diff --git a/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary.interface.ts b/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary.interface.ts index 11faf68fdf2..6b652c20278 100644 --- a/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary.interface.ts +++ b/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary.interface.ts @@ -1,7 +1,7 @@ import { QueueDeletionRequestOutput } from '../../services'; export interface BatchDeletionSummary { - SuccessCount: number; - FailureCount: number; - Details: QueueDeletionRequestOutput[]; + successCount: number; + failureCount: number; + details: QueueDeletionRequestOutput[]; } diff --git a/apps/server/src/modules/deletion/uc/interface/index.ts b/apps/server/src/modules/deletion/uc/interface/index.ts new file mode 100644 index 00000000000..57c633a76e4 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/interface/index.ts @@ -0,0 +1 @@ +export * from './batch-deletion-summary.interface'; From f20422037e5a38279da074be8c3aa0d7f91b29d4 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Sat, 4 Nov 2023 16:07:09 +0100 Subject: [PATCH 23/72] add uc for the batch deletion --- .../modules/deletion/uc/batch-deletion.uc.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 apps/server/src/modules/deletion/uc/batch-deletion.uc.ts diff --git a/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts b/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts new file mode 100644 index 00000000000..4128de05235 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@nestjs/common'; +import { ReferencesService, BatchDeletionService, QueueDeletionRequestInput } from '../services'; +import { BatchDeletionSummary } from './interface'; + +@Injectable() +export class BatchDeletionUc { + constructor( + private readonly referencesService: ReferencesService, + private readonly batchDeletionService: BatchDeletionService + ) {} + + deleteRefsFromTxtFile(filePath: string): BatchDeletionSummary { + const refsFromTxtFile = ReferencesService.loadFromTxtFile(filePath); + + const inputs: QueueDeletionRequestInput[] = []; + + refsFromTxtFile.forEach((ref) => + inputs.push({ + targetRefDomain: 'user', + targetRefId: ref, + deleteInMinutes: 60, + }) + ); + + const outputs = this.batchDeletionService.queueDeletionRequests(inputs); + + const summary: BatchDeletionSummary = { + successCount: 0, + failureCount: 0, + details: outputs, + }; + + outputs.forEach((output) => { + if (output.error !== undefined) { + summary.failureCount += 1; + } else { + summary.successCount += 1; + } + }); + + return summary; + } +} From 3ecc282715e8c12a75a6a4e4b688cfc5bd60a0be Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Tue, 7 Nov 2023 00:21:06 +0100 Subject: [PATCH 24/72] remove unused annotation --- apps/server/src/modules/deletion/services/references.service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/server/src/modules/deletion/services/references.service.ts b/apps/server/src/modules/deletion/services/references.service.ts index 0224283a28a..7706f11fee5 100644 --- a/apps/server/src/modules/deletion/services/references.service.ts +++ b/apps/server/src/modules/deletion/services/references.service.ts @@ -1,7 +1,5 @@ import fs from 'fs'; -import { Injectable } from '@nestjs/common'; -@Injectable() export class ReferencesService { static loadFromTxtFile(filePath: string): string[] { let fileContent = fs.readFileSync(filePath).toString(); From 78dedbf6a218d9affcdf59e0ad0ccc392b462e0a Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Tue, 7 Nov 2023 00:46:33 +0100 Subject: [PATCH 25/72] refactor deletion client implementation --- .../deletion/client/deletion-client.config.ts | 4 +- .../deletion/client/deletion.client.spec.ts | 2 +- .../deletion/client/deletion.client.ts | 61 ++++++++++++------- 3 files changed, 42 insertions(+), 25 deletions(-) diff --git a/apps/server/src/modules/deletion/client/deletion-client.config.ts b/apps/server/src/modules/deletion/client/deletion-client.config.ts index 4438f1f7fe4..b880cf785cd 100644 --- a/apps/server/src/modules/deletion/client/deletion-client.config.ts +++ b/apps/server/src/modules/deletion/client/deletion-client.config.ts @@ -5,9 +5,9 @@ export interface DeletionClientConfig { ADMIN_API_API_KEY: string; } -const deletionClientConfig = { +const config: DeletionClientConfig = { ADMIN_API_BASE_URL: Configuration.get('ADMIN_API__BASE_URL') as string, ADMIN_API_API_KEY: Configuration.get('ADMIN_API__API_KEY') as string, }; -export const config = () => {}; +export const deletionClientConfig = () => config; diff --git a/apps/server/src/modules/deletion/client/deletion.client.spec.ts b/apps/server/src/modules/deletion/client/deletion.client.spec.ts index a01e3d698fd..a8481b3d6ef 100644 --- a/apps/server/src/modules/deletion/client/deletion.client.spec.ts +++ b/apps/server/src/modules/deletion/client/deletion.client.spec.ts @@ -21,7 +21,7 @@ describe(DeletionClient.name, () => { provide: ConfigService, useValue: createMock({ get: jest.fn((key: string) => { - if (key === 'ADMIN_API_BASE_URL') { + if (key === 'ADMIN_API__BASE_URL') { return 'http://localhost:8080'; } diff --git a/apps/server/src/modules/deletion/client/deletion.client.ts b/apps/server/src/modules/deletion/client/deletion.client.ts index 1d20eecab80..93830ee72fd 100644 --- a/apps/server/src/modules/deletion/client/deletion.client.ts +++ b/apps/server/src/modules/deletion/client/deletion.client.ts @@ -1,49 +1,66 @@ -import { Injectable } from '@nestjs/common'; -import { HttpService } from '@nestjs/axios'; import { firstValueFrom } from 'rxjs'; import { AxiosResponse } from 'axios'; -import { DeletionRequestInput, DeletionRequestOutput } from './interface'; +import { Injectable } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; import { ConfigService } from '@nestjs/config'; +import { DeletionRequestInput, DeletionRequestOutput } from './interface'; import { DeletionClientConfig } from './deletion-client.config'; @Injectable() export class DeletionClient { + private readonly baseUrl: string; + private readonly apiKey: string; + + private readonly postDeletionRequestsEndpoint: string; + constructor( private readonly httpService: HttpService, private readonly configService: ConfigService - ) {} + ) { + this.baseUrl = this.configService.get('ADMIN_API_BASE_URL'); + this.apiKey = this.configService.get('ADMIN_API_API_KEY'); - async queueDeletionRequest(input: DeletionRequestInput): Promise { - const url = this.endpointUrl('/admin/api/v1/deletionRequests'); - - const request = this.httpService.post(url.toString(), input, { - headers: this.apiKeyHeader(), - }); + // Prepare the POST /deletionRequests endpoint beforehand to not do it on every client call. + this.postDeletionRequestsEndpoint = new URL('/admin/api/v1/deletionRequests', this.baseUrl).toString(); + } - const expectedStatus = 202; + async queueDeletionRequest(input: DeletionRequestInput): Promise { + const request = this.httpService.post(this.postDeletionRequestsEndpoint, input, this.headers()); return firstValueFrom(request) .then((resp: AxiosResponse) => { - if (resp.status !== expectedStatus) { - throw new Error( - `invalid HTTP status code in a response from the server - ${resp.status} instead of ${expectedStatus}` - ); + // Throw an error if any other status code (other than expected "202 Accepted" is returned). + if (resp.status !== 202) { + throw new Error(`invalid HTTP status code in a response from the server - ${resp.status} instead of 202`); + } + + // Throw an error if server didn't return a requestId in a response (and it is + // required as it gives client the reference to the created deletion request). + if (!resp.data.requestId) { + throw new Error('no valid requestId returned from the server'); + } + + // Throw an error if server didn't return a deletionPlannedAt timestamp so the user + // will not be aware after which date the deletion request's execution will begin. + if (!resp.data.deletionPlannedAt) { + throw new Error('no valid deletionPlannedAt returned from the server'); } return resp.data; }) .catch((err) => { - throw new Error(`failed to send a deletion request: ${err}`); + // Throw an error if sending/processing deletion request by the client failed in any way. + throw new Error(`failed to send/process a deletion request: ${err}`); }); } - private endpointUrl(endpoint: string): URL { - const baseUrl = this.configService.get('ADMIN_API_BASE_URL'); - - return new URL(endpoint, baseUrl); + private apiKeyHeader() { + return { 'X-Api-Key': this.apiKey }; } - private apiKeyHeader() { - return { 'X-Api-Key': this.configService.get('ADMIN_API_API_KEY') }; + private headers() { + return { + headers: this.apiKeyHeader(), + }; } } From 6c66cf663fcc79b9c301a743718327388fa940f7 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Tue, 7 Nov 2023 00:52:17 +0100 Subject: [PATCH 26/72] add batch deletion service implementation --- .../deletion/services/batch-deletion.service.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/server/src/modules/deletion/services/batch-deletion.service.ts b/apps/server/src/modules/deletion/services/batch-deletion.service.ts index 612432ffbb5..72a4b6a6fd7 100644 --- a/apps/server/src/modules/deletion/services/batch-deletion.service.ts +++ b/apps/server/src/modules/deletion/services/batch-deletion.service.ts @@ -6,10 +6,14 @@ import { QueueDeletionRequestInput, QueueDeletionRequestOutput } from './interfa export class BatchDeletionService { constructor(private readonly deletionClient: DeletionClient) {} - queueDeletionRequests(inputs: QueueDeletionRequestInput[]): QueueDeletionRequestOutput[] { + async queueDeletionRequests(inputs: QueueDeletionRequestInput[]): Promise { const outputs: QueueDeletionRequestOutput[] = []; - inputs.forEach(async (input) => { + // For every provided deletion request input, try to queue it via deletion client. + // In any case, add the result of the trial to the outputs - it will be either a valid + // response in a form of a requestId + deletionPlannedAt values pair or some error + // returned from the client. In any case, every input should be processed. + for (const input of inputs) { const deletionRequestInput = { targetRef: { domain: input.targetRefDomain, @@ -21,14 +25,17 @@ export class BatchDeletionService { try { const deletionRequestOutput = await this.deletionClient.queueDeletionRequest(deletionRequestInput); + // In case of a successful client response, add the + // requestId + deletionPlannedAt values pair to the outputs. outputs.push({ requestId: deletionRequestOutput.requestId, deletionPlannedAt: deletionRequestOutput.deletionPlannedAt, }); } catch (err) { - outputs.push({ error: err }); + // In case of a failure client response, add the full error message to the outputs. + outputs.push({ error: err.toString() }); } - }); + } return outputs; } From 1c8cae936e5c112ad8995745e81280556507023d Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Tue, 7 Nov 2023 01:08:27 +0100 Subject: [PATCH 27/72] add UC for the batch deletion --- .../modules/deletion/uc/batch-deletion.uc.ts | 53 ++++++++++++++----- apps/server/src/modules/deletion/uc/index.ts | 1 + .../batch-deletion-summary.interface.ts | 10 +++- 3 files changed, 49 insertions(+), 15 deletions(-) create mode 100644 apps/server/src/modules/deletion/uc/index.ts diff --git a/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts b/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts index 4128de05235..079e28aca46 100644 --- a/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts +++ b/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts @@ -4,39 +4,66 @@ import { BatchDeletionSummary } from './interface'; @Injectable() export class BatchDeletionUc { - constructor( - private readonly referencesService: ReferencesService, - private readonly batchDeletionService: BatchDeletionService - ) {} + constructor(private readonly batchDeletionService: BatchDeletionService) {} - deleteRefsFromTxtFile(filePath: string): BatchDeletionSummary { - const refsFromTxtFile = ReferencesService.loadFromTxtFile(filePath); + async deleteRefsFromTxtFile( + refsFilePath: string, + targetRefDomain: string = 'user', + deleteInMinutes: number = 43200 // 43200 minutes = 720 hours = 30 days + ): Promise { + // First, load all the references from the provided text file (with given path). + const refsFromTxtFile = ReferencesService.loadFromTxtFile(refsFilePath); const inputs: QueueDeletionRequestInput[] = []; + // For each reference found in a given file, add it to the inputs + // array (with added targetRefDomain and deleteInMinutes fields). refsFromTxtFile.forEach((ref) => inputs.push({ - targetRefDomain: 'user', targetRefId: ref, - deleteInMinutes: 60, + targetRefDomain: targetRefDomain, + deleteInMinutes: deleteInMinutes, }) ); - const outputs = this.batchDeletionService.queueDeletionRequests(inputs); + const outputs = await this.batchDeletionService.queueDeletionRequests(inputs); + + // Throw an error if the returned outputs number doesn't match the returned inputs number. + if (outputs.length !== inputs.length) { + throw new Error( + 'invalid result from the batch deletion service - expected to ' + + 'receive the same amount of outputs as the provided inputs, ' + + `instead received ${outputs.length} outputs for ${inputs.length} inputs` + ); + } const summary: BatchDeletionSummary = { + overallStatus: 'failure', successCount: 0, failureCount: 0, - details: outputs, + details: [], }; - outputs.forEach((output) => { - if (output.error !== undefined) { + // Go through every received output and, in case of an error presence increase + // a failure count or, in case of no error, increase a success count. + for (let i = 0; i < outputs.length; i++) { + if (outputs[i].error) { summary.failureCount += 1; } else { summary.successCount += 1; } - }); + + // Also add all the processed inputs and outputs details to the overall summary. + summary.details.push({ + input: inputs[i], + output: outputs[i], + }); + } + + // If no failure has been spotted, assume an overall success. + if (summary.failureCount === 0) { + summary.overallStatus = 'success'; + } return summary; } diff --git a/apps/server/src/modules/deletion/uc/index.ts b/apps/server/src/modules/deletion/uc/index.ts new file mode 100644 index 00000000000..180609470b9 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/index.ts @@ -0,0 +1 @@ +export * from './batch-deletion.uc'; diff --git a/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary.interface.ts b/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary.interface.ts index 6b652c20278..49625cd6cbf 100644 --- a/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary.interface.ts +++ b/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary.interface.ts @@ -1,7 +1,13 @@ -import { QueueDeletionRequestOutput } from '../../services'; +import { QueueDeletionRequestInput, QueueDeletionRequestOutput } from '../../services'; + +export interface BatchDeletionSummaryDetail { + input: QueueDeletionRequestInput; + output: QueueDeletionRequestOutput; +} export interface BatchDeletionSummary { + overallStatus: string; successCount: number; failureCount: number; - details: QueueDeletionRequestOutput[]; + details: BatchDeletionSummaryDetail[]; } From 90aca1973d25d6f57356df00dc0eeb7705c63c17 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Tue, 7 Nov 2023 01:30:43 +0100 Subject: [PATCH 28/72] add a console app for the deletion module and a console command to manage deletion requests queue --- .../src/modules/deletion/console/console.ts | 23 ++++++++++ .../console/deletion-console.module.ts | 21 +++++++++ .../console/deletion-queue.console.ts | 45 +++++++++++++++++++ apps/server/src/modules/deletion/index.ts | 1 + nest-cli.json | 9 ++++ package.json | 3 ++ 6 files changed, 102 insertions(+) create mode 100644 apps/server/src/modules/deletion/console/console.ts create mode 100644 apps/server/src/modules/deletion/console/deletion-console.module.ts create mode 100644 apps/server/src/modules/deletion/console/deletion-queue.console.ts diff --git a/apps/server/src/modules/deletion/console/console.ts b/apps/server/src/modules/deletion/console/console.ts new file mode 100644 index 00000000000..12c6ab6bb33 --- /dev/null +++ b/apps/server/src/modules/deletion/console/console.ts @@ -0,0 +1,23 @@ +import { BootstrapConsole } from 'nestjs-console'; +import { DeletionConsoleModule } from './deletion-console.module'; + +const bootstrap = new BootstrapConsole({ + module: DeletionConsoleModule, + useDecorators: true, +}); + +void bootstrap.init().then(async (app) => { + try { + await app.init(); + + await bootstrap.boot(); + + await app.close(); + } catch (e) { + console.error(e); + + await app.close(); + + process.exitCode = 1; + } +}); diff --git a/apps/server/src/modules/deletion/console/deletion-console.module.ts b/apps/server/src/modules/deletion/console/deletion-console.module.ts new file mode 100644 index 00000000000..e16814f1266 --- /dev/null +++ b/apps/server/src/modules/deletion/console/deletion-console.module.ts @@ -0,0 +1,21 @@ +import { Module } from '@nestjs/common'; +import { HttpModule } from '@nestjs/axios'; +import { ConfigModule } from '@nestjs/config'; +import { ConsoleModule } from 'nestjs-console'; +import { ConsoleWriterModule } from '@shared/infra/console'; +import { createConfigModuleOptions } from '@src/config'; +import { DeletionClient, deletionClientConfig } from '../client'; +import { BatchDeletionService } from '../services'; +import { BatchDeletionUc } from '../uc'; +import { DeletionQueueConsole } from './deletion-queue.console'; + +@Module({ + imports: [ + ConsoleModule, + ConsoleWriterModule, + HttpModule, + ConfigModule.forRoot(createConfigModuleOptions(deletionClientConfig)), + ], + providers: [DeletionClient, BatchDeletionService, BatchDeletionUc, DeletionQueueConsole], +}) +export class DeletionConsoleModule {} diff --git a/apps/server/src/modules/deletion/console/deletion-queue.console.ts b/apps/server/src/modules/deletion/console/deletion-queue.console.ts new file mode 100644 index 00000000000..264b3180679 --- /dev/null +++ b/apps/server/src/modules/deletion/console/deletion-queue.console.ts @@ -0,0 +1,45 @@ +import { Console, Command } from 'nestjs-console'; +import { ConsoleWriterService } from '@shared/infra/console'; +import { BatchDeletionUc } from '../uc'; + +interface BatchDeleteOptions { + refsFilePath: string; + targetRefDomain: string; + deleteInMinutes: number; +} + +@Console({ command: 'queue', description: 'Console providing an access to the deletion queue.' }) +export class DeletionQueueConsole { + constructor(private consoleWriter: ConsoleWriterService, private batchDeletionUc: BatchDeletionUc) {} + + @Command({ + command: 'push', + description: 'Push new deletion requests to the deletion queue.', + options: [ + { + flags: '-rfp, --refsFilePath ', + description: 'Path of the file containing all the references to the data that should be deleted.', + required: true, + }, + { + flags: '-trd, --targetRefDomain ', + description: 'Name of the target ref domain.', + required: false, + }, + { + flags: '-dim, --deleteInMinutes ', + description: 'Number of minutes after which the data deletion process should begin.', + required: false, + }, + ], + }) + async pushDeletionRequests(options: BatchDeleteOptions): Promise { + const summary = await this.batchDeletionUc.deleteRefsFromTxtFile( + options.refsFilePath, + options.targetRefDomain, + options.deleteInMinutes ? Number(options.deleteInMinutes) : undefined + ); + + this.consoleWriter.info(JSON.stringify(summary)); + } +} diff --git a/apps/server/src/modules/deletion/index.ts b/apps/server/src/modules/deletion/index.ts index bd89c1e8d84..3ef971320a7 100644 --- a/apps/server/src/modules/deletion/index.ts +++ b/apps/server/src/modules/deletion/index.ts @@ -1,2 +1,3 @@ export * from './deletion.module'; export * from './services'; +export * from './client'; diff --git a/nest-cli.json b/nest-cli.json index c8cd9726936..d5d30313354 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -45,6 +45,15 @@ "tsConfigPath": "apps/server/tsconfig.app.json" } }, + "deletion-console": { + "type": "application", + "root": "apps/server", + "entryFile": "modules/deletion/console/console", + "sourceRoot": "apps/server/src", + "compilerOptions": { + "tsConfigPath": "apps/server/tsconfig.app.json" + } + }, "files-storage": { "type": "application", "root": "apps/server", diff --git a/package.json b/package.json index abef31027a9..203c6942652 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,9 @@ "nest:start:console": "nest start console --", "nest:start:console:dev": "nest start console --watch --", "nest:start:console:debug": "nest start console --debug --watch --", + "nest:start:deletion-console": "nest start deletion-console --", + "nest:start:deletion-console:dev": "nest start deletion-console --watch --", + "nest:start:deletion-console:debug": "nest start deletion-console --debug --watch --", "nest:start:batch-deletion": "nest start batch-deletion", "nest:start:batch-deletion:dev": "nest start batch-deletion --watch", "nest:start:batch-deletion:debug": "nest start batch-deletion --watch --debug", From 20a04e9701f860f18d42f63f16a45bf352ac0aff Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Tue, 7 Nov 2023 02:00:30 +0100 Subject: [PATCH 29/72] remove no longer used app, add param to make it possible to define delay between the client calls for the case one would like to queue many thousands of deletion requests at once --- apps/server/src/apps/batch-deletion.app.ts | 20 ------------------- .../console/deletion-queue.console.ts | 13 +++++++++--- .../services/batch-deletion.service.ts | 10 +++++++++- .../modules/deletion/uc/batch-deletion.uc.ts | 12 +++++++++-- .../batch-deletion-summary.interface.ts | 1 + 5 files changed, 30 insertions(+), 26 deletions(-) delete mode 100644 apps/server/src/apps/batch-deletion.app.ts diff --git a/apps/server/src/apps/batch-deletion.app.ts b/apps/server/src/apps/batch-deletion.app.ts deleted file mode 100644 index 4ac70f8e8e5..00000000000 --- a/apps/server/src/apps/batch-deletion.app.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { NestFactory } from '@nestjs/core'; -import { ConfigService } from '@nestjs/config'; -import { Logger } from '@src/core/logger'; -import { BatchDeletionModule } from '@modules/batch-deletion/batch-deletion.module'; -import { BatchDeletionAppStartupLoggable } from './loggables/batch-deletion-app-startup.loggable'; - -async function bootstrap() { - const app = await NestFactory.createApplicationContext(BatchDeletionModule); - - const logger = await app.resolve(Logger); - const configService = app.get(ConfigService); - - const targetRefDomain = configService.get('TARGET_REF_DOMAIN') as string; - const targetRefsFilePath = configService.get('TARGET_REFS_FILE_PATH') as string; - const deleteInMinutes = configService.get('DELETE_IN_MINUTES') as number; - - logger.info(new BatchDeletionAppStartupLoggable({ targetRefDomain, targetRefsFilePath, deleteInMinutes })); -} - -void bootstrap(); diff --git a/apps/server/src/modules/deletion/console/deletion-queue.console.ts b/apps/server/src/modules/deletion/console/deletion-queue.console.ts index 264b3180679..bdd52786e51 100644 --- a/apps/server/src/modules/deletion/console/deletion-queue.console.ts +++ b/apps/server/src/modules/deletion/console/deletion-queue.console.ts @@ -2,10 +2,11 @@ import { Console, Command } from 'nestjs-console'; import { ConsoleWriterService } from '@shared/infra/console'; import { BatchDeletionUc } from '../uc'; -interface BatchDeleteOptions { +interface PushDeletionRequestsOptions { refsFilePath: string; targetRefDomain: string; deleteInMinutes: number; + callsDelayMs: number; } @Console({ command: 'queue', description: 'Console providing an access to the deletion queue.' }) @@ -31,13 +32,19 @@ export class DeletionQueueConsole { description: 'Number of minutes after which the data deletion process should begin.', required: false, }, + { + flags: '-cdm, --callsDelayMs ', + description: 'Delay between all the performed client calls, in milliseconds.', + required: false, + }, ], }) - async pushDeletionRequests(options: BatchDeleteOptions): Promise { + async pushDeletionRequests(options: PushDeletionRequestsOptions): Promise { const summary = await this.batchDeletionUc.deleteRefsFromTxtFile( options.refsFilePath, options.targetRefDomain, - options.deleteInMinutes ? Number(options.deleteInMinutes) : undefined + options.deleteInMinutes ? Number(options.deleteInMinutes) : undefined, + options.callsDelayMs ? Number(options.callsDelayMs) : undefined ); this.consoleWriter.info(JSON.stringify(summary)); diff --git a/apps/server/src/modules/deletion/services/batch-deletion.service.ts b/apps/server/src/modules/deletion/services/batch-deletion.service.ts index 72a4b6a6fd7..97312a0fafb 100644 --- a/apps/server/src/modules/deletion/services/batch-deletion.service.ts +++ b/apps/server/src/modules/deletion/services/batch-deletion.service.ts @@ -6,7 +6,10 @@ import { QueueDeletionRequestInput, QueueDeletionRequestOutput } from './interfa export class BatchDeletionService { constructor(private readonly deletionClient: DeletionClient) {} - async queueDeletionRequests(inputs: QueueDeletionRequestInput[]): Promise { + async queueDeletionRequests( + inputs: QueueDeletionRequestInput[], + callsDelayMilliseconds?: number + ): Promise { const outputs: QueueDeletionRequestOutput[] = []; // For every provided deletion request input, try to queue it via deletion client. @@ -35,6 +38,11 @@ export class BatchDeletionService { // In case of a failure client response, add the full error message to the outputs. outputs.push({ error: err.toString() }); } + + // If any delay between the client calls has been requested, "sleep" for the specified amount of time. + if (callsDelayMilliseconds && callsDelayMilliseconds > 0) { + await new Promise((resolve) => setTimeout(resolve, callsDelayMilliseconds)); + } } return outputs; diff --git a/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts b/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts index 079e28aca46..51b73bf70ba 100644 --- a/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts +++ b/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts @@ -9,7 +9,8 @@ export class BatchDeletionUc { async deleteRefsFromTxtFile( refsFilePath: string, targetRefDomain: string = 'user', - deleteInMinutes: number = 43200 // 43200 minutes = 720 hours = 30 days + deleteInMinutes: number = 43200, // 43200 minutes = 720 hours = 30 days + callsDelayMilliseconds?: number ): Promise { // First, load all the references from the provided text file (with given path). const refsFromTxtFile = ReferencesService.loadFromTxtFile(refsFilePath); @@ -26,7 +27,13 @@ export class BatchDeletionUc { }) ); - const outputs = await this.batchDeletionService.queueDeletionRequests(inputs); + // Measure the overall queueing execution time by setting the start... + const startTime = performance.now(); + + const outputs = await this.batchDeletionService.queueDeletionRequests(inputs, callsDelayMilliseconds); + + // ...and end timestamps before and after the batch deletion service method execution. + const endTime = performance.now(); // Throw an error if the returned outputs number doesn't match the returned inputs number. if (outputs.length !== inputs.length) { @@ -38,6 +45,7 @@ export class BatchDeletionUc { } const summary: BatchDeletionSummary = { + executionTimeMilliseconds: endTime - startTime, overallStatus: 'failure', successCount: 0, failureCount: 0, diff --git a/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary.interface.ts b/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary.interface.ts index 49625cd6cbf..209fc1a3d8e 100644 --- a/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary.interface.ts +++ b/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary.interface.ts @@ -6,6 +6,7 @@ export interface BatchDeletionSummaryDetail { } export interface BatchDeletionSummary { + executionTimeMilliseconds: number; overallStatus: string; successCount: number; failureCount: number; From 9e0d281baf97ba7976a7d7b00e191b3a61ddef16 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Tue, 7 Nov 2023 02:04:43 +0100 Subject: [PATCH 30/72] remove no longer used separate batch-deletion module (it became a part of the main deletion module) --- .../modules/batch-deletion/batch-deletion-config.ts | 11 ----------- .../modules/batch-deletion/batch-deletion.module.ts | 12 ------------ apps/server/src/modules/batch-deletion/index.ts | 0 3 files changed, 23 deletions(-) delete mode 100644 apps/server/src/modules/batch-deletion/batch-deletion-config.ts delete mode 100644 apps/server/src/modules/batch-deletion/batch-deletion.module.ts delete mode 100644 apps/server/src/modules/batch-deletion/index.ts diff --git a/apps/server/src/modules/batch-deletion/batch-deletion-config.ts b/apps/server/src/modules/batch-deletion/batch-deletion-config.ts deleted file mode 100644 index 8b573558014..00000000000 --- a/apps/server/src/modules/batch-deletion/batch-deletion-config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Configuration } from '@hpi-schul-cloud/commons'; - -const batchDeletionConfig = { - NEST_LOG_LEVEL: Configuration.get('NEST_LOG_LEVEL') as string, - TARGET_REF_DOMAIN: Configuration.get('BATCH_DELETION__TARGET_REF_DOMAIN') as string, - TARGET_REFS_FILE_PATH: Configuration.get('BATCH_DELETION__TARGET_REFS_FILE_PATH') as string, - DELETE_IN_MINUTES: Configuration.get('BATCH_DELETION__DELETE_IN_MINUTES') as number, - CALLS_DELAY_MILLISECONDS: Configuration.get('BATCH_DELETION__CALLS_DELAY_MILLISECONDS') as number, -}; - -export const config = () => batchDeletionConfig; diff --git a/apps/server/src/modules/batch-deletion/batch-deletion.module.ts b/apps/server/src/modules/batch-deletion/batch-deletion.module.ts deleted file mode 100644 index 893fb2c9703..00000000000 --- a/apps/server/src/modules/batch-deletion/batch-deletion.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; -import { HttpModule } from '@nestjs/axios'; -import { LoggerModule } from '@src/core/logger'; -import { createConfigModuleOptions } from '@src/config'; -import { config } from './batch-deletion-config'; - -@Module({ - imports: [HttpModule, LoggerModule, ConfigModule.forRoot(createConfigModuleOptions(config))], - providers: [], -}) -export class BatchDeletionModule {} diff --git a/apps/server/src/modules/batch-deletion/index.ts b/apps/server/src/modules/batch-deletion/index.ts deleted file mode 100644 index e69de29bb2d..00000000000 From ab14b19167b8e99f92ecf335bd71b9e45aafbac7 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Tue, 7 Nov 2023 02:07:18 +0100 Subject: [PATCH 31/72] fix invalid key --- .../src/modules/deletion/client/deletion.client.spec.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/server/src/modules/deletion/client/deletion.client.spec.ts b/apps/server/src/modules/deletion/client/deletion.client.spec.ts index a8481b3d6ef..662848ca5b4 100644 --- a/apps/server/src/modules/deletion/client/deletion.client.spec.ts +++ b/apps/server/src/modules/deletion/client/deletion.client.spec.ts @@ -7,6 +7,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { axiosResponseFactory } from '@shared/testing'; import { DeletionRequestOutput } from './interface'; import { DeletionClient } from './deletion.client'; +import { DeletionClientConfig } from './deletion-client.config'; describe(DeletionClient.name, () => { let module: TestingModule; @@ -21,8 +22,8 @@ describe(DeletionClient.name, () => { provide: ConfigService, useValue: createMock({ get: jest.fn((key: string) => { - if (key === 'ADMIN_API__BASE_URL') { - return 'http://localhost:8080'; + if (key === 'ADMIN_API_BASE_URL') { + return 'http://localhost:4030'; } // Default is for the Admin APIs API Key. From 4d13aa525544158a73260694d20820e0c0c819db Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Tue, 7 Nov 2023 02:29:25 +0100 Subject: [PATCH 32/72] remove no longer used config vars --- config/default.json | 2 +- config/default.schema.json | 28 ---------------------------- 2 files changed, 1 insertion(+), 29 deletions(-) diff --git a/config/default.json b/config/default.json index 9278a7b9e57..3e21415f49d 100644 --- a/config/default.json +++ b/config/default.json @@ -42,5 +42,5 @@ "CTL_TOOLS": { "EXTERNAL_TOOL_MAX_LOGO_SIZE_IN_BYTES": 300000 }, - "BATCH_DELETION": {} + "ADMIN_API": {} } diff --git a/config/default.schema.json b/config/default.schema.json index 0f3c6562e4d..aca8bb8edf1 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -1346,7 +1346,6 @@ "ADMIN_API": { "type": "object", "description": "Configuration of the schulcloud-server's admin API.", - "required": ["API_KEY"], "properties": { "BASE_URL": { "type": "string", @@ -1358,33 +1357,6 @@ "description": "API key for accessing the Admin API." } } - }, - "BATCH_DELETION": { - "type": "object", - "description": "Configuration for the \"batch deletion\" application.", - "required": [], - "properties": { - "TARGET_REF_DOMAIN": { - "type": "string", - "default": "user", - "description": "Domain of the provided references (ids)." - }, - "TARGET_REFS_FILE_PATH": { - "type": "string", - "default": "/data/ids-to-delete.txt", - "description": "Path to the file containing all the references (ids) that should be deleted." - }, - "DELETE_IN_MINUTES": { - "type": "integer", - "default": 43200, - "description": "The number of minutes after which the data should be deleted." - }, - "CALLS_DELAY_MILLISECONDS": { - "type": "integer", - "default": 0, - "description": "Delay between the HTTP calls in milliseconds - 0 (or less) indicates no delay." - } - } } }, "required": [], From ee43ec28f1830071b2e45b830a499b7418e79bc3 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Tue, 7 Nov 2023 02:32:05 +0100 Subject: [PATCH 33/72] remove no longer used commands --- package.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/package.json b/package.json index 203c6942652..100bbb35edd 100644 --- a/package.json +++ b/package.json @@ -79,10 +79,6 @@ "nest:start:deletion-console": "nest start deletion-console --", "nest:start:deletion-console:dev": "nest start deletion-console --watch --", "nest:start:deletion-console:debug": "nest start deletion-console --debug --watch --", - "nest:start:batch-deletion": "nest start batch-deletion", - "nest:start:batch-deletion:dev": "nest start batch-deletion --watch", - "nest:start:batch-deletion:debug": "nest start batch-deletion --watch --debug", - "nest:start:batch-deletion:prod": "node dist/apps/server/apps/batch-deletion.app", "nest:test": "npm run nest:test:cov && npm run nest:lint", "nest:test:all": "jest", "nest:test:unit": "jest \"^((?!\\.api\\.spec\\.ts).)*\\.spec\\.ts$\"", From 8dbeeb7b86dd9d9daf00980d48fcb3b15c3ce976 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Tue, 7 Nov 2023 02:32:52 +0100 Subject: [PATCH 34/72] remove no longer used Nest cli config --- nest-cli.json | 9 --------- 1 file changed, 9 deletions(-) diff --git a/nest-cli.json b/nest-cli.json index d5d30313354..1c6d418d1d8 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -98,15 +98,6 @@ "compilerOptions": { "tsConfigPath": "apps/server/tsconfig.app.json" } - }, - "batch-deletion": { - "type": "application", - "root": "apps/server", - "entryFile": "apps/batch-deletion.app", - "sourceRoot": "apps/server/src", - "compilerOptions": { - "tsConfigPath": "apps/server/tsconfig.app.json" - } } } } From 71d0b4090d509d003088b9bc86ab5e84ac773700 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Tue, 7 Nov 2023 02:46:04 +0100 Subject: [PATCH 35/72] remove no longer used code --- .../batch-deletion-app-startup.loggable.ts | 24 ------------------- 1 file changed, 24 deletions(-) delete mode 100644 apps/server/src/apps/loggables/batch-deletion-app-startup.loggable.ts diff --git a/apps/server/src/apps/loggables/batch-deletion-app-startup.loggable.ts b/apps/server/src/apps/loggables/batch-deletion-app-startup.loggable.ts deleted file mode 100644 index 152998607f9..00000000000 --- a/apps/server/src/apps/loggables/batch-deletion-app-startup.loggable.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Loggable, LogMessage } from '@src/core/logger'; - -interface BatchDeletionAppStartupInfo { - targetRefDomain: string; - targetRefsFilePath: string; - deleteInMinutes: number; - callsDelayMilliseconds: number; -} - -export class BatchDeletionAppStartupLoggable implements Loggable { - constructor(private readonly info: BatchDeletionAppStartupInfo) {} - - getLogMessage(): LogMessage { - return { - message: 'Successfully started batch deletion app...', - data: { - targetRefDomain: this.info.targetRefDomain, - targetRefsFilePath: this.info.targetRefsFilePath, - deleteInMinutes: this.info.deleteInMinutes, - callsDelayMilliseconds: this.info.callsDelayMilliseconds, - }, - }; - } -} From 4c5de4edb6ae69e3a19c6b5a4d64f8d92235f3ec Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Tue, 7 Nov 2023 02:47:49 +0100 Subject: [PATCH 36/72] change name of the method that prepares default headers --- apps/server/src/modules/deletion/client/deletion.client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server/src/modules/deletion/client/deletion.client.ts b/apps/server/src/modules/deletion/client/deletion.client.ts index 93830ee72fd..9791858ccf4 100644 --- a/apps/server/src/modules/deletion/client/deletion.client.ts +++ b/apps/server/src/modules/deletion/client/deletion.client.ts @@ -25,7 +25,7 @@ export class DeletionClient { } async queueDeletionRequest(input: DeletionRequestInput): Promise { - const request = this.httpService.post(this.postDeletionRequestsEndpoint, input, this.headers()); + const request = this.httpService.post(this.postDeletionRequestsEndpoint, input, this.defaultHeaders()); return firstValueFrom(request) .then((resp: AxiosResponse) => { @@ -58,7 +58,7 @@ export class DeletionClient { return { 'X-Api-Key': this.apiKey }; } - private headers() { + private defaultHeaders() { return { headers: this.apiKeyHeader(), }; From 02c5ed41a4e054886ac7d8627b1a5218faabd3dc Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Tue, 7 Nov 2023 17:32:56 +0100 Subject: [PATCH 37/72] add builders for most of the interfaces --- .../deletion-request-input.builder.spec.ts | 33 +++++++++++++ .../builder/deletion-request-input.builder.ts | 11 +++++ ...n-request-target-ref-input.builder.spec.ts | 26 +++++++++++ ...letion-request-target-ref-input.builder.ts | 7 +++ .../modules/deletion/client/builder/index.ts | 2 + .../src/modules/deletion/client/index.ts | 4 +- .../deletion-request-input.interface.ts | 5 +- ...tion-request-target-ref-input.interface.ts | 4 ++ .../deletion/client/interface/index.ts | 1 + .../services/batch-deletion.service.ts | 33 +++++++------ .../deletion/services/builder/index.ts | 1 + ...ue-deletion-request-output.builder.spec.ts | 46 +++++++++++++++++++ .../queue-deletion-request-output.builder.ts | 29 ++++++++++++ .../modules/deletion/uc/batch-deletion.uc.ts | 8 ++-- 14 files changed, 186 insertions(+), 24 deletions(-) create mode 100644 apps/server/src/modules/deletion/client/builder/deletion-request-input.builder.spec.ts create mode 100644 apps/server/src/modules/deletion/client/builder/deletion-request-input.builder.ts create mode 100644 apps/server/src/modules/deletion/client/builder/deletion-request-target-ref-input.builder.spec.ts create mode 100644 apps/server/src/modules/deletion/client/builder/deletion-request-target-ref-input.builder.ts create mode 100644 apps/server/src/modules/deletion/client/builder/index.ts create mode 100644 apps/server/src/modules/deletion/client/interface/deletion-request-target-ref-input.interface.ts create mode 100644 apps/server/src/modules/deletion/services/builder/index.ts create mode 100644 apps/server/src/modules/deletion/services/builder/queue-deletion-request-output.builder.spec.ts create mode 100644 apps/server/src/modules/deletion/services/builder/queue-deletion-request-output.builder.ts diff --git a/apps/server/src/modules/deletion/client/builder/deletion-request-input.builder.spec.ts b/apps/server/src/modules/deletion/client/builder/deletion-request-input.builder.spec.ts new file mode 100644 index 00000000000..bd49bb841e6 --- /dev/null +++ b/apps/server/src/modules/deletion/client/builder/deletion-request-input.builder.spec.ts @@ -0,0 +1,33 @@ +import { ObjectId } from 'bson'; +import { DeletionRequestInput } from '../interface'; +import { DeletionRequestInputBuilder } from './deletion-request-input.builder'; + +describe(DeletionRequestInputBuilder.name, () => { + describe(DeletionRequestInputBuilder.build.name, () => { + describe('when called with proper arguments', () => { + const setup = () => { + const targetRefDomain = 'school'; + const targetRefId = new ObjectId().toHexString(); + const deleteInMinutes = 43200; + + const expectedOutput: DeletionRequestInput = { + targetRef: { + domain: targetRefDomain, + id: targetRefId, + }, + deleteInMinutes, + }; + + return { targetRefDomain, targetRefId, deleteInMinutes, expectedOutput }; + }; + + it('should return valid object with expected values', () => { + const { targetRefDomain, targetRefId, deleteInMinutes, expectedOutput } = setup(); + + const output = DeletionRequestInputBuilder.build(targetRefDomain, targetRefId, deleteInMinutes); + + expect(output).toStrictEqual(expectedOutput); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/client/builder/deletion-request-input.builder.ts b/apps/server/src/modules/deletion/client/builder/deletion-request-input.builder.ts new file mode 100644 index 00000000000..28091418065 --- /dev/null +++ b/apps/server/src/modules/deletion/client/builder/deletion-request-input.builder.ts @@ -0,0 +1,11 @@ +import { DeletionRequestInput } from '../interface'; +import { DeletionRequestTargetRefInputBuilder } from './deletion-request-target-ref-input.builder'; + +export class DeletionRequestInputBuilder { + static build(targetRefDomain: string, targetRefId: string, deleteInMinutes?: number): DeletionRequestInput { + return { + targetRef: DeletionRequestTargetRefInputBuilder.build(targetRefDomain, targetRefId), + deleteInMinutes, + }; + } +} diff --git a/apps/server/src/modules/deletion/client/builder/deletion-request-target-ref-input.builder.spec.ts b/apps/server/src/modules/deletion/client/builder/deletion-request-target-ref-input.builder.spec.ts new file mode 100644 index 00000000000..74b0631e49d --- /dev/null +++ b/apps/server/src/modules/deletion/client/builder/deletion-request-target-ref-input.builder.spec.ts @@ -0,0 +1,26 @@ +import { ObjectId } from 'bson'; +import { DeletionRequestTargetRefInput } from '../interface'; +import { DeletionRequestTargetRefInputBuilder } from './deletion-request-target-ref-input.builder'; + +describe(DeletionRequestTargetRefInputBuilder.name, () => { + describe(DeletionRequestTargetRefInputBuilder.build.name, () => { + describe('when called with proper arguments', () => { + const setup = () => { + const domain = 'user'; + const id = new ObjectId().toHexString(); + + const expectedOutput: DeletionRequestTargetRefInput = { domain, id }; + + return { domain, id, expectedOutput }; + }; + + it('should return valid object with expected values', () => { + const { domain, id, expectedOutput } = setup(); + + const output = DeletionRequestTargetRefInputBuilder.build(domain, id); + + expect(output).toStrictEqual(expectedOutput); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/client/builder/deletion-request-target-ref-input.builder.ts b/apps/server/src/modules/deletion/client/builder/deletion-request-target-ref-input.builder.ts new file mode 100644 index 00000000000..ed3c0219993 --- /dev/null +++ b/apps/server/src/modules/deletion/client/builder/deletion-request-target-ref-input.builder.ts @@ -0,0 +1,7 @@ +import { DeletionRequestTargetRefInput } from '../interface'; + +export class DeletionRequestTargetRefInputBuilder { + static build(domain: string, id: string): DeletionRequestTargetRefInput { + return { domain, id }; + } +} diff --git a/apps/server/src/modules/deletion/client/builder/index.ts b/apps/server/src/modules/deletion/client/builder/index.ts new file mode 100644 index 00000000000..e01d4e396eb --- /dev/null +++ b/apps/server/src/modules/deletion/client/builder/index.ts @@ -0,0 +1,2 @@ +export * from './deletion-request-target-ref-input.builder'; +export * from './deletion-request-input.builder'; diff --git a/apps/server/src/modules/deletion/client/index.ts b/apps/server/src/modules/deletion/client/index.ts index 9aa334f0034..efe1ffe228e 100644 --- a/apps/server/src/modules/deletion/client/index.ts +++ b/apps/server/src/modules/deletion/client/index.ts @@ -1,4 +1,4 @@ -export * from './interface/deletion-request-input.interface'; -export * from './interface/deletion-request-output.interface'; +export * from './interface'; +export * from './builder'; export * from './deletion-client.config'; export * from './deletion.client'; diff --git a/apps/server/src/modules/deletion/client/interface/deletion-request-input.interface.ts b/apps/server/src/modules/deletion/client/interface/deletion-request-input.interface.ts index f8fc9a7eca1..4879ce4d972 100644 --- a/apps/server/src/modules/deletion/client/interface/deletion-request-input.interface.ts +++ b/apps/server/src/modules/deletion/client/interface/deletion-request-input.interface.ts @@ -1,7 +1,4 @@ -export interface DeletionRequestTargetRefInput { - domain: string; - id: string; -} +import { DeletionRequestTargetRefInput } from './deletion-request-target-ref-input.interface'; export interface DeletionRequestInput { targetRef: DeletionRequestTargetRefInput; diff --git a/apps/server/src/modules/deletion/client/interface/deletion-request-target-ref-input.interface.ts b/apps/server/src/modules/deletion/client/interface/deletion-request-target-ref-input.interface.ts new file mode 100644 index 00000000000..603bb0c13ec --- /dev/null +++ b/apps/server/src/modules/deletion/client/interface/deletion-request-target-ref-input.interface.ts @@ -0,0 +1,4 @@ +export interface DeletionRequestTargetRefInput { + domain: string; + id: string; +} diff --git a/apps/server/src/modules/deletion/client/interface/index.ts b/apps/server/src/modules/deletion/client/interface/index.ts index 2005003d049..11d38082a0e 100644 --- a/apps/server/src/modules/deletion/client/interface/index.ts +++ b/apps/server/src/modules/deletion/client/interface/index.ts @@ -1,2 +1,3 @@ +export * from './deletion-request-target-ref-input.interface'; export * from './deletion-request-input.interface'; export * from './deletion-request-output.interface'; diff --git a/apps/server/src/modules/deletion/services/batch-deletion.service.ts b/apps/server/src/modules/deletion/services/batch-deletion.service.ts index 97312a0fafb..84ec53a8d07 100644 --- a/apps/server/src/modules/deletion/services/batch-deletion.service.ts +++ b/apps/server/src/modules/deletion/services/batch-deletion.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { DeletionClient } from '../client'; +import { QueueDeletionRequestOutputBuilder } from '@modules/deletion/services/builder'; +import { DeletionClient, DeletionRequestInputBuilder } from '../client'; import { QueueDeletionRequestInput, QueueDeletionRequestOutput } from './interface'; @Injectable() @@ -17,31 +18,35 @@ export class BatchDeletionService { // response in a form of a requestId + deletionPlannedAt values pair or some error // returned from the client. In any case, every input should be processed. for (const input of inputs) { - const deletionRequestInput = { - targetRef: { - domain: input.targetRefDomain, - id: input.targetRefId, - }, - deleteInMinutes: input.deleteInMinutes, - }; + const deletionRequestInput = DeletionRequestInputBuilder.build( + input.targetRefDomain, + input.targetRefId, + input.deleteInMinutes + ); try { + // eslint-disable-next-line no-await-in-loop const deletionRequestOutput = await this.deletionClient.queueDeletionRequest(deletionRequestInput); // In case of a successful client response, add the // requestId + deletionPlannedAt values pair to the outputs. - outputs.push({ - requestId: deletionRequestOutput.requestId, - deletionPlannedAt: deletionRequestOutput.deletionPlannedAt, - }); + outputs.push( + QueueDeletionRequestOutputBuilder.buildSuccess( + deletionRequestOutput.requestId, + deletionRequestOutput.deletionPlannedAt + ) + ); } catch (err) { // In case of a failure client response, add the full error message to the outputs. - outputs.push({ error: err.toString() }); + outputs.push(QueueDeletionRequestOutputBuilder.buildError(err as Error)); } // If any delay between the client calls has been requested, "sleep" for the specified amount of time. if (callsDelayMilliseconds && callsDelayMilliseconds > 0) { - await new Promise((resolve) => setTimeout(resolve, callsDelayMilliseconds)); + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { + setTimeout(resolve, callsDelayMilliseconds); + }); } } diff --git a/apps/server/src/modules/deletion/services/builder/index.ts b/apps/server/src/modules/deletion/services/builder/index.ts new file mode 100644 index 00000000000..4d8ad4eb57a --- /dev/null +++ b/apps/server/src/modules/deletion/services/builder/index.ts @@ -0,0 +1 @@ +export * from './queue-deletion-request-output.builder'; diff --git a/apps/server/src/modules/deletion/services/builder/queue-deletion-request-output.builder.spec.ts b/apps/server/src/modules/deletion/services/builder/queue-deletion-request-output.builder.spec.ts new file mode 100644 index 00000000000..cd835a9cf4a --- /dev/null +++ b/apps/server/src/modules/deletion/services/builder/queue-deletion-request-output.builder.spec.ts @@ -0,0 +1,46 @@ +import { ObjectId } from 'bson'; +import { QueueDeletionRequestOutput } from '../interface'; +import { QueueDeletionRequestOutputBuilder } from './queue-deletion-request-output.builder'; + +describe(QueueDeletionRequestOutputBuilder.name, () => { + describe(QueueDeletionRequestOutputBuilder.buildSuccess.name, () => { + describe('when called with proper arguments', () => { + const setup = () => { + const requestId = new ObjectId().toHexString(); + const deletionPlannedAt = new Date(); + + const expectedOutput: QueueDeletionRequestOutput = { requestId, deletionPlannedAt }; + + return { requestId, deletionPlannedAt, expectedOutput }; + }; + + it('should return valid object with expected values', () => { + const { requestId, deletionPlannedAt, expectedOutput } = setup(); + + const output = QueueDeletionRequestOutputBuilder.buildSuccess(requestId, deletionPlannedAt); + + expect(output).toStrictEqual(expectedOutput); + }); + }); + }); + + describe(QueueDeletionRequestOutputBuilder.buildError.name, () => { + describe('when called with proper arguments', () => { + const setup = () => { + const error = new Error('test error message'); + + const expectedOutput: QueueDeletionRequestOutput = { error: error.toString() }; + + return { error, expectedOutput }; + }; + + it('should return valid object with expected values', () => { + const { error, expectedOutput } = setup(); + + const output = QueueDeletionRequestOutputBuilder.buildError(error); + + expect(output).toStrictEqual(expectedOutput); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/services/builder/queue-deletion-request-output.builder.ts b/apps/server/src/modules/deletion/services/builder/queue-deletion-request-output.builder.ts new file mode 100644 index 00000000000..f16ac924bb5 --- /dev/null +++ b/apps/server/src/modules/deletion/services/builder/queue-deletion-request-output.builder.ts @@ -0,0 +1,29 @@ +import { QueueDeletionRequestOutput } from '../interface'; + +export class QueueDeletionRequestOutputBuilder { + private static build(requestId?: string, deletionPlannedAt?: Date, error?: string): QueueDeletionRequestOutput { + const output: QueueDeletionRequestOutput = {}; + + if (requestId) { + output.requestId = requestId; + } + + if (deletionPlannedAt) { + output.deletionPlannedAt = deletionPlannedAt; + } + + if (error) { + output.error = error.toString(); + } + + return output; + } + + static buildSuccess(requestId: string, deletionPlannedAt: Date): QueueDeletionRequestOutput { + return this.build(requestId, deletionPlannedAt, undefined); + } + + static buildError(err: Error): QueueDeletionRequestOutput { + return this.build(undefined, undefined, err.toString()); + } +} diff --git a/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts b/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts index 51b73bf70ba..39db4c69125 100644 --- a/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts +++ b/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts @@ -8,8 +8,8 @@ export class BatchDeletionUc { async deleteRefsFromTxtFile( refsFilePath: string, - targetRefDomain: string = 'user', - deleteInMinutes: number = 43200, // 43200 minutes = 720 hours = 30 days + targetRefDomain = 'user', + deleteInMinutes = 43200, // 43200 minutes = 720 hours = 30 days callsDelayMilliseconds?: number ): Promise { // First, load all the references from the provided text file (with given path). @@ -22,8 +22,8 @@ export class BatchDeletionUc { refsFromTxtFile.forEach((ref) => inputs.push({ targetRefId: ref, - targetRefDomain: targetRefDomain, - deleteInMinutes: deleteInMinutes, + targetRefDomain, + deleteInMinutes, }) ); From df4124e3b6720f4505110ae22ff591998e4c9d1b Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Tue, 7 Nov 2023 18:03:14 +0100 Subject: [PATCH 38/72] add builders for the remaining interfaces --- .../deletion/services/builder/index.ts | 1 + ...eue-deletion-request-input.builder.spec.ts | 27 ++++++++ .../queue-deletion-request-input.builder.ts | 7 ++ .../src/modules/deletion/services/index.ts | 1 + .../modules/deletion/uc/batch-deletion.uc.ts | 15 ++-- ...ch-deletion-summary-detail.builder.spec.ts | 69 +++++++++++++++++++ .../batch-deletion-summary-detail.builder.ts | 8 +++ .../batch-deletion-summary.builder.spec.ts | 30 ++++++++ .../builder/batch-deletion-summary.builder.ts | 13 ++++ .../src/modules/deletion/uc/builder/index.ts | 1 + apps/server/src/modules/deletion/uc/index.ts | 1 + ...batch-deletion-summary-detail.interface.ts | 6 ++ ...ch-deletion-summary-overall-status.enum.ts | 4 ++ .../batch-deletion-summary.interface.ts | 7 +- .../modules/deletion/uc/interface/index.ts | 2 + 15 files changed, 179 insertions(+), 13 deletions(-) create mode 100644 apps/server/src/modules/deletion/services/builder/queue-deletion-request-input.builder.spec.ts create mode 100644 apps/server/src/modules/deletion/services/builder/queue-deletion-request-input.builder.ts create mode 100644 apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.spec.ts create mode 100644 apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.ts create mode 100644 apps/server/src/modules/deletion/uc/builder/batch-deletion-summary.builder.spec.ts create mode 100644 apps/server/src/modules/deletion/uc/builder/batch-deletion-summary.builder.ts create mode 100644 apps/server/src/modules/deletion/uc/builder/index.ts create mode 100644 apps/server/src/modules/deletion/uc/interface/batch-deletion-summary-detail.interface.ts create mode 100644 apps/server/src/modules/deletion/uc/interface/batch-deletion-summary-overall-status.enum.ts diff --git a/apps/server/src/modules/deletion/services/builder/index.ts b/apps/server/src/modules/deletion/services/builder/index.ts index 4d8ad4eb57a..acd85a37989 100644 --- a/apps/server/src/modules/deletion/services/builder/index.ts +++ b/apps/server/src/modules/deletion/services/builder/index.ts @@ -1 +1,2 @@ +export * from './queue-deletion-request-input.builder'; export * from './queue-deletion-request-output.builder'; diff --git a/apps/server/src/modules/deletion/services/builder/queue-deletion-request-input.builder.spec.ts b/apps/server/src/modules/deletion/services/builder/queue-deletion-request-input.builder.spec.ts new file mode 100644 index 00000000000..e5d87858156 --- /dev/null +++ b/apps/server/src/modules/deletion/services/builder/queue-deletion-request-input.builder.spec.ts @@ -0,0 +1,27 @@ +import { ObjectId } from 'bson'; +import { QueueDeletionRequestInput } from '../interface'; +import { QueueDeletionRequestInputBuilder } from './queue-deletion-request-input.builder'; + +describe(QueueDeletionRequestInputBuilder.name, () => { + describe(QueueDeletionRequestInputBuilder.build.name, () => { + describe('when called with proper arguments', () => { + const setup = () => { + const targetRefDomain = 'user'; + const targetRefId = new ObjectId().toHexString(); + const deleteInMinutes = 60; + + const expectedOutput: QueueDeletionRequestInput = { targetRefDomain, targetRefId, deleteInMinutes }; + + return { targetRefDomain, targetRefId, deleteInMinutes, expectedOutput }; + }; + + it('should return valid object with expected values', () => { + const { targetRefDomain, targetRefId, deleteInMinutes, expectedOutput } = setup(); + + const output = QueueDeletionRequestInputBuilder.build(targetRefDomain, targetRefId, deleteInMinutes); + + expect(output).toStrictEqual(expectedOutput); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/services/builder/queue-deletion-request-input.builder.ts b/apps/server/src/modules/deletion/services/builder/queue-deletion-request-input.builder.ts new file mode 100644 index 00000000000..a7fff2152b9 --- /dev/null +++ b/apps/server/src/modules/deletion/services/builder/queue-deletion-request-input.builder.ts @@ -0,0 +1,7 @@ +import { QueueDeletionRequestInput } from '../interface'; + +export class QueueDeletionRequestInputBuilder { + static build(targetRefDomain: string, targetRefId: string, deleteInMinutes: number): QueueDeletionRequestInput { + return { targetRefDomain, targetRefId, deleteInMinutes }; + } +} diff --git a/apps/server/src/modules/deletion/services/index.ts b/apps/server/src/modules/deletion/services/index.ts index dce7ed21fee..4336416b1bd 100644 --- a/apps/server/src/modules/deletion/services/index.ts +++ b/apps/server/src/modules/deletion/services/index.ts @@ -1,4 +1,5 @@ export * from './interface'; +export * from './builder'; export * from './deletion-request.service'; export * from './references.service'; export * from './batch-deletion.service'; diff --git a/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts b/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts index 39db4c69125..e17ae2ddc24 100644 --- a/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts +++ b/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts @@ -1,5 +1,10 @@ import { Injectable } from '@nestjs/common'; -import { ReferencesService, BatchDeletionService, QueueDeletionRequestInput } from '../services'; +import { + ReferencesService, + BatchDeletionService, + QueueDeletionRequestInput, + QueueDeletionRequestInputBuilder, +} from '../services'; import { BatchDeletionSummary } from './interface'; @Injectable() @@ -20,11 +25,7 @@ export class BatchDeletionUc { // For each reference found in a given file, add it to the inputs // array (with added targetRefDomain and deleteInMinutes fields). refsFromTxtFile.forEach((ref) => - inputs.push({ - targetRefId: ref, - targetRefDomain, - deleteInMinutes, - }) + inputs.push(QueueDeletionRequestInputBuilder.build(targetRefDomain, ref, deleteInMinutes)) ); // Measure the overall queueing execution time by setting the start... @@ -54,7 +55,7 @@ export class BatchDeletionUc { // Go through every received output and, in case of an error presence increase // a failure count or, in case of no error, increase a success count. - for (let i = 0; i < outputs.length; i++) { + for (let i = 0; i < outputs.length; i += 1) { if (outputs[i].error) { summary.failureCount += 1; } else { diff --git a/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.spec.ts b/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.spec.ts new file mode 100644 index 00000000000..dfa97fbe242 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.spec.ts @@ -0,0 +1,69 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BatchDeletionSummaryDetail } from '@modules/deletion/uc'; +import { QueueDeletionRequestInput, QueueDeletionRequestOutput } from '../../services'; +import { BatchDeletionSummaryDetailBuilder } from './batch-deletion-summary-detail.builder'; + +describe(BatchDeletionSummaryDetailBuilder.name, () => { + describe(BatchDeletionSummaryDetailBuilder.build.name, () => { + describe('when called with proper arguments for', () => { + describe('a successful output case', () => { + const setup = () => { + const deletionRequestInput: QueueDeletionRequestInput = { + targetRefDomain: 'user', + targetRefId: new ObjectId().toHexString(), + deleteInMinutes: 1440, + }; + + const deletionRequestOutput: QueueDeletionRequestOutput = { + requestId: new ObjectId().toHexString(), + deletionPlannedAt: new Date(), + }; + + const expectedOutput: BatchDeletionSummaryDetail = { + input: deletionRequestInput, + output: deletionRequestOutput, + }; + + return { deletionRequestInput, deletionRequestOutput, expectedOutput }; + }; + + it('should return valid object with expected values', () => { + const { deletionRequestInput, deletionRequestOutput, expectedOutput } = setup(); + + const output = BatchDeletionSummaryDetailBuilder.build(deletionRequestInput, deletionRequestOutput); + + expect(output).toStrictEqual(expectedOutput); + }); + }); + + describe('an error output case', () => { + const setup = () => { + const deletionRequestInput: QueueDeletionRequestInput = { + targetRefDomain: 'user', + targetRefId: new ObjectId().toHexString(), + deleteInMinutes: 1440, + }; + + const deletionRequestOutput: QueueDeletionRequestOutput = { + error: 'some error occurred...', + }; + + const expectedOutput: BatchDeletionSummaryDetail = { + input: deletionRequestInput, + output: deletionRequestOutput, + }; + + return { deletionRequestInput, deletionRequestOutput, expectedOutput }; + }; + + it('should return valid object with expected values', () => { + const { deletionRequestInput, deletionRequestOutput, expectedOutput } = setup(); + + const output = BatchDeletionSummaryDetailBuilder.build(deletionRequestInput, deletionRequestOutput); + + expect(output).toStrictEqual(expectedOutput); + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.ts b/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.ts new file mode 100644 index 00000000000..9ebbce66171 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.ts @@ -0,0 +1,8 @@ +import { QueueDeletionRequestInput, QueueDeletionRequestOutput } from '../../services'; +import { BatchDeletionSummaryDetail } from '../interface'; + +export class BatchDeletionSummaryDetailBuilder { + static build(input: QueueDeletionRequestInput, output: QueueDeletionRequestOutput): BatchDeletionSummaryDetail { + return { input, output }; + } +} diff --git a/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary.builder.spec.ts b/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary.builder.spec.ts new file mode 100644 index 00000000000..a2b534602d4 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary.builder.spec.ts @@ -0,0 +1,30 @@ +import { BatchDeletionSummary, BatchDeletionSummaryOverallStatus } from '../interface'; +import { BatchDeletionSummaryBuilder } from './batch-deletion-summary.builder'; + +describe(BatchDeletionSummaryBuilder.name, () => { + describe(BatchDeletionSummaryBuilder.build.name, () => { + describe('when called with proper arguments', () => { + const setup = () => { + const executionTimeMilliseconds = 142; + + const expectedOutput: BatchDeletionSummary = { + executionTimeMilliseconds: 142, + overallStatus: BatchDeletionSummaryOverallStatus.FAILURE, + successCount: 0, + failureCount: 0, + details: [], + }; + + return { executionTimeMilliseconds, expectedOutput }; + }; + + it('should return valid object with expected values', () => { + const { executionTimeMilliseconds, expectedOutput } = setup(); + + const output = BatchDeletionSummaryBuilder.build(executionTimeMilliseconds); + + expect(output).toStrictEqual(expectedOutput); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary.builder.ts b/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary.builder.ts new file mode 100644 index 00000000000..57fa2bcccd9 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary.builder.ts @@ -0,0 +1,13 @@ +import { BatchDeletionSummary, BatchDeletionSummaryOverallStatus } from '../interface'; + +export class BatchDeletionSummaryBuilder { + static build(executionTimeMilliseconds: number): BatchDeletionSummary { + return { + executionTimeMilliseconds, + overallStatus: BatchDeletionSummaryOverallStatus.FAILURE, + successCount: 0, + failureCount: 0, + details: [], + }; + } +} diff --git a/apps/server/src/modules/deletion/uc/builder/index.ts b/apps/server/src/modules/deletion/uc/builder/index.ts new file mode 100644 index 00000000000..ddf3346b705 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/builder/index.ts @@ -0,0 +1 @@ +export * from './batch-deletion-summary-detail.builder'; diff --git a/apps/server/src/modules/deletion/uc/index.ts b/apps/server/src/modules/deletion/uc/index.ts index 180609470b9..cf74de969e5 100644 --- a/apps/server/src/modules/deletion/uc/index.ts +++ b/apps/server/src/modules/deletion/uc/index.ts @@ -1 +1,2 @@ +export * from './interface'; export * from './batch-deletion.uc'; diff --git a/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary-detail.interface.ts b/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary-detail.interface.ts new file mode 100644 index 00000000000..4fe99c13fad --- /dev/null +++ b/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary-detail.interface.ts @@ -0,0 +1,6 @@ +import { QueueDeletionRequestInput, QueueDeletionRequestOutput } from '../../services'; + +export interface BatchDeletionSummaryDetail { + input: QueueDeletionRequestInput; + output: QueueDeletionRequestOutput; +} diff --git a/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary-overall-status.enum.ts b/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary-overall-status.enum.ts new file mode 100644 index 00000000000..4ae91bdf70e --- /dev/null +++ b/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary-overall-status.enum.ts @@ -0,0 +1,4 @@ +export const enum BatchDeletionSummaryOverallStatus { + SUCCESS = 'success', + FAILURE = 'failure', +} diff --git a/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary.interface.ts b/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary.interface.ts index 209fc1a3d8e..ce633e164f1 100644 --- a/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary.interface.ts +++ b/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary.interface.ts @@ -1,9 +1,4 @@ -import { QueueDeletionRequestInput, QueueDeletionRequestOutput } from '../../services'; - -export interface BatchDeletionSummaryDetail { - input: QueueDeletionRequestInput; - output: QueueDeletionRequestOutput; -} +import { BatchDeletionSummaryDetail } from './batch-deletion-summary-detail.interface'; export interface BatchDeletionSummary { executionTimeMilliseconds: number; diff --git a/apps/server/src/modules/deletion/uc/interface/index.ts b/apps/server/src/modules/deletion/uc/interface/index.ts index 57c633a76e4..57bb0c94462 100644 --- a/apps/server/src/modules/deletion/uc/interface/index.ts +++ b/apps/server/src/modules/deletion/uc/interface/index.ts @@ -1 +1,3 @@ +export * from './batch-deletion-summary-overall-status.enum'; +export * from './batch-deletion-summary-detail.interface'; export * from './batch-deletion-summary.interface'; From 0d0fdb2d777fe9eeaa3c97663e7175d939a990d4 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Tue, 7 Nov 2023 18:05:20 +0100 Subject: [PATCH 39/72] add type in catch clause --- apps/server/src/modules/deletion/client/deletion.client.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/server/src/modules/deletion/client/deletion.client.ts b/apps/server/src/modules/deletion/client/deletion.client.ts index 9791858ccf4..c0323d8a04b 100644 --- a/apps/server/src/modules/deletion/client/deletion.client.ts +++ b/apps/server/src/modules/deletion/client/deletion.client.ts @@ -9,6 +9,7 @@ import { DeletionClientConfig } from './deletion-client.config'; @Injectable() export class DeletionClient { private readonly baseUrl: string; + private readonly apiKey: string; private readonly postDeletionRequestsEndpoint: string; @@ -48,9 +49,9 @@ export class DeletionClient { return resp.data; }) - .catch((err) => { + .catch((err: Error) => { // Throw an error if sending/processing deletion request by the client failed in any way. - throw new Error(`failed to send/process a deletion request: ${err}`); + throw new Error(`failed to send/process a deletion request: ${err.toString()}`); }); } From 62695f517fc7aa28e33a6331ad9aef208df93662 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Tue, 7 Nov 2023 18:12:03 +0100 Subject: [PATCH 40/72] do some adjustments, move PushDeletionRequestsOptions interface to a separate file --- apps/server/src/modules/deletion/console/console.ts | 6 ++---- .../modules/deletion/console/deletion-queue.console.ts | 8 +------- .../src/modules/deletion/console/interface/index.ts | 1 + .../interface/push-delete-requests-options.interface.ts | 6 ++++++ .../modules/deletion/services/references.service.spec.ts | 6 ++---- 5 files changed, 12 insertions(+), 15 deletions(-) create mode 100644 apps/server/src/modules/deletion/console/interface/index.ts create mode 100644 apps/server/src/modules/deletion/console/interface/push-delete-requests-options.interface.ts diff --git a/apps/server/src/modules/deletion/console/console.ts b/apps/server/src/modules/deletion/console/console.ts index 12c6ab6bb33..65260846750 100644 --- a/apps/server/src/modules/deletion/console/console.ts +++ b/apps/server/src/modules/deletion/console/console.ts @@ -7,17 +7,15 @@ const bootstrap = new BootstrapConsole({ }); void bootstrap.init().then(async (app) => { + // eslint-disable-next-line promise/always-return try { await app.init(); - await bootstrap.boot(); - await app.close(); } catch (e) { + // eslint-disable-next-line no-console console.error(e); - await app.close(); - process.exitCode = 1; } }); diff --git a/apps/server/src/modules/deletion/console/deletion-queue.console.ts b/apps/server/src/modules/deletion/console/deletion-queue.console.ts index bdd52786e51..d67edd3dde1 100644 --- a/apps/server/src/modules/deletion/console/deletion-queue.console.ts +++ b/apps/server/src/modules/deletion/console/deletion-queue.console.ts @@ -1,13 +1,7 @@ import { Console, Command } from 'nestjs-console'; import { ConsoleWriterService } from '@shared/infra/console'; import { BatchDeletionUc } from '../uc'; - -interface PushDeletionRequestsOptions { - refsFilePath: string; - targetRefDomain: string; - deleteInMinutes: number; - callsDelayMs: number; -} +import { PushDeletionRequestsOptions } from './interface'; @Console({ command: 'queue', description: 'Console providing an access to the deletion queue.' }) export class DeletionQueueConsole { diff --git a/apps/server/src/modules/deletion/console/interface/index.ts b/apps/server/src/modules/deletion/console/interface/index.ts new file mode 100644 index 00000000000..2fcb281430f --- /dev/null +++ b/apps/server/src/modules/deletion/console/interface/index.ts @@ -0,0 +1 @@ +export * from './push-delete-requests-options.interface'; diff --git a/apps/server/src/modules/deletion/console/interface/push-delete-requests-options.interface.ts b/apps/server/src/modules/deletion/console/interface/push-delete-requests-options.interface.ts new file mode 100644 index 00000000000..a54f652cf94 --- /dev/null +++ b/apps/server/src/modules/deletion/console/interface/push-delete-requests-options.interface.ts @@ -0,0 +1,6 @@ +export interface PushDeletionRequestsOptions { + refsFilePath: string; + targetRefDomain: string; + deleteInMinutes: number; + callsDelayMs: number; +} diff --git a/apps/server/src/modules/deletion/services/references.service.spec.ts b/apps/server/src/modules/deletion/services/references.service.spec.ts index 02ed9b53068..36b1c6a8151 100644 --- a/apps/server/src/modules/deletion/services/references.service.spec.ts +++ b/apps/server/src/modules/deletion/services/references.service.spec.ts @@ -30,7 +30,7 @@ describe(ReferencesService.name, () => { describe('when passed a file with 3 references on a few separate lines', () => { describe('split with LFs', () => { it('should return an array with all the references present in a file', () => { - setup('653fd3b784ca851b17e98579\n' + '653fd3b784ca851b17e9857a\n' + '653fd3b784ca851b17e9857b\n\n\n'); + setup('653fd3b784ca851b17e98579\n653fd3b784ca851b17e9857a\n653fd3b784ca851b17e9857b\n\n\n'); const references = ReferencesService.loadFromTxtFile('references.txt'); @@ -44,9 +44,7 @@ describe(ReferencesService.name, () => { describe('split with CRLFs', () => { it('should return an array with all the references present in a file', () => { - setup( - '653fd3b784ca851b17e98579\r\n' + '653fd3b784ca851b17e9857a\r\n' + '653fd3b784ca851b17e9857b\r\n\r\n\r\n' - ); + setup('653fd3b784ca851b17e98579\r\n653fd3b784ca851b17e9857a\r\n653fd3b784ca851b17e9857b\r\n\r\n\r\n'); const references = ReferencesService.loadFromTxtFile('references.txt'); From a1c7477f3a2e6f80203f8381363920be5d95a2a9 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Tue, 7 Nov 2023 18:19:26 +0100 Subject: [PATCH 41/72] remove unused import --- apps/server/src/modules/deletion/client/deletion.client.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/server/src/modules/deletion/client/deletion.client.spec.ts b/apps/server/src/modules/deletion/client/deletion.client.spec.ts index 662848ca5b4..9ead617f5a6 100644 --- a/apps/server/src/modules/deletion/client/deletion.client.spec.ts +++ b/apps/server/src/modules/deletion/client/deletion.client.spec.ts @@ -7,7 +7,6 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { axiosResponseFactory } from '@shared/testing'; import { DeletionRequestOutput } from './interface'; import { DeletionClient } from './deletion.client'; -import { DeletionClientConfig } from './deletion-client.config'; describe(DeletionClient.name, () => { let module: TestingModule; From 5d19f5d50f8906d247ce9ccdb5eead27acaf9e9a Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Tue, 7 Nov 2023 23:16:53 +0100 Subject: [PATCH 42/72] rollback --- .../modules/deletion/entity/deletion-request.entity.spec.ts | 4 ++++ 1 file changed, 4 insertions(+) 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 1fad4c3a7a4..7f272b615c9 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 @@ -9,6 +9,10 @@ describe(DeletionRequestEntity.name, () => { await setupEntities(); }); + beforeEach(() => { + jest.clearAllMocks(); + }); + const setup = () => { const props = { id: new ObjectId().toHexString(), From a63c594ce2f778d95e524c043fa3ccd8dbf6ddf2 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Tue, 7 Nov 2023 23:17:23 +0100 Subject: [PATCH 43/72] remove unnecessary indent --- .../src/modules/deletion/entity/deletion-request.entity.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7f272b615c9..6a0e416d580 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 @@ -12,7 +12,7 @@ describe(DeletionRequestEntity.name, () => { beforeEach(() => { jest.clearAllMocks(); }); - + const setup = () => { const props = { id: new ObjectId().toHexString(), From 2f52adc85e03c9c0ef14410b42ec3598c6a19aed Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Tue, 7 Nov 2023 23:19:39 +0100 Subject: [PATCH 44/72] remove unnecessary indents --- .../src/modules/deletion/uc/deletion-request.uc.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 d0d294a2719..063f3d46b48 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 @@ -412,7 +412,7 @@ describe(DeletionRequestUc.name, () => { notExecutedDeletionRequestSummary, }; }; - + it('should call to deletionRequestService', async () => { const { deletionRequest } = setup(); @@ -446,7 +446,7 @@ describe(DeletionRequestUc.name, () => { deletionRequest, }; }; - + it('should call the service deletionRequestService.deleteById', async () => { const { deletionRequest } = setup(); From 90015922fe0db6c636d6df003f0a0af8fde97cd8 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Tue, 7 Nov 2023 23:20:53 +0100 Subject: [PATCH 45/72] remove empty line --- apps/server/src/modules/deletion/uc/deletion-request.uc.ts | 1 - 1 file changed, 1 deletion(-) 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 ac7ea184187..d94a129310f 100644 --- a/apps/server/src/modules/deletion/uc/deletion-request.uc.ts +++ b/apps/server/src/modules/deletion/uc/deletion-request.uc.ts @@ -64,7 +64,6 @@ export class DeletionRequestUc { async findById(deletionRequestId: EntityId): Promise { const deletionRequest: DeletionRequest = await this.deletionRequestService.findById(deletionRequestId); - let response: DeletionRequestLog = DeletionRequestLogBuilder.build( DeletionTargetRefBuilder.build(deletionRequest.targetRefDomain, deletionRequest.targetRefId), deletionRequest.deleteAfter From da9c2a671c2f15c1ac930aaebc252e672c322601 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Wed, 8 Nov 2023 00:34:08 +0100 Subject: [PATCH 46/72] remove repeated imports --- apps/server/src/modules/deletion/services/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/server/src/modules/deletion/services/index.ts b/apps/server/src/modules/deletion/services/index.ts index 0a5625bd00e..52e0b6ba22d 100644 --- a/apps/server/src/modules/deletion/services/index.ts +++ b/apps/server/src/modules/deletion/services/index.ts @@ -1,6 +1,5 @@ export * from './interface'; export * from './builder'; -export * from './deletion-request.service'; export * from './references.service'; export * from './batch-deletion.service'; export * from './deletion-request.service'; From 66daec16bc0f207841c5b064011d20e56b285d2a Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Wed, 8 Nov 2023 00:49:58 +0100 Subject: [PATCH 47/72] refactor some imports to omit calling Configuration.get() on every subpackage import --- apps/server/src/modules/deletion/client/index.ts | 1 - .../src/modules/deletion/console/deletion-console.module.ts | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server/src/modules/deletion/client/index.ts b/apps/server/src/modules/deletion/client/index.ts index efe1ffe228e..fde3db98f3b 100644 --- a/apps/server/src/modules/deletion/client/index.ts +++ b/apps/server/src/modules/deletion/client/index.ts @@ -1,4 +1,3 @@ export * from './interface'; export * from './builder'; -export * from './deletion-client.config'; export * from './deletion.client'; diff --git a/apps/server/src/modules/deletion/console/deletion-console.module.ts b/apps/server/src/modules/deletion/console/deletion-console.module.ts index e16814f1266..174009b1b74 100644 --- a/apps/server/src/modules/deletion/console/deletion-console.module.ts +++ b/apps/server/src/modules/deletion/console/deletion-console.module.ts @@ -4,7 +4,8 @@ import { ConfigModule } from '@nestjs/config'; import { ConsoleModule } from 'nestjs-console'; import { ConsoleWriterModule } from '@shared/infra/console'; import { createConfigModuleOptions } from '@src/config'; -import { DeletionClient, deletionClientConfig } from '../client'; +import { DeletionClient } from '../client'; +import { deletionClientConfig } from '../client/deletion-client.config'; import { BatchDeletionService } from '../services'; import { BatchDeletionUc } from '../uc'; import { DeletionQueueConsole } from './deletion-queue.console'; From 2bffa6daab03400fa5810d98dfa89c155c4a6dba Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Wed, 8 Nov 2023 01:12:20 +0100 Subject: [PATCH 48/72] add builder for the DeletionRequestOutput class --- .../deletion-request-output.builder.spec.ts | 29 +++++++++++++++++++ .../deletion-request-output.builder.ts | 10 +++++++ .../modules/deletion/client/builder/index.ts | 1 + 3 files changed, 40 insertions(+) create mode 100644 apps/server/src/modules/deletion/client/builder/deletion-request-output.builder.spec.ts create mode 100644 apps/server/src/modules/deletion/client/builder/deletion-request-output.builder.ts diff --git a/apps/server/src/modules/deletion/client/builder/deletion-request-output.builder.spec.ts b/apps/server/src/modules/deletion/client/builder/deletion-request-output.builder.spec.ts new file mode 100644 index 00000000000..631f7335b13 --- /dev/null +++ b/apps/server/src/modules/deletion/client/builder/deletion-request-output.builder.spec.ts @@ -0,0 +1,29 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { DeletionRequestOutput } from '../interface'; +import { DeletionRequestOutputBuilder } from './deletion-request-output.builder'; + +describe(DeletionRequestOutputBuilder.name, () => { + describe(DeletionRequestOutputBuilder.build.name, () => { + describe('when called with proper arguments', () => { + const setup = () => { + const requestId = new ObjectId().toHexString(); + const deletionPlannedAt = new Date(); + + const expectedOutput: DeletionRequestOutput = { + requestId, + deletionPlannedAt, + }; + + return { requestId, deletionPlannedAt, expectedOutput }; + }; + + it('should return valid object with expected values', () => { + const { requestId, deletionPlannedAt, expectedOutput } = setup(); + + const output = DeletionRequestOutputBuilder.build(requestId, deletionPlannedAt); + + expect(output).toStrictEqual(expectedOutput); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/client/builder/deletion-request-output.builder.ts b/apps/server/src/modules/deletion/client/builder/deletion-request-output.builder.ts new file mode 100644 index 00000000000..9192c1a47ce --- /dev/null +++ b/apps/server/src/modules/deletion/client/builder/deletion-request-output.builder.ts @@ -0,0 +1,10 @@ +import { DeletionRequestOutput } from '../interface'; + +export class DeletionRequestOutputBuilder { + static build(requestId: string, deletionPlannedAt: Date): DeletionRequestOutput { + return { + requestId, + deletionPlannedAt, + }; + } +} diff --git a/apps/server/src/modules/deletion/client/builder/index.ts b/apps/server/src/modules/deletion/client/builder/index.ts index e01d4e396eb..85644a6b2ee 100644 --- a/apps/server/src/modules/deletion/client/builder/index.ts +++ b/apps/server/src/modules/deletion/client/builder/index.ts @@ -1,2 +1,3 @@ export * from './deletion-request-target-ref-input.builder'; export * from './deletion-request-input.builder'; +export * from './deletion-request-output.builder'; From 06ef50bcd7348d379b7b1e8cbb8a8a5373abf48f Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Wed, 8 Nov 2023 01:21:59 +0100 Subject: [PATCH 49/72] add unit tests for the batch deletion service --- .../services/batch-deletion.service.spec.ts | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 apps/server/src/modules/deletion/services/batch-deletion.service.spec.ts diff --git a/apps/server/src/modules/deletion/services/batch-deletion.service.spec.ts b/apps/server/src/modules/deletion/services/batch-deletion.service.spec.ts new file mode 100644 index 00000000000..640e290af1a --- /dev/null +++ b/apps/server/src/modules/deletion/services/batch-deletion.service.spec.ts @@ -0,0 +1,97 @@ +import { ObjectId } from 'bson'; +import { Test, TestingModule } from '@nestjs/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { DeletionClient, DeletionRequestOutput, DeletionRequestOutputBuilder } from '../client'; +import { QueueDeletionRequestInputBuilder, QueueDeletionRequestOutputBuilder } from './builder'; +import { BatchDeletionService } from './batch-deletion.service'; + +describe(BatchDeletionService.name, () => { + let module: TestingModule; + let service: BatchDeletionService; + let deletionClient: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + BatchDeletionService, + { + provide: DeletionClient, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(BatchDeletionService); + deletionClient = module.get(DeletionClient); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + it('service should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('queueDeletionRequests', () => { + describe('when called with valid inputs array and a requested delay between the client calls', () => { + describe("when client doesn't throw any error", () => { + const setup = () => { + const inputs = [QueueDeletionRequestInputBuilder.build('user', new ObjectId().toHexString(), 60)]; + + const requestId = new ObjectId().toHexString(); + const deletionPlannedAt = new Date(); + + const queueDeletionRequestOutput: DeletionRequestOutput = DeletionRequestOutputBuilder.build( + requestId, + deletionPlannedAt + ); + + deletionClient.queueDeletionRequest.mockResolvedValueOnce(queueDeletionRequestOutput); + + const expectedOutput = QueueDeletionRequestOutputBuilder.buildSuccess(requestId, deletionPlannedAt); + + const expectedOutputs = [expectedOutput]; + + return { inputs, expectedOutputs }; + }; + + it('should return an output object with successful status info', async () => { + const { inputs, expectedOutputs } = setup(); + + const outputs = await service.queueDeletionRequests(inputs, 1); + + expect(outputs).toStrictEqual(expectedOutputs); + }); + }); + + describe('when client throws an error', () => { + const setup = () => { + const inputs = [QueueDeletionRequestInputBuilder.build('user', new ObjectId().toHexString(), 60)]; + + const error = new Error('connection error'); + + deletionClient.queueDeletionRequest.mockRejectedValueOnce(error); + + const expectedOutput = QueueDeletionRequestOutputBuilder.buildError(error); + + const expectedOutputs = [expectedOutput]; + + return { inputs, expectedOutputs }; + }; + + it('should return an output object with failure status info', async () => { + const { inputs, expectedOutputs } = setup(); + + const outputs = await service.queueDeletionRequests(inputs); + + expect(outputs).toStrictEqual(expectedOutputs); + }); + }); + }); + }); +}); From 5317daebc44d29d1335387f7fcb619bb1c92e3a6 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Wed, 8 Nov 2023 02:44:27 +0100 Subject: [PATCH 50/72] add unit tests for the BatchDeletionUc --- .../deletion/uc/batch-deletion.uc.spec.ts | 195 ++++++++++++++++++ .../modules/deletion/uc/batch-deletion.uc.ts | 12 +- 2 files changed, 200 insertions(+), 7 deletions(-) create mode 100644 apps/server/src/modules/deletion/uc/batch-deletion.uc.spec.ts diff --git a/apps/server/src/modules/deletion/uc/batch-deletion.uc.spec.ts b/apps/server/src/modules/deletion/uc/batch-deletion.uc.spec.ts new file mode 100644 index 00000000000..7292f36efb0 --- /dev/null +++ b/apps/server/src/modules/deletion/uc/batch-deletion.uc.spec.ts @@ -0,0 +1,195 @@ +import { ObjectId } from 'bson'; +import { Test, TestingModule } from '@nestjs/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { + BatchDeletionService, + QueueDeletionRequestInputBuilder, + QueueDeletionRequestOutput, + QueueDeletionRequestOutputBuilder, + ReferencesService, +} from '../services'; +import { BatchDeletionSummaryDetail, BatchDeletionSummaryOverallStatus } from './interface'; +import { BatchDeletionSummaryDetailBuilder } from './builder'; +import { BatchDeletionUc } from './batch-deletion.uc'; + +describe(BatchDeletionUc.name, () => { + let module: TestingModule; + let uc: BatchDeletionUc; + let batchDeletionService: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + BatchDeletionUc, + { + provide: BatchDeletionService, + useValue: createMock(), + }, + ], + }).compile(); + + uc = module.get(BatchDeletionUc); + batchDeletionService = module.get(BatchDeletionService); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + it('uc should be defined', () => { + expect(uc).toBeDefined(); + }); + + describe('deleteRefsFromTxtFile', () => { + describe('when called with valid arguments', () => { + describe('when batch deletion service returns an expected amount of outputs', () => { + describe('when only successful executions took place', () => { + const setup = () => { + const targetRefsCount = 3; + + const targetRefIds: string[] = []; + const outputs: QueueDeletionRequestOutput[] = []; + + for (let i = 0; i < targetRefsCount; i += 1) { + targetRefIds.push(new ObjectId().toHexString()); + outputs.push(QueueDeletionRequestOutputBuilder.buildSuccess(new ObjectId().toHexString(), new Date())); + } + + ReferencesService.loadFromTxtFile = jest.fn().mockReturnValueOnce(targetRefIds); + + batchDeletionService.queueDeletionRequests.mockResolvedValueOnce([outputs[0], outputs[1], outputs[2]]); + + const targetRefDomain = 'school'; + const deleteInMinutes = 60; + + const expectedSummaryFieldsDetails: BatchDeletionSummaryDetail[] = []; + + for (let i = 0; i < targetRefIds.length; i += 1) { + expectedSummaryFieldsDetails.push( + BatchDeletionSummaryDetailBuilder.build( + QueueDeletionRequestInputBuilder.build(targetRefDomain, targetRefIds[i], deleteInMinutes), + outputs[i] + ) + ); + } + + const expectedSummaryFields = { + overallStatus: BatchDeletionSummaryOverallStatus.SUCCESS, + successCount: 3, + failureCount: 0, + details: expectedSummaryFieldsDetails, + }; + + const refsFilePath = '/tmp/ids.txt'; + + return { refsFilePath, targetRefDomain, deleteInMinutes, expectedSummaryFields }; + }; + + it('should return proper summary with all the successes and a successful overall status', async () => { + const { refsFilePath, targetRefDomain, deleteInMinutes, expectedSummaryFields } = setup(); + + const summary = await uc.deleteRefsFromTxtFile(refsFilePath, targetRefDomain, deleteInMinutes); + + expect(summary.executionTimeMilliseconds).toBeGreaterThan(0); + expect(summary).toMatchObject(expectedSummaryFields); + }); + }); + + describe('when both successful and failed executions took place', () => { + const setup = () => { + const targetRefsCount = 3; + + const targetRefIds: string[] = []; + + for (let i = 0; i < targetRefsCount; i += 1) { + targetRefIds.push(new ObjectId().toHexString()); + } + + const targetRefDomain = 'school'; + const deleteInMinutes = 60; + + ReferencesService.loadFromTxtFile = jest.fn().mockReturnValueOnce(targetRefIds); + + const outputs = [ + QueueDeletionRequestOutputBuilder.buildSuccess(new ObjectId().toHexString(), new Date()), + QueueDeletionRequestOutputBuilder.buildError(new Error('some error occurred...')), + QueueDeletionRequestOutputBuilder.buildSuccess(new ObjectId().toHexString(), new Date()), + ]; + + batchDeletionService.queueDeletionRequests.mockResolvedValueOnce([outputs[0], outputs[1], outputs[2]]); + + const expectedSummaryFieldsDetails: BatchDeletionSummaryDetail[] = []; + + for (let i = 0; i < targetRefIds.length; i += 1) { + expectedSummaryFieldsDetails.push( + BatchDeletionSummaryDetailBuilder.build( + QueueDeletionRequestInputBuilder.build(targetRefDomain, targetRefIds[i], deleteInMinutes), + outputs[i] + ) + ); + } + + const expectedSummaryFields = { + overallStatus: BatchDeletionSummaryOverallStatus.FAILURE, + successCount: 2, + failureCount: 1, + details: expectedSummaryFieldsDetails, + }; + + const refsFilePath = '/tmp/ids.txt'; + + return { refsFilePath, targetRefDomain, deleteInMinutes, expectedSummaryFields }; + }; + + it('should return proper summary with all the successes and failures', async () => { + const { refsFilePath, targetRefDomain, deleteInMinutes, expectedSummaryFields } = setup(); + + const summary = await uc.deleteRefsFromTxtFile(refsFilePath, targetRefDomain, deleteInMinutes); + + expect(summary.executionTimeMilliseconds).toBeGreaterThan(0); + expect(summary).toMatchObject(expectedSummaryFields); + }); + }); + }); + + describe('when batch deletion service returns an invalid amount of outputs', () => { + const setup = () => { + const targetRefsCount = 3; + + const targetRefIds: string[] = []; + + for (let i = 0; i < targetRefsCount; i += 1) { + targetRefIds.push(new ObjectId().toHexString()); + } + + ReferencesService.loadFromTxtFile = jest.fn().mockReturnValueOnce(targetRefIds); + + const outputs: QueueDeletionRequestOutput[] = []; + + for (let i = 0; i < targetRefsCount - 1; i += 1) { + targetRefIds.push(new ObjectId().toHexString()); + outputs.push(QueueDeletionRequestOutputBuilder.buildSuccess(new ObjectId().toHexString(), new Date())); + } + + batchDeletionService.queueDeletionRequests.mockResolvedValueOnce(outputs); + + const refsFilePath = '/tmp/ids.txt'; + + return { refsFilePath }; + }; + + it('should throw an error', async () => { + const { refsFilePath } = setup(); + + const func = () => uc.deleteRefsFromTxtFile(refsFilePath); + + await expect(func()).rejects.toThrow(); + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts b/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts index e17ae2ddc24..721f51110e7 100644 --- a/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts +++ b/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts @@ -1,11 +1,12 @@ import { Injectable } from '@nestjs/common'; +import { BatchDeletionSummaryDetailBuilder } from '@modules/deletion/uc/builder'; import { ReferencesService, BatchDeletionService, QueueDeletionRequestInput, QueueDeletionRequestInputBuilder, } from '../services'; -import { BatchDeletionSummary } from './interface'; +import { BatchDeletionSummary, BatchDeletionSummaryOverallStatus } from './interface'; @Injectable() export class BatchDeletionUc { @@ -47,7 +48,7 @@ export class BatchDeletionUc { const summary: BatchDeletionSummary = { executionTimeMilliseconds: endTime - startTime, - overallStatus: 'failure', + overallStatus: BatchDeletionSummaryOverallStatus.FAILURE, successCount: 0, failureCount: 0, details: [], @@ -63,15 +64,12 @@ export class BatchDeletionUc { } // Also add all the processed inputs and outputs details to the overall summary. - summary.details.push({ - input: inputs[i], - output: outputs[i], - }); + summary.details.push(BatchDeletionSummaryDetailBuilder.build(inputs[i], outputs[i])); } // If no failure has been spotted, assume an overall success. if (summary.failureCount === 0) { - summary.overallStatus = 'success'; + summary.overallStatus = BatchDeletionSummaryOverallStatus.SUCCESS; } return summary; From a1b117b8727eec8d596dfd99480bde72188c266c Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Wed, 8 Nov 2023 13:20:43 +0100 Subject: [PATCH 51/72] modify env keys for the Admin API client configuration, refactor the way the deletion module's console is bootstrapped --- .../deletion/client/deletion-client.config.ts | 8 ++--- .../deletion/client/deletion.client.spec.ts | 2 +- .../deletion/client/deletion.client.ts | 4 +-- .../src/modules/deletion/console/console.ts | 30 ++++++++++++------- config/default.json | 2 +- config/default.schema.json | 4 +-- 6 files changed, 29 insertions(+), 21 deletions(-) diff --git a/apps/server/src/modules/deletion/client/deletion-client.config.ts b/apps/server/src/modules/deletion/client/deletion-client.config.ts index b880cf785cd..a7c47c48c5d 100644 --- a/apps/server/src/modules/deletion/client/deletion-client.config.ts +++ b/apps/server/src/modules/deletion/client/deletion-client.config.ts @@ -1,13 +1,13 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; export interface DeletionClientConfig { - ADMIN_API_BASE_URL: string; - ADMIN_API_API_KEY: string; + ADMIN_API_CLIENT_BASE_URL: string; + ADMIN_API_CLIENT_API_KEY: string; } const config: DeletionClientConfig = { - ADMIN_API_BASE_URL: Configuration.get('ADMIN_API__BASE_URL') as string, - ADMIN_API_API_KEY: Configuration.get('ADMIN_API__API_KEY') as string, + ADMIN_API_CLIENT_BASE_URL: Configuration.get('ADMIN_API_CLIENT__BASE_URL') as string, + ADMIN_API_CLIENT_API_KEY: Configuration.get('ADMIN_API_CLIENT__API_KEY') as string, }; export const deletionClientConfig = () => config; diff --git a/apps/server/src/modules/deletion/client/deletion.client.spec.ts b/apps/server/src/modules/deletion/client/deletion.client.spec.ts index 9ead617f5a6..ae83ad71f7d 100644 --- a/apps/server/src/modules/deletion/client/deletion.client.spec.ts +++ b/apps/server/src/modules/deletion/client/deletion.client.spec.ts @@ -21,7 +21,7 @@ describe(DeletionClient.name, () => { provide: ConfigService, useValue: createMock({ get: jest.fn((key: string) => { - if (key === 'ADMIN_API_BASE_URL') { + if (key === 'ADMIN_API_CLIENT_BASE_URL') { return 'http://localhost:4030'; } diff --git a/apps/server/src/modules/deletion/client/deletion.client.ts b/apps/server/src/modules/deletion/client/deletion.client.ts index c0323d8a04b..e50356c98a6 100644 --- a/apps/server/src/modules/deletion/client/deletion.client.ts +++ b/apps/server/src/modules/deletion/client/deletion.client.ts @@ -18,8 +18,8 @@ export class DeletionClient { private readonly httpService: HttpService, private readonly configService: ConfigService ) { - this.baseUrl = this.configService.get('ADMIN_API_BASE_URL'); - this.apiKey = this.configService.get('ADMIN_API_API_KEY'); + this.baseUrl = this.configService.get('ADMIN_API_CLIENT_BASE_URL'); + this.apiKey = this.configService.get('ADMIN_API_CLIENT_API_KEY'); // Prepare the POST /deletionRequests endpoint beforehand to not do it on every client call. this.postDeletionRequestsEndpoint = new URL('/admin/api/v1/deletionRequests', this.baseUrl).toString(); diff --git a/apps/server/src/modules/deletion/console/console.ts b/apps/server/src/modules/deletion/console/console.ts index 65260846750..1ca9e59c020 100644 --- a/apps/server/src/modules/deletion/console/console.ts +++ b/apps/server/src/modules/deletion/console/console.ts @@ -6,16 +6,24 @@ const bootstrap = new BootstrapConsole({ useDecorators: true, }); -void bootstrap.init().then(async (app) => { - // eslint-disable-next-line promise/always-return - try { - await app.init(); - await bootstrap.boot(); - await app.close(); - } catch (e) { +bootstrap + .init() + .then(async (app) => { + // eslint-disable-next-line promise/always-return + try { + await app.init(); + await bootstrap.boot(); + await app.close(); + } catch (err) { + await app.close(); + + // eslint-disable-next-line no-console + console.error(err); + process.exitCode = 1; + } + }) + .catch((err) => { // eslint-disable-next-line no-console - console.error(e); - await app.close(); + console.error(err); process.exitCode = 1; - } -}); + }); diff --git a/config/default.json b/config/default.json index 3e21415f49d..7079e66d18b 100644 --- a/config/default.json +++ b/config/default.json @@ -42,5 +42,5 @@ "CTL_TOOLS": { "EXTERNAL_TOOL_MAX_LOGO_SIZE_IN_BYTES": 300000 }, - "ADMIN_API": {} + "ADMIN_API_CLIENT": {} } diff --git a/config/default.schema.json b/config/default.schema.json index aca8bb8edf1..770267efbe8 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -1343,9 +1343,9 @@ } } }, - "ADMIN_API": { + "ADMIN_API_CLIENT": { "type": "object", - "description": "Configuration of the schulcloud-server's admin API.", + "description": "Configuration of the schulcloud-server's admin API client.", "properties": { "BASE_URL": { "type": "string", From ebfe6bd3da51a73f36723d40d3f2c168c2067fb2 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Wed, 8 Nov 2023 14:06:08 +0100 Subject: [PATCH 52/72] fix invalid import, remove unused undefined arg --- .../src/modules/deletion/console/deletion-console.module.ts | 2 +- .../services/builder/queue-deletion-request-output.builder.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server/src/modules/deletion/console/deletion-console.module.ts b/apps/server/src/modules/deletion/console/deletion-console.module.ts index 174009b1b74..155d392d634 100644 --- a/apps/server/src/modules/deletion/console/deletion-console.module.ts +++ b/apps/server/src/modules/deletion/console/deletion-console.module.ts @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common'; import { HttpModule } from '@nestjs/axios'; import { ConfigModule } from '@nestjs/config'; import { ConsoleModule } from 'nestjs-console'; -import { ConsoleWriterModule } from '@shared/infra/console'; +import { ConsoleWriterModule } from '@infra/console'; import { createConfigModuleOptions } from '@src/config'; import { DeletionClient } from '../client'; import { deletionClientConfig } from '../client/deletion-client.config'; diff --git a/apps/server/src/modules/deletion/services/builder/queue-deletion-request-output.builder.ts b/apps/server/src/modules/deletion/services/builder/queue-deletion-request-output.builder.ts index f16ac924bb5..c44e503cbaf 100644 --- a/apps/server/src/modules/deletion/services/builder/queue-deletion-request-output.builder.ts +++ b/apps/server/src/modules/deletion/services/builder/queue-deletion-request-output.builder.ts @@ -20,7 +20,7 @@ export class QueueDeletionRequestOutputBuilder { } static buildSuccess(requestId: string, deletionPlannedAt: Date): QueueDeletionRequestOutput { - return this.build(requestId, deletionPlannedAt, undefined); + return this.build(requestId, deletionPlannedAt); } static buildError(err: Error): QueueDeletionRequestOutput { From 9f62aa70c4c610cd7b63b647f08b75ee48750dd5 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Wed, 8 Nov 2023 14:35:08 +0100 Subject: [PATCH 53/72] add comment to ignore console.ts file for coverage --- apps/server/src/modules/deletion/console/console.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/server/src/modules/deletion/console/console.ts b/apps/server/src/modules/deletion/console/console.ts index 1ca9e59c020..9e9917b9d46 100644 --- a/apps/server/src/modules/deletion/console/console.ts +++ b/apps/server/src/modules/deletion/console/console.ts @@ -1,3 +1,4 @@ +/* istanbul ignore file */ import { BootstrapConsole } from 'nestjs-console'; import { DeletionConsoleModule } from './deletion-console.module'; From abc6656358a471bdea847d06617bd279828aa167 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Wed, 8 Nov 2023 14:58:34 +0100 Subject: [PATCH 54/72] move deletion client config interface to a separate file, refactor function that prepares current config, add unit tests for it --- .../client/deletion-client.config.spec.ts | 41 +++++++++++++++++++ .../deletion/client/deletion-client.config.ts | 16 +++----- .../deletion-client-config.interface.ts | 4 ++ .../deletion/client/interface/index.ts | 1 + .../console/deletion-console.module.ts | 4 +- 5 files changed, 54 insertions(+), 12 deletions(-) create mode 100644 apps/server/src/modules/deletion/client/deletion-client.config.spec.ts create mode 100644 apps/server/src/modules/deletion/client/interface/deletion-client-config.interface.ts diff --git a/apps/server/src/modules/deletion/client/deletion-client.config.spec.ts b/apps/server/src/modules/deletion/client/deletion-client.config.spec.ts new file mode 100644 index 00000000000..a3cae21e425 --- /dev/null +++ b/apps/server/src/modules/deletion/client/deletion-client.config.spec.ts @@ -0,0 +1,41 @@ +import { IConfig } from '@hpi-schul-cloud/commons/lib/interfaces/IConfig'; +import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { DeletionClientConfig } from './interface'; +import { getDeletionClientConfig } from './deletion-client.config'; + +describe(getDeletionClientConfig.name, () => { + let configBefore: IConfig; + + beforeAll(() => { + configBefore = Configuration.toObject({ plainSecrets: true }); + }); + + afterEach(() => { + Configuration.reset(configBefore); + }); + + describe('when called', () => { + const setup = () => { + const baseUrl = 'http://api-admin:4030'; + const apiKey = '652559c2-93da-42ad-94e1-640e3afbaca0'; + + Configuration.set('ADMIN_API_CLIENT__BASE_URL', baseUrl); + Configuration.set('ADMIN_API_CLIENT__API_KEY', apiKey); + + const expectedConfig: DeletionClientConfig = { + ADMIN_API_CLIENT_BASE_URL: baseUrl, + ADMIN_API_CLIENT_API_KEY: apiKey, + }; + + return { expectedConfig }; + }; + + it('should return config with proper values', () => { + const { expectedConfig } = setup(); + + const config = getDeletionClientConfig(); + + expect(config).toEqual(expectedConfig); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/client/deletion-client.config.ts b/apps/server/src/modules/deletion/client/deletion-client.config.ts index a7c47c48c5d..db5bf7ff226 100644 --- a/apps/server/src/modules/deletion/client/deletion-client.config.ts +++ b/apps/server/src/modules/deletion/client/deletion-client.config.ts @@ -1,13 +1,9 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { DeletionClientConfig } from './interface'; -export interface DeletionClientConfig { - ADMIN_API_CLIENT_BASE_URL: string; - ADMIN_API_CLIENT_API_KEY: string; -} - -const config: DeletionClientConfig = { - ADMIN_API_CLIENT_BASE_URL: Configuration.get('ADMIN_API_CLIENT__BASE_URL') as string, - ADMIN_API_CLIENT_API_KEY: Configuration.get('ADMIN_API_CLIENT__API_KEY') as string, +export const getDeletionClientConfig = (): DeletionClientConfig => { + return { + ADMIN_API_CLIENT_BASE_URL: Configuration.get('ADMIN_API_CLIENT__BASE_URL') as string, + ADMIN_API_CLIENT_API_KEY: Configuration.get('ADMIN_API_CLIENT__API_KEY') as string, + }; }; - -export const deletionClientConfig = () => config; diff --git a/apps/server/src/modules/deletion/client/interface/deletion-client-config.interface.ts b/apps/server/src/modules/deletion/client/interface/deletion-client-config.interface.ts new file mode 100644 index 00000000000..8178515b6d4 --- /dev/null +++ b/apps/server/src/modules/deletion/client/interface/deletion-client-config.interface.ts @@ -0,0 +1,4 @@ +export interface DeletionClientConfig { + ADMIN_API_CLIENT_BASE_URL: string; + ADMIN_API_CLIENT_API_KEY: string; +} diff --git a/apps/server/src/modules/deletion/client/interface/index.ts b/apps/server/src/modules/deletion/client/interface/index.ts index 11d38082a0e..38f0f639731 100644 --- a/apps/server/src/modules/deletion/client/interface/index.ts +++ b/apps/server/src/modules/deletion/client/interface/index.ts @@ -1,3 +1,4 @@ +export * from './deletion-client-config.interface'; export * from './deletion-request-target-ref-input.interface'; export * from './deletion-request-input.interface'; export * from './deletion-request-output.interface'; diff --git a/apps/server/src/modules/deletion/console/deletion-console.module.ts b/apps/server/src/modules/deletion/console/deletion-console.module.ts index 155d392d634..0585b3631da 100644 --- a/apps/server/src/modules/deletion/console/deletion-console.module.ts +++ b/apps/server/src/modules/deletion/console/deletion-console.module.ts @@ -5,7 +5,7 @@ import { ConsoleModule } from 'nestjs-console'; import { ConsoleWriterModule } from '@infra/console'; import { createConfigModuleOptions } from '@src/config'; import { DeletionClient } from '../client'; -import { deletionClientConfig } from '../client/deletion-client.config'; +import { getDeletionClientConfig } from '../client/deletion-client.config'; import { BatchDeletionService } from '../services'; import { BatchDeletionUc } from '../uc'; import { DeletionQueueConsole } from './deletion-queue.console'; @@ -15,7 +15,7 @@ import { DeletionQueueConsole } from './deletion-queue.console'; ConsoleModule, ConsoleWriterModule, HttpModule, - ConfigModule.forRoot(createConfigModuleOptions(deletionClientConfig)), + ConfigModule.forRoot(createConfigModuleOptions(getDeletionClientConfig)), ], providers: [DeletionClient, BatchDeletionService, BatchDeletionUc, DeletionQueueConsole], }) From 638d1e9945b67fedd19c31760529ac6cd6548d03 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Wed, 8 Nov 2023 15:00:48 +0100 Subject: [PATCH 55/72] fix invalid import --- apps/server/src/modules/deletion/client/deletion.client.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/server/src/modules/deletion/client/deletion.client.ts b/apps/server/src/modules/deletion/client/deletion.client.ts index e50356c98a6..66bb267d070 100644 --- a/apps/server/src/modules/deletion/client/deletion.client.ts +++ b/apps/server/src/modules/deletion/client/deletion.client.ts @@ -3,8 +3,7 @@ import { AxiosResponse } from 'axios'; import { Injectable } from '@nestjs/common'; import { HttpService } from '@nestjs/axios'; import { ConfigService } from '@nestjs/config'; -import { DeletionRequestInput, DeletionRequestOutput } from './interface'; -import { DeletionClientConfig } from './deletion-client.config'; +import { DeletionRequestInput, DeletionRequestOutput, DeletionClientConfig } from './interface'; @Injectable() export class DeletionClient { From a5180aec0c35d8d33171a5772cad4d582773e8c3 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Wed, 8 Nov 2023 16:03:06 +0100 Subject: [PATCH 56/72] add more test cases to the deletion client unit tests --- .../deletion/client/deletion.client.spec.ts | 74 ++++++++++++++----- 1 file changed, 57 insertions(+), 17 deletions(-) diff --git a/apps/server/src/modules/deletion/client/deletion.client.spec.ts b/apps/server/src/modules/deletion/client/deletion.client.spec.ts index ae83ad71f7d..eeebd688b21 100644 --- a/apps/server/src/modules/deletion/client/deletion.client.spec.ts +++ b/apps/server/src/modules/deletion/client/deletion.client.spec.ts @@ -5,6 +5,7 @@ import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { axiosResponseFactory } from '@shared/testing'; +import { DeletionRequestInputBuilder, DeletionRequestOutputBuilder } from '@modules/deletion'; import { DeletionRequestOutput } from './interface'; import { DeletionClient } from './deletion.client'; @@ -52,17 +53,12 @@ describe(DeletionClient.name, () => { describe('queueDeletionRequest', () => { describe('when received valid response with expected HTTP status code', () => { const setup = () => { - const input = { - targetRef: { - domain: 'user', - id: '652f1625e9bc1a13bdaae48b', - }, - }; + const input = DeletionRequestInputBuilder.build('user', '652f1625e9bc1a13bdaae48b'); - const output: DeletionRequestOutput = { - requestId: '6536ce29b595d7c8e5faf200', - deletionPlannedAt: new Date('2024-10-15T12:42:50.521Z'), - }; + const output: DeletionRequestOutput = DeletionRequestOutputBuilder.build( + '6536ce29b595d7c8e5faf200', + new Date('2024-10-15T12:42:50.521Z') + ); const response: AxiosResponse = axiosResponseFactory.build({ data: output, @@ -85,14 +81,9 @@ describe(DeletionClient.name, () => { describe('when received invalid HTTP status code in a response', () => { const setup = () => { - const input = { - targetRef: { - domain: 'user', - id: '652f1625e9bc1a13bdaae48b', - }, - }; + const input = DeletionRequestInputBuilder.build('user', '652f1625e9bc1a13bdaae48b'); - const output: DeletionRequestOutput = { requestId: '', deletionPlannedAt: new Date() }; + const output: DeletionRequestOutput = DeletionRequestOutputBuilder.build('', new Date()); const response: AxiosResponse = axiosResponseFactory.build({ data: output, @@ -110,5 +101,54 @@ describe(DeletionClient.name, () => { await expect(client.queueDeletionRequest(input)).rejects.toThrow(Error); }); }); + + describe('when received no requestId in a response', () => { + const setup = () => { + const input = DeletionRequestInputBuilder.build('user', '652f1625e9bc1a13bdaae48b'); + + const output: DeletionRequestOutput = DeletionRequestOutputBuilder.build( + '', + new Date('2024-10-15T12:42:50.521Z') + ); + + const response: AxiosResponse = axiosResponseFactory.build({ + data: output, + status: 202, + }); + + httpService.post.mockReturnValueOnce(of(response)); + + return { input }; + }; + + it('should throw an exception', async () => { + const { input } = setup(); + + await expect(client.queueDeletionRequest(input)).rejects.toThrow(Error); + }); + }); + + describe('when received no deletionPlannedAt in a response', () => { + const setup = () => { + const input = DeletionRequestInputBuilder.build('user', '652f1625e9bc1a13bdaae48b'); + + const response: AxiosResponse = axiosResponseFactory.build({ + data: { + requestId: '6536ce29b595d7c8e5faf200', + }, + status: 202, + }); + + httpService.post.mockReturnValueOnce(of(response)); + + return { input }; + }; + + it('should throw an exception', async () => { + const { input } = setup(); + + await expect(client.queueDeletionRequest(input)).rejects.toThrow(Error); + }); + }); }); }); From e1c931268ef0fd1f40c1b7ca907b2afb481e4b6f Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Wed, 8 Nov 2023 16:07:25 +0100 Subject: [PATCH 57/72] change invalid import Co-authored-by: WojciechGrancow <116577704+WojciechGrancow@users.noreply.github.com> --- .../client/builder/deletion-request-output.builder.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/modules/deletion/client/builder/deletion-request-output.builder.spec.ts b/apps/server/src/modules/deletion/client/builder/deletion-request-output.builder.spec.ts index 631f7335b13..399821f33ff 100644 --- a/apps/server/src/modules/deletion/client/builder/deletion-request-output.builder.spec.ts +++ b/apps/server/src/modules/deletion/client/builder/deletion-request-output.builder.spec.ts @@ -1,4 +1,4 @@ -import { ObjectId } from '@mikro-orm/mongodb'; +import { ObjectId } from 'bson'; import { DeletionRequestOutput } from '../interface'; import { DeletionRequestOutputBuilder } from './deletion-request-output.builder'; From b35aa9c6fea684ad930c31b43a91f37891438db2 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Wed, 8 Nov 2023 16:19:48 +0100 Subject: [PATCH 58/72] fix invalid import --- .../src/modules/deletion/console/deletion-queue.console.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/modules/deletion/console/deletion-queue.console.ts b/apps/server/src/modules/deletion/console/deletion-queue.console.ts index d67edd3dde1..c8b133dba84 100644 --- a/apps/server/src/modules/deletion/console/deletion-queue.console.ts +++ b/apps/server/src/modules/deletion/console/deletion-queue.console.ts @@ -1,5 +1,5 @@ import { Console, Command } from 'nestjs-console'; -import { ConsoleWriterService } from '@shared/infra/console'; +import { ConsoleWriterService } from '@infra/console'; import { BatchDeletionUc } from '../uc'; import { PushDeletionRequestsOptions } from './interface'; From 667427fe40d193b9e9abfa1f42da97dcd51ca6c1 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Wed, 8 Nov 2023 17:15:05 +0100 Subject: [PATCH 59/72] add builder for the PushDeletionRequestsOptions class, add unit tests for the DeletionQueueConsole --- .../modules/deletion/console/builder/index.ts | 1 + ...sh-delete-requests-options.builder.spec.ts | 43 ++++++++++ .../push-delete-requests-options.builder.ts | 17 ++++ .../console/deletion-queue.console.spec.ts | 79 +++++++++++++++++++ 4 files changed, 140 insertions(+) create mode 100644 apps/server/src/modules/deletion/console/builder/index.ts create mode 100644 apps/server/src/modules/deletion/console/builder/push-delete-requests-options.builder.spec.ts create mode 100644 apps/server/src/modules/deletion/console/builder/push-delete-requests-options.builder.ts create mode 100644 apps/server/src/modules/deletion/console/deletion-queue.console.spec.ts diff --git a/apps/server/src/modules/deletion/console/builder/index.ts b/apps/server/src/modules/deletion/console/builder/index.ts new file mode 100644 index 00000000000..12fd0997ebe --- /dev/null +++ b/apps/server/src/modules/deletion/console/builder/index.ts @@ -0,0 +1 @@ +export * from './push-delete-requests-options.builder'; diff --git a/apps/server/src/modules/deletion/console/builder/push-delete-requests-options.builder.spec.ts b/apps/server/src/modules/deletion/console/builder/push-delete-requests-options.builder.spec.ts new file mode 100644 index 00000000000..5c83defdd1e --- /dev/null +++ b/apps/server/src/modules/deletion/console/builder/push-delete-requests-options.builder.spec.ts @@ -0,0 +1,43 @@ +import { PushDeletionRequestsOptions } from '../interface'; +import { PushDeleteRequestsOptionsBuilder } from './push-delete-requests-options.builder'; + +describe(PushDeleteRequestsOptionsBuilder.name, () => { + describe(PushDeleteRequestsOptionsBuilder.build.name, () => { + describe('when called with proper arguments', () => { + const setup = () => { + const refsFilePath = '/tmp/ids.txt'; + const targetRefDomain = 'school'; + const deleteInMinutes = 43200; + const callsDelayMs = 100; + + const expectedOutput: PushDeletionRequestsOptions = { + refsFilePath, + targetRefDomain, + deleteInMinutes, + callsDelayMs, + }; + + return { + refsFilePath, + targetRefDomain, + deleteInMinutes, + callsDelayMs, + expectedOutput, + }; + }; + + it('should return valid object with expected values', () => { + const { refsFilePath, targetRefDomain, deleteInMinutes, callsDelayMs, expectedOutput } = setup(); + + const output = PushDeleteRequestsOptionsBuilder.build( + refsFilePath, + targetRefDomain, + deleteInMinutes, + callsDelayMs + ); + + expect(output).toStrictEqual(expectedOutput); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/console/builder/push-delete-requests-options.builder.ts b/apps/server/src/modules/deletion/console/builder/push-delete-requests-options.builder.ts new file mode 100644 index 00000000000..f8ceae9263d --- /dev/null +++ b/apps/server/src/modules/deletion/console/builder/push-delete-requests-options.builder.ts @@ -0,0 +1,17 @@ +import { PushDeletionRequestsOptions } from '../interface'; + +export class PushDeleteRequestsOptionsBuilder { + static build( + refsFilePath: string, + targetRefDomain: string, + deleteInMinutes: number, + callsDelayMs: number + ): PushDeletionRequestsOptions { + return { + refsFilePath, + targetRefDomain, + deleteInMinutes, + callsDelayMs, + }; + } +} diff --git a/apps/server/src/modules/deletion/console/deletion-queue.console.spec.ts b/apps/server/src/modules/deletion/console/deletion-queue.console.spec.ts new file mode 100644 index 00000000000..61f9cf0ff53 --- /dev/null +++ b/apps/server/src/modules/deletion/console/deletion-queue.console.spec.ts @@ -0,0 +1,79 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ConsoleWriterService } from '@infra/console'; +import { createMock } from '@golevelup/ts-jest'; +import { BatchDeletionUc } from '../uc'; +import { DeletionQueueConsole } from './deletion-queue.console'; +import { PushDeleteRequestsOptionsBuilder } from './builder'; + +describe(DeletionQueueConsole.name, () => { + let module: TestingModule; + let console: DeletionQueueConsole; + let batchDeletionUc: BatchDeletionUc; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + DeletionQueueConsole, + { + provide: ConsoleWriterService, + useValue: createMock(), + }, + { + provide: BatchDeletionUc, + useValue: createMock(), + }, + ], + }).compile(); + + console = module.get(DeletionQueueConsole); + batchDeletionUc = module.get(BatchDeletionUc); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + it('console should be defined', () => { + expect(console).toBeDefined(); + }); + + describe('pushDeletionRequests', () => { + describe('when called with valid options', () => { + const setup = () => { + const refsFilePath = '/tmp/ids.txt'; + const targetRefDomain = 'school'; + const deleteInMinutes = 43200; + const callsDelayMs = 100; + + const options = PushDeleteRequestsOptionsBuilder.build( + refsFilePath, + targetRefDomain, + deleteInMinutes, + callsDelayMs + ); + + return { + refsFilePath, + targetRefDomain, + deleteInMinutes, + callsDelayMs, + options, + }; + }; + + it(`should call ${BatchDeletionUc.name} with proper arguments`, async () => { + const { refsFilePath, targetRefDomain, deleteInMinutes, callsDelayMs, options } = setup(); + + const spy = jest.spyOn(batchDeletionUc, 'deleteRefsFromTxtFile'); + + await console.pushDeletionRequests(options); + + expect(spy).toBeCalledWith(refsFilePath, targetRefDomain, deleteInMinutes, callsDelayMs); + }); + }); + }); +}); From 02bd3a2b582d46aa61f502f24e9a8350d6105209 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Wed, 8 Nov 2023 18:29:56 +0100 Subject: [PATCH 60/72] rename the file containing the deletion module console to deletion.console.ts, add coverage exclusion for it for the Sonar coverage analysis --- .../deletion/console/{console.ts => deletion.console.ts} | 0 nest-cli.json | 2 +- sonar-project.properties | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename apps/server/src/modules/deletion/console/{console.ts => deletion.console.ts} (100%) diff --git a/apps/server/src/modules/deletion/console/console.ts b/apps/server/src/modules/deletion/console/deletion.console.ts similarity index 100% rename from apps/server/src/modules/deletion/console/console.ts rename to apps/server/src/modules/deletion/console/deletion.console.ts diff --git a/nest-cli.json b/nest-cli.json index 1c6d418d1d8..9199961524a 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -48,7 +48,7 @@ "deletion-console": { "type": "application", "root": "apps/server", - "entryFile": "modules/deletion/console/console", + "entryFile": "modules/deletion/console/deletion.console", "sourceRoot": "apps/server/src", "compilerOptions": { "tsConfigPath": "apps/server/tsconfig.app.json" diff --git a/sonar-project.properties b/sonar-project.properties index 82334f8ff5d..f7a511890ed 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -4,7 +4,7 @@ sonar.sources=. sonar.tests=. sonar.test.inclusions=**/*.spec.ts sonar.exclusions=**/*.js,jest.config.ts,globalSetup.ts,globalTeardown.ts,**/*.app.ts,**/seed-data/*.ts -sonar.coverage.exclusions=**/board-management.uc.ts,**/*.module.ts,**/*.factory.ts +sonar.coverage.exclusions=**/board-management.uc.ts,**/*.module.ts,**/*.factory.ts,deletion.console.ts sonar.cpd.exclusions=**/controller/dto/*.ts sonar.javascript.lcov.reportPaths=merged-lcov.info sonar.typescript.tsconfigPaths=tsconfig.json,src/apps/server/tsconfig.app.json From 88b0b289812af8117dd43615b2e4c908ca326d21 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Wed, 8 Nov 2023 23:12:51 +0100 Subject: [PATCH 61/72] remove deletion.console.ts from the sonar.coverage.exclusions param as it doesn't seem to work anyway --- sonar-project.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonar-project.properties b/sonar-project.properties index f7a511890ed..82334f8ff5d 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -4,7 +4,7 @@ sonar.sources=. sonar.tests=. sonar.test.inclusions=**/*.spec.ts sonar.exclusions=**/*.js,jest.config.ts,globalSetup.ts,globalTeardown.ts,**/*.app.ts,**/seed-data/*.ts -sonar.coverage.exclusions=**/board-management.uc.ts,**/*.module.ts,**/*.factory.ts,deletion.console.ts +sonar.coverage.exclusions=**/board-management.uc.ts,**/*.module.ts,**/*.factory.ts sonar.cpd.exclusions=**/controller/dto/*.ts sonar.javascript.lcov.reportPaths=merged-lcov.info sonar.typescript.tsconfigPaths=tsconfig.json,src/apps/server/tsconfig.app.json From 7f06aa6b3ddf75748a56dc5a4b01d309f716f819 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Wed, 8 Nov 2023 23:47:55 +0100 Subject: [PATCH 62/72] add deletion.console.ts file to the coverage exclusions (another try with different path) --- sonar-project.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonar-project.properties b/sonar-project.properties index 82334f8ff5d..15000ed8aa1 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -4,7 +4,7 @@ sonar.sources=. sonar.tests=. sonar.test.inclusions=**/*.spec.ts sonar.exclusions=**/*.js,jest.config.ts,globalSetup.ts,globalTeardown.ts,**/*.app.ts,**/seed-data/*.ts -sonar.coverage.exclusions=**/board-management.uc.ts,**/*.module.ts,**/*.factory.ts +sonar.coverage.exclusions=**/board-management.uc.ts,**/*.module.ts,**/*.factory.ts,**/deletion.console.ts sonar.cpd.exclusions=**/controller/dto/*.ts sonar.javascript.lcov.reportPaths=merged-lcov.info sonar.typescript.tsconfigPaths=tsconfig.json,src/apps/server/tsconfig.app.json From 4d1831398797583a91e57acce1ae14e6b40a1b36 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Thu, 9 Nov 2023 12:55:18 +0100 Subject: [PATCH 63/72] change name of the file containing the deletion console app --- .../console/{deletion.console.ts => deletion-console.app.ts} | 0 nest-cli.json | 2 +- sonar-project.properties | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename apps/server/src/modules/deletion/console/{deletion.console.ts => deletion-console.app.ts} (100%) diff --git a/apps/server/src/modules/deletion/console/deletion.console.ts b/apps/server/src/modules/deletion/console/deletion-console.app.ts similarity index 100% rename from apps/server/src/modules/deletion/console/deletion.console.ts rename to apps/server/src/modules/deletion/console/deletion-console.app.ts diff --git a/nest-cli.json b/nest-cli.json index 9199961524a..67ca4b0da7c 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -48,7 +48,7 @@ "deletion-console": { "type": "application", "root": "apps/server", - "entryFile": "modules/deletion/console/deletion.console", + "entryFile": "modules/deletion/console/deletion-console.app", "sourceRoot": "apps/server/src", "compilerOptions": { "tsConfigPath": "apps/server/tsconfig.app.json" diff --git a/sonar-project.properties b/sonar-project.properties index 15000ed8aa1..82334f8ff5d 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -4,7 +4,7 @@ sonar.sources=. sonar.tests=. sonar.test.inclusions=**/*.spec.ts sonar.exclusions=**/*.js,jest.config.ts,globalSetup.ts,globalTeardown.ts,**/*.app.ts,**/seed-data/*.ts -sonar.coverage.exclusions=**/board-management.uc.ts,**/*.module.ts,**/*.factory.ts,**/deletion.console.ts +sonar.coverage.exclusions=**/board-management.uc.ts,**/*.module.ts,**/*.factory.ts sonar.cpd.exclusions=**/controller/dto/*.ts sonar.javascript.lcov.reportPaths=merged-lcov.info sonar.typescript.tsconfigPaths=tsconfig.json,src/apps/server/tsconfig.app.json From a3037ea76a0a8ac2447a5f6b92bd5550d17dbf02 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Mon, 13 Nov 2023 13:39:01 +0100 Subject: [PATCH 64/72] fix some imports --- apps/server/src/modules/deletion/client/deletion.client.spec.ts | 2 +- .../src/modules/deletion/services/batch-deletion.service.ts | 2 +- apps/server/src/modules/deletion/uc/batch-deletion.uc.ts | 2 +- .../uc/builder/batch-deletion-summary-detail.builder.spec.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/server/src/modules/deletion/client/deletion.client.spec.ts b/apps/server/src/modules/deletion/client/deletion.client.spec.ts index eeebd688b21..096b1f9b082 100644 --- a/apps/server/src/modules/deletion/client/deletion.client.spec.ts +++ b/apps/server/src/modules/deletion/client/deletion.client.spec.ts @@ -5,7 +5,7 @@ import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { axiosResponseFactory } from '@shared/testing'; -import { DeletionRequestInputBuilder, DeletionRequestOutputBuilder } from '@modules/deletion'; +import { DeletionRequestInputBuilder, DeletionRequestOutputBuilder } from '.'; import { DeletionRequestOutput } from './interface'; import { DeletionClient } from './deletion.client'; diff --git a/apps/server/src/modules/deletion/services/batch-deletion.service.ts b/apps/server/src/modules/deletion/services/batch-deletion.service.ts index 84ec53a8d07..d8c14f315a0 100644 --- a/apps/server/src/modules/deletion/services/batch-deletion.service.ts +++ b/apps/server/src/modules/deletion/services/batch-deletion.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { QueueDeletionRequestOutputBuilder } from '@modules/deletion/services/builder'; +import { QueueDeletionRequestOutputBuilder } from './builder'; import { DeletionClient, DeletionRequestInputBuilder } from '../client'; import { QueueDeletionRequestInput, QueueDeletionRequestOutput } from './interface'; diff --git a/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts b/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts index 721f51110e7..3556622acb8 100644 --- a/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts +++ b/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { BatchDeletionSummaryDetailBuilder } from '@modules/deletion/uc/builder'; +import { BatchDeletionSummaryDetailBuilder } from './builder'; import { ReferencesService, BatchDeletionService, diff --git a/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.spec.ts b/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.spec.ts index dfa97fbe242..43ea82e86d5 100644 --- a/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.spec.ts +++ b/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.spec.ts @@ -1,5 +1,5 @@ import { ObjectId } from '@mikro-orm/mongodb'; -import { BatchDeletionSummaryDetail } from '@modules/deletion/uc'; +import { BatchDeletionSummaryDetail } from '..'; import { QueueDeletionRequestInput, QueueDeletionRequestOutput } from '../../services'; import { BatchDeletionSummaryDetailBuilder } from './batch-deletion-summary-detail.builder'; From d8ff82f1d9db44cc6708c1bb92c513980381b0ed Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Mon, 13 Nov 2023 13:57:45 +0100 Subject: [PATCH 65/72] move default value for the ADMIN_API_CLIENT object to default.schema.json --- config/default.json | 3 +-- config/default.schema.json | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/default.json b/config/default.json index 7079e66d18b..7dd1802a037 100644 --- a/config/default.json +++ b/config/default.json @@ -41,6 +41,5 @@ }, "CTL_TOOLS": { "EXTERNAL_TOOL_MAX_LOGO_SIZE_IN_BYTES": 300000 - }, - "ADMIN_API_CLIENT": {} + } } diff --git a/config/default.schema.json b/config/default.schema.json index 1140f527b25..fac7d10ffa0 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -1352,7 +1352,8 @@ "type": "string", "description": "API key for accessing the Admin API." } - } + }, + "default": {} } }, "required": [], From 5624dc541df777495fe56042b44a6ce00bc6710e Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Mon, 13 Nov 2023 14:05:30 +0100 Subject: [PATCH 66/72] move default for the BASE_URL --- config/default.schema.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/config/default.schema.json b/config/default.schema.json index fac7d10ffa0..7be915aaa18 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -1345,7 +1345,6 @@ "properties": { "BASE_URL": { "type": "string", - "default": "http://localhost:4030", "description": "Base URL of the Admin API." }, "API_KEY": { @@ -1353,7 +1352,9 @@ "description": "API key for accessing the Admin API." } }, - "default": {} + "default": { + "BASE_URL": "http://localhost:4030" + } } }, "required": [], From 8ec624f7d083e1c792863fd311beebcafa07f6b6 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Mon, 13 Nov 2023 14:43:31 +0100 Subject: [PATCH 67/72] move Deletion module console app to the apps/ dir --- .../{modules/deletion/console => apps}/deletion-console.app.ts | 2 +- apps/server/src/modules/deletion/console/index.ts | 1 + apps/server/src/modules/deletion/index.ts | 1 + nest-cli.json | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) rename apps/server/src/{modules/deletion/console => apps}/deletion-console.app.ts (89%) create mode 100644 apps/server/src/modules/deletion/console/index.ts diff --git a/apps/server/src/modules/deletion/console/deletion-console.app.ts b/apps/server/src/apps/deletion-console.app.ts similarity index 89% rename from apps/server/src/modules/deletion/console/deletion-console.app.ts rename to apps/server/src/apps/deletion-console.app.ts index 9e9917b9d46..8c6939de7bb 100644 --- a/apps/server/src/modules/deletion/console/deletion-console.app.ts +++ b/apps/server/src/apps/deletion-console.app.ts @@ -1,6 +1,6 @@ /* istanbul ignore file */ import { BootstrapConsole } from 'nestjs-console'; -import { DeletionConsoleModule } from './deletion-console.module'; +import { DeletionConsoleModule } from '@modules/deletion'; const bootstrap = new BootstrapConsole({ module: DeletionConsoleModule, diff --git a/apps/server/src/modules/deletion/console/index.ts b/apps/server/src/modules/deletion/console/index.ts new file mode 100644 index 00000000000..db47bd8c99f --- /dev/null +++ b/apps/server/src/modules/deletion/console/index.ts @@ -0,0 +1 @@ +export * from './deletion-console.module'; diff --git a/apps/server/src/modules/deletion/index.ts b/apps/server/src/modules/deletion/index.ts index 3ef971320a7..793f306e7a0 100644 --- a/apps/server/src/modules/deletion/index.ts +++ b/apps/server/src/modules/deletion/index.ts @@ -1,3 +1,4 @@ export * from './deletion.module'; export * from './services'; export * from './client'; +export * from './console'; diff --git a/nest-cli.json b/nest-cli.json index 67ca4b0da7c..8ce5461bb6f 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -48,7 +48,7 @@ "deletion-console": { "type": "application", "root": "apps/server", - "entryFile": "modules/deletion/console/deletion-console.app", + "entryFile": "apps/deletion-console.app", "sourceRoot": "apps/server/src", "compilerOptions": { "tsConfigPath": "apps/server/tsconfig.app.json" From e87ede71385e4d6d25ee612b857a1127768326a4 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Mon, 13 Nov 2023 17:37:26 +0100 Subject: [PATCH 68/72] add separate functino to log error and set exit code --- apps/server/src/apps/deletion-console.app.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/server/src/apps/deletion-console.app.ts b/apps/server/src/apps/deletion-console.app.ts index 8c6939de7bb..0ef823ae42a 100644 --- a/apps/server/src/apps/deletion-console.app.ts +++ b/apps/server/src/apps/deletion-console.app.ts @@ -7,6 +7,12 @@ const bootstrap = new BootstrapConsole({ useDecorators: true, }); +const logErrorAndSetExitCode = (err: unknown) => { + // eslint-disable-next-line no-console + console.error(err); + process.exitCode = 1; +}; + bootstrap .init() .then(async (app) => { @@ -18,13 +24,7 @@ bootstrap } catch (err) { await app.close(); - // eslint-disable-next-line no-console - console.error(err); - process.exitCode = 1; + logErrorAndSetExitCode(err); } }) - .catch((err) => { - // eslint-disable-next-line no-console - console.error(err); - process.exitCode = 1; - }); + .catch((err) => logErrorAndSetExitCode(err)); From abc83e57d3fdc80d4d26c74c07bc7845fc628151 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Mon, 13 Nov 2023 17:43:25 +0100 Subject: [PATCH 69/72] add handling of the case that only CR chars are used as a line separators --- .../deletion/services/references.service.spec.ts | 14 ++++++++++++++ .../deletion/services/references.service.ts | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/server/src/modules/deletion/services/references.service.spec.ts b/apps/server/src/modules/deletion/services/references.service.spec.ts index 36b1c6a8151..26eb6e0c8d7 100644 --- a/apps/server/src/modules/deletion/services/references.service.spec.ts +++ b/apps/server/src/modules/deletion/services/references.service.spec.ts @@ -28,6 +28,20 @@ describe(ReferencesService.name, () => { }); describe('when passed a file with 3 references on a few separate lines', () => { + describe('split with CRs', () => { + it('should return an array with all the references present in a file', () => { + setup('653fd3b784ca851b17e98579\r653fd3b784ca851b17e9857a\r653fd3b784ca851b17e9857b\n\n\n'); + + const references = ReferencesService.loadFromTxtFile('references.txt'); + + expect(references).toEqual([ + '653fd3b784ca851b17e98579', + '653fd3b784ca851b17e9857a', + '653fd3b784ca851b17e9857b', + ]); + }); + }); + describe('split with LFs', () => { it('should return an array with all the references present in a file', () => { setup('653fd3b784ca851b17e98579\n653fd3b784ca851b17e9857a\n653fd3b784ca851b17e9857b\n\n\n'); diff --git a/apps/server/src/modules/deletion/services/references.service.ts b/apps/server/src/modules/deletion/services/references.service.ts index 7706f11fee5..991c36c02af 100644 --- a/apps/server/src/modules/deletion/services/references.service.ts +++ b/apps/server/src/modules/deletion/services/references.service.ts @@ -5,7 +5,7 @@ export class ReferencesService { let fileContent = fs.readFileSync(filePath).toString(); // Replace all the CRLF occurrences with just a LF. - fileContent = fileContent.replace(/\r\n/g, '\n'); + fileContent = fileContent.replace(/\r\n?/g, '\n'); // Split the whole file content by a line feed (LF) char (\n). const fileLines = fileContent.split('\n'); From 3cd6e25edfca0354e4fb8b4f14c64cd32d3d9009 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Mon, 13 Nov 2023 17:59:20 +0100 Subject: [PATCH 70/72] add use of the BatchDeletionSummaryBuilder in place of an anonymous object creation --- apps/server/src/modules/deletion/uc/batch-deletion.uc.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts b/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts index 3556622acb8..f435f2f4338 100644 --- a/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts +++ b/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts @@ -1,4 +1,5 @@ import { Injectable } from '@nestjs/common'; +import { BatchDeletionSummaryBuilder } from '@modules/deletion/uc/builder/batch-deletion-summary.builder'; import { BatchDeletionSummaryDetailBuilder } from './builder'; import { ReferencesService, @@ -46,13 +47,7 @@ export class BatchDeletionUc { ); } - const summary: BatchDeletionSummary = { - executionTimeMilliseconds: endTime - startTime, - overallStatus: BatchDeletionSummaryOverallStatus.FAILURE, - successCount: 0, - failureCount: 0, - details: [], - }; + const summary: BatchDeletionSummary = BatchDeletionSummaryBuilder.build(endTime - startTime); // Go through every received output and, in case of an error presence increase // a failure count or, in case of no error, increase a success count. From 3bf6813b3a76008c1cd30a6b98b1358d2b0f79f3 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Mon, 13 Nov 2023 18:00:30 +0100 Subject: [PATCH 71/72] fix some imports/exports --- apps/server/src/modules/deletion/uc/batch-deletion.uc.ts | 3 +-- apps/server/src/modules/deletion/uc/builder/index.ts | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts b/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts index f435f2f4338..258b1b53f65 100644 --- a/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts +++ b/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts @@ -1,6 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { BatchDeletionSummaryBuilder } from '@modules/deletion/uc/builder/batch-deletion-summary.builder'; -import { BatchDeletionSummaryDetailBuilder } from './builder'; +import { BatchDeletionSummaryBuilder, BatchDeletionSummaryDetailBuilder } from './builder'; import { ReferencesService, BatchDeletionService, diff --git a/apps/server/src/modules/deletion/uc/builder/index.ts b/apps/server/src/modules/deletion/uc/builder/index.ts index ddf3346b705..46733980f94 100644 --- a/apps/server/src/modules/deletion/uc/builder/index.ts +++ b/apps/server/src/modules/deletion/uc/builder/index.ts @@ -1 +1,2 @@ export * from './batch-deletion-summary-detail.builder'; +export * from './batch-deletion-summary.builder'; From cfa6ac9e14968dcf6ec3b833e02fb7eb5a27c7a7 Mon Sep 17 00:00:00 2001 From: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Date: Wed, 15 Nov 2023 11:54:00 +0100 Subject: [PATCH 72/72] refactor console app flow --- apps/server/src/apps/deletion-console.app.ts | 47 ++++++++++---------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/apps/server/src/apps/deletion-console.app.ts b/apps/server/src/apps/deletion-console.app.ts index 0ef823ae42a..cafb137e160 100644 --- a/apps/server/src/apps/deletion-console.app.ts +++ b/apps/server/src/apps/deletion-console.app.ts @@ -2,29 +2,30 @@ import { BootstrapConsole } from 'nestjs-console'; import { DeletionConsoleModule } from '@modules/deletion'; -const bootstrap = new BootstrapConsole({ - module: DeletionConsoleModule, - useDecorators: true, -}); +async function run() { + const bootstrap = new BootstrapConsole({ + module: DeletionConsoleModule, + useDecorators: true, + }); -const logErrorAndSetExitCode = (err: unknown) => { - // eslint-disable-next-line no-console - console.error(err); - process.exitCode = 1; -}; + const app = await bootstrap.init(); -bootstrap - .init() - .then(async (app) => { - // eslint-disable-next-line promise/always-return - try { - await app.init(); - await bootstrap.boot(); - await app.close(); - } catch (err) { - await app.close(); + try { + await app.init(); - logErrorAndSetExitCode(err); - } - }) - .catch((err) => logErrorAndSetExitCode(err)); + // Execute console application with provided arguments. + await bootstrap.boot(); + } catch (err) { + // eslint-disable-next-line no-console, @typescript-eslint/no-unsafe-call + console.error(err); + + // Set the exit code to 1 to indicate a console app failure. + process.exitCode = 1; + } + + // Always close the app, even if some exception + // has been thrown from the console app. + await app.close(); +} + +void run();