From 6c1076d32f9fbb66e495651c827c0c809d17f24d Mon Sep 17 00:00:00 2001 From: Szymon Szafoni Date: Sun, 14 Jan 2024 21:21:58 +0100 Subject: [PATCH] impl --- .../deletion/uc/deletion-request.uc.ts | 17 ++++++- .../news/controller/dto/news.response.ts | 2 +- .../src/modules/news/mapper/news.mapper.ts | 9 ++-- .../src/modules/news/uc/news.uc.spec.ts | 48 ++++++++++++++++++- apps/server/src/modules/news/uc/news.uc.ts | 48 ++++++++++++++++++- .../shared/domain/entity/news.entity.spec.ts | 31 ++++++++++++ .../src/shared/domain/entity/news.entity.ts | 12 +++-- apps/server/src/shared/domain/types/domain.ts | 1 + .../repo/news/news.repo.integration.spec.ts | 36 ++++++++++++-- 9 files changed, 191 insertions(+), 13 deletions(-) create mode 100644 apps/server/src/shared/domain/entity/news.entity.spec.ts 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 16973af5a2c..6211f09698b 100644 --- a/apps/server/src/modules/deletion/uc/deletion-request.uc.ts +++ b/apps/server/src/modules/deletion/uc/deletion-request.uc.ts @@ -21,6 +21,7 @@ import { DeletionRequestBodyProps, DeletionRequestLogResponse, DeletionRequestRe import { DeletionRequest, DeletionLog } from '../domain'; import { DeletionOperationModel, DeletionStatusModel } from '../domain/types'; import { DeletionRequestService, DeletionLogService } from '../services'; +import { NewsUc } from '@src/modules/news/uc'; @Injectable() export class DeletionRequestUc { @@ -42,7 +43,8 @@ export class DeletionRequestUc { private readonly registrationPinService: RegistrationPinService, private readonly filesStorageClientAdapterService: FilesStorageClientAdapterService, private readonly dashboardService: DashboardService, - private readonly taskService: TaskService + private readonly taskService: TaskService, + private readonly newsUc: NewsUc ) { this.logger.setContext(DeletionRequestUc.name); } @@ -114,6 +116,7 @@ export class DeletionRequestUc { this.removeUserRegistrationPin(deletionRequest), this.removeUsersDashboard(deletionRequest), this.removeUserFromTasks(deletionRequest), + this.removeUsersDataFromNews(deletionRequest), ]); await this.deletionRequestService.markDeletionRequestAsExecuted(deletionRequest.id); } catch (error) { @@ -295,4 +298,16 @@ export class DeletionRequestUc { tasksDeleted.deletedCount ); } + + private async removeUsersDataFromNews(deletionRequest: DeletionRequest) { + const newsesModifiedByRemoveCreator = await this.newsUc.deleteCreatorReference(deletionRequest.targetRefId); + + await this.logDeletion( + deletionRequest, + DomainModel.NEWS, + DeletionOperationModel.UPDATE, + newsesModifiedByRemoveCreator, + 0 + ); + } } diff --git a/apps/server/src/modules/news/controller/dto/news.response.ts b/apps/server/src/modules/news/controller/dto/news.response.ts index 766f40e19c2..2224c72b4e6 100644 --- a/apps/server/src/modules/news/controller/dto/news.response.ts +++ b/apps/server/src/modules/news/controller/dto/news.response.ts @@ -103,7 +103,7 @@ export class NewsResponse { @ApiProperty({ description: 'Reference to the User that created the News entity', }) - creator: UserInfoResponse; + creator: UserInfoResponse | undefined; @ApiPropertyOptional({ description: 'Reference to the User that updated the News entity', diff --git a/apps/server/src/modules/news/mapper/news.mapper.ts b/apps/server/src/modules/news/mapper/news.mapper.ts index 30bf13e6540..ef27426b326 100644 --- a/apps/server/src/modules/news/mapper/news.mapper.ts +++ b/apps/server/src/modules/news/mapper/news.mapper.ts @@ -1,7 +1,7 @@ -import { News } from '@shared/domain/entity'; +import { News, User } from '@shared/domain/entity'; import { CreateNews, INewsScope, IUpdateNews, NewsTargetModel } from '@shared/domain/types'; import { LogMessageData } from '@src/core/logger'; -import { CreateNewsParams, FilterNewsParams, NewsResponse, UpdateNewsParams } from '../controller/dto'; +import { CreateNewsParams, FilterNewsParams, NewsResponse, UpdateNewsParams, UserInfoResponse } from '../controller/dto'; import { SchoolInfoMapper } from './school-info.mapper'; import { TargetInfoMapper } from './target-info.mapper'; import { UserInfoMapper } from './user-info.mapper'; @@ -10,7 +10,10 @@ export class NewsMapper { static mapToResponse(news: News): NewsResponse { const target = TargetInfoMapper.mapToResponse(news.target); const school = SchoolInfoMapper.mapToResponse(news.school); - const creator = UserInfoMapper.mapToResponse(news.creator); + let creator: UserInfoResponse | undefined; + if (news.creator !== undefined) { + creator = UserInfoMapper.mapToResponse(news.creator); + } const dto = new NewsResponse({ id: news.id, diff --git a/apps/server/src/modules/news/uc/news.uc.spec.ts b/apps/server/src/modules/news/uc/news.uc.spec.ts index 45250a28935..c828434f765 100644 --- a/apps/server/src/modules/news/uc/news.uc.spec.ts +++ b/apps/server/src/modules/news/uc/news.uc.spec.ts @@ -4,10 +4,11 @@ import { FeathersAuthorizationService } from '@modules/authorization'; import { UnauthorizedException } from '@nestjs/common'; import { NotFoundException } from '@nestjs/common/exceptions/not-found.exception'; import { Test, TestingModule } from '@nestjs/testing'; -import { Permission } from '@shared/domain/interface'; +import { Permission, RoleName } from '@shared/domain/interface'; import { CreateNews, NewsTargetModel } from '@shared/domain/types'; import { NewsRepo } from '@shared/repo'; import { Logger } from '@src/core/logger'; +import { FederalStateEntity, Role, SchoolEntity, TeamNews, User } from '@shared/domain/entity'; import { NewsUc } from './news.uc'; describe('NewsUc', () => { @@ -40,6 +41,35 @@ describe('NewsUc', () => { id: courseTargetId, }, }; + const federalState = new FederalStateEntity({ + name: 'string', + abbreviation: 'string', + logoUrl: 'string', + createdAt: new Date(), + updatedAt: new Date(), + }); + + const school = new SchoolEntity({ + _id: schoolId, + + name: 'string', + federalState, + }); + const creator = new User({ + email: 'string', + firstName: 'string', + lastName: 'string', + roles: [new Role({ name: RoleName.TEACHER })] as Role[], + school, + }); + const exampleNews: TeamNews = new TeamNews({ + title: 'string', + content: 'string', + displayAt, + creator, + school: schoolId, + target: teamTargetId, + }); const pagination = {}; const targets = [ @@ -75,6 +105,9 @@ describe('NewsUc', () => { } throw new NotFoundException(); }, + findByCreatorId() { + return [[exampleNews], 1]; + }, delete() {}, }, }, @@ -301,4 +334,17 @@ describe('NewsUc', () => { await expect(service.delete(newsId, anotherUser)).rejects.toThrow(UnauthorizedException); }); }); + + describe('deleteCreatorReference', () => { + it('should successfully delete creator reference from news', async () => { + const result = await service.deleteCreatorReference(userId); + expect(exampleNews.creator).toBeUndefined(); + expect(result).toBe(1); + }); + + it('should throw Unauthorized exception if user doesnt have permission NEWS_EDIT', async () => { + const anotherUser = new ObjectId().toHexString(); + await expect(service.delete(newsId, anotherUser)).rejects.toThrow(UnauthorizedException); + }); + }); }); diff --git a/apps/server/src/modules/news/uc/news.uc.ts b/apps/server/src/modules/news/uc/news.uc.ts index 2983010fcb6..aba150fe47e 100644 --- a/apps/server/src/modules/news/uc/news.uc.ts +++ b/apps/server/src/modules/news/uc/news.uc.ts @@ -2,10 +2,20 @@ import { FeathersAuthorizationService } from '@modules/authorization'; import { Injectable } from '@nestjs/common'; import { News } from '@shared/domain/entity'; import { IFindOptions, Permission, SortOrder } from '@shared/domain/interface'; -import { Counted, CreateNews, EntityId, INewsScope, IUpdateNews, NewsTargetModel } from '@shared/domain/types'; +import { + Counted, + CreateNews, + DomainModel, + EntityId, + INewsScope, + IUpdateNews, + NewsTargetModel, + StatusModel, +} from '@shared/domain/types'; import { NewsRepo, NewsTargetFilter } from '@shared/repo'; import { CrudOperation } from '@shared/types'; import { Logger } from '@src/core/logger'; +import { DataDeletionDomainOperationLoggable } from '@shared/common/loggable'; import { NewsCrudOperationLoggable } from '../loggable/news-crud-operation.loggable'; type NewsPermission = Permission.NEWS_VIEW | Permission.NEWS_EDIT; @@ -147,6 +157,42 @@ export class NewsUc { return id; } + public async deleteCreatorReference(creatorId: EntityId): Promise { + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Deleting user data from News', + DomainModel.NEWS, + creatorId, + StatusModel.PENDING + ) + ); + + const news = await this.newsRepo.findByCreatorId(creatorId); + + const newsCount = news[1]; + if (newsCount === 0) { + return 0; + } + + news[0].forEach((newsEntity) => { + newsEntity.removeCreatorReference(creatorId); + }); + + await this.newsRepo.save(news[0]); + + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Successfully removed user data from News', + DomainModel.NEWS, + creatorId, + StatusModel.FINISHED, + newsCount, + 0 + ) + ); + return newsCount; + } + private async getPermittedTargets(userId: EntityId, scope: INewsScope | undefined, permissions: NewsPermission[]) { let targets: NewsTargetFilter[]; diff --git a/apps/server/src/shared/domain/entity/news.entity.spec.ts b/apps/server/src/shared/domain/entity/news.entity.spec.ts new file mode 100644 index 00000000000..9ceb65ddd49 --- /dev/null +++ b/apps/server/src/shared/domain/entity/news.entity.spec.ts @@ -0,0 +1,31 @@ +import { setupEntities, teamNewsFactory, userFactory } from '@shared/testing'; +import { News } from './news.entity'; + +describe(News.name, () => { + beforeAll(async () => { + await setupEntities(); + }); + + describe('removeCreatorReference', () => { + describe('when called on a news that contains some creator with given refId', () => { + const setup = () => { + const creator = userFactory.buildWithId(); + const news = teamNewsFactory.build({ + creator, + }); + + const expectedNews = news; + expectedNews.creator = undefined; + + return { creator, news, expectedNews }; + }; + it('should properly remove this creator reference', () => { + const { creator, news, expectedNews } = setup(); + + news.removeCreatorReference(creator.id); + + expect(news).toEqual(expectedNews); + }); + }); + }); +}); diff --git a/apps/server/src/shared/domain/entity/news.entity.ts b/apps/server/src/shared/domain/entity/news.entity.ts index 682192770a1..cd77d0b35cf 100644 --- a/apps/server/src/shared/domain/entity/news.entity.ts +++ b/apps/server/src/shared/domain/entity/news.entity.ts @@ -12,7 +12,7 @@ export interface NewsProperties { content: string; displayAt: Date; school: EntityId | SchoolEntity; - creator: EntityId | User; + creator?: EntityId | User; target: EntityId | NewsTarget; externalId?: string; @@ -61,14 +61,20 @@ export abstract class News extends BaseEntityWithTimestamps { @ManyToOne(() => SchoolEntity, { fieldName: 'schoolId' }) school!: SchoolEntity; - @ManyToOne('User', { fieldName: 'creatorId' }) - creator!: User; + @ManyToOne('User', { fieldName: 'creatorId', nullable: true }) + creator?: User | undefined; @ManyToOne('User', { fieldName: 'updaterId', nullable: true }) updater?: User; permissions: string[] = []; + public removeCreatorReference(creatorId: EntityId): void { + if (creatorId === this.creator?.id) { + this.creator = undefined; + } + } + constructor(props: NewsProperties) { super(); this.title = props.title; diff --git a/apps/server/src/shared/domain/types/domain.ts b/apps/server/src/shared/domain/types/domain.ts index babc23631d1..117fd5e425f 100644 --- a/apps/server/src/shared/domain/types/domain.ts +++ b/apps/server/src/shared/domain/types/domain.ts @@ -13,4 +13,5 @@ export const enum DomainModel { TASK = 'task', TEAMS = 'teams', USER = 'user', + NEWS = 'news', } diff --git a/apps/server/src/shared/repo/news/news.repo.integration.spec.ts b/apps/server/src/shared/repo/news/news.repo.integration.spec.ts index 41df6d26b7c..072762438db 100644 --- a/apps/server/src/shared/repo/news/news.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/news/news.repo.integration.spec.ts @@ -223,8 +223,9 @@ describe('NewsRepo', () => { targetIds: [news.target.id], }; const pagination = { skip: 0, limit: 20 }; + const creatorId = news.creator?.id as string; - const [result, count] = await repo.findAllUnpublishedByUser([target], news.creator.id, { pagination }); + const [result, count] = await repo.findAllUnpublishedByUser([target], creatorId, { pagination }); expect(count).toBeGreaterThanOrEqual(result.length); expect(result.length).toEqual(1); @@ -241,7 +242,8 @@ describe('NewsRepo', () => { targetModel: NewsTargetModel.School, targetIds: [news.target.id], }; - const [result, count] = await repo.findAllUnpublishedByUser([target], news.creator.id, { pagination }); + const creatorId = news.creator?.id as string; + const [result, count] = await repo.findAllUnpublishedByUser([target], creatorId, { pagination }); expect(count).toBeGreaterThanOrEqual(result.length); expect(result.length).toEqual(1); expect(result[0].id).toEqual(news.id); @@ -256,8 +258,9 @@ describe('NewsRepo', () => { targetModel: NewsTargetModel.Course, targetIds: [news.target.id], }; + const creatorId = news.creator?.id as string; const pagination = { skip: 0, limit: 20 }; - const [result, count] = await repo.findAllUnpublishedByUser([target], news.creator.id, { pagination }); + const [result, count] = await repo.findAllUnpublishedByUser([target], creatorId, { pagination }); expect(count).toBeGreaterThanOrEqual(result.length); expect(result.length).toEqual(1); expect(result[0].id).toEqual(news.id); @@ -301,4 +304,31 @@ describe('NewsRepo', () => { await expect(repo.findOneById(failNewsId)).rejects.toThrow(NotFoundError); }); }); + + describe('findByCreatorId', () => { + const setup = async () => { + const creator = userFactory.buildWithId(); + const news = teamNewsFactory.build({ + creator, + }); + + await em.persistAndFlush(news); + em.clear(); + + return { news, creator }; + }; + it('should find a news entity by creatorId', async () => { + const { news, creator } = await setup(); + + const result = await repo.findByCreatorId(creator.id); + expect(result).toBeDefined(); + expect(result[0][0].id).toEqual(news.id); + }); + + it('should throw an exception if not found', async () => { + const failNewsId = new ObjectId().toString(); + const result = await repo.findByCreatorId(failNewsId); + expect(result[1]).toEqual(0); + }); + }); });