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(); + } +}