From 5308c8ba81732dc036b03c3e96a1fb86ae8916d0 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Mon, 20 Nov 2023 17:31:43 +0100 Subject: [PATCH 1/4] basic implementation --- .../recursive-copy.visitor.ts | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/apps/server/src/modules/board/service/board-do-copy-service/recursive-copy.visitor.ts b/apps/server/src/modules/board/service/board-do-copy-service/recursive-copy.visitor.ts index 137f189319c..03cdfb15b69 100644 --- a/apps/server/src/modules/board/service/board-do-copy-service/recursive-copy.visitor.ts +++ b/apps/server/src/modules/board/service/board-do-copy-service/recursive-copy.visitor.ts @@ -1,3 +1,5 @@ +import { FileRecordParentType } from '@infra/rabbitmq'; +import { CopyElementType, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; import { AnyBoardDo, BoardCompositeVisitorAsync, @@ -12,8 +14,6 @@ import { SubmissionItem, } from '@shared/domain'; import { LinkElement } from '@shared/domain/domainobject/board/link-element.do'; -import { FileRecordParentType } from '@infra/rabbitmq'; -import { CopyElementType, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; import { ObjectId } from 'bson'; import { SchoolSpecificFileCopyService } from './school-specific-file-copy.interface'; @@ -133,11 +133,38 @@ export class RecursiveCopyVisitor implements BoardCompositeVisitorAsync { createdAt: new Date(), updatedAt: new Date(), }); - this.resultMap.set(original.id, { + + const result: CopyStatus = { copyEntity: copy, type: CopyElementType.LINK_ELEMENT, status: CopyStatusEnum.SUCCESS, - }); + }; + + if (original.imageUrl) { + const fileCopy = await this.fileCopyService.copyFilesOfParent({ + sourceParentId: original.id, + targetParentId: copy.id, + parentType: FileRecordParentType.BoardNode, + }); + fileCopy.forEach((copyFileDto) => { + if (copyFileDto.id) { + if (copy.imageUrl.includes(copyFileDto.sourceId)) { + copy.imageUrl = copy.imageUrl.replace(copyFileDto.sourceId, copyFileDto.id); + } else { + copy.imageUrl = ''; + } + } + }); + const fileCopyStatus = fileCopy.map((copyFileDto) => { + return { + type: CopyElementType.FILE, + status: copyFileDto.id ? CopyStatusEnum.SUCCESS : CopyStatusEnum.FAIL, + title: copyFileDto.name ?? `(old fileid: ${copyFileDto.sourceId})`, + }; + }); + result.elements = fileCopyStatus; + } + this.resultMap.set(original.id, result); this.copyMap.set(original.id, copy); return Promise.resolve(); From 459e0dd5bc2e8edbf674a1c6279706951229770c Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Tue, 21 Nov 2023 15:46:37 +0100 Subject: [PATCH 2/4] chore: test for recursive copy visitor for link element --- .../recursive-copy.visitor.spec.ts | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 apps/server/src/modules/board/service/board-do-copy-service/recursive-copy.visitor.spec.ts diff --git a/apps/server/src/modules/board/service/board-do-copy-service/recursive-copy.visitor.spec.ts b/apps/server/src/modules/board/service/board-do-copy-service/recursive-copy.visitor.spec.ts new file mode 100644 index 00000000000..bb0a7fda403 --- /dev/null +++ b/apps/server/src/modules/board/service/board-do-copy-service/recursive-copy.visitor.spec.ts @@ -0,0 +1,109 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { LinkElement } from '@shared/domain'; +import { linkElementFactory, setupEntities } from '@shared/testing'; +import { CopyFileDto } from '@src/modules/files-storage-client/dto'; + +import { RecursiveCopyVisitor } from './recursive-copy.visitor'; +import { SchoolSpecificFileCopyServiceFactory } from './school-specific-file-copy-service.factory'; +import { SchoolSpecificFileCopyService } from './school-specific-file-copy.interface'; + +describe(RecursiveCopyVisitor.name, () => { + let module: TestingModule; + let fileCopyServiceFactory: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + { + provide: SchoolSpecificFileCopyServiceFactory, + useValue: createMock(), + }, + RecursiveCopyVisitor, + ], + }).compile(); + + fileCopyServiceFactory = module.get(SchoolSpecificFileCopyServiceFactory); + + await setupEntities(); + }); + + describe('visitLinkElementAsync', () => { + const setup = (options: { withFileCopy: boolean } = { withFileCopy: false }) => { + const fileCopyServiceMock = createMock(); + fileCopyServiceFactory.build.mockReturnValue(fileCopyServiceMock); + const sourceFileId = 'abe223e22134'; + const imageUrl = `https://abc.de/file/${sourceFileId}`; + + let newFileId: string | undefined; + let copyResultMock: CopyFileDto[] = []; + if (options?.withFileCopy) { + newFileId = 'bbbbbbb123'; + copyResultMock = [ + { + sourceId: sourceFileId, + id: newFileId, + name: 'myfile.jpg', + }, + ]; + } + + fileCopyServiceMock.copyFilesOfParent.mockResolvedValueOnce(copyResultMock); + return { fileCopyServiceMock, imageUrl, newFileId }; + }; + + describe('when copying a LinkElement without preview url', () => { + it('should not call fileCopyService', async () => { + const { fileCopyServiceMock } = setup(); + + const linkElement = linkElementFactory.build(); + const visitor = new RecursiveCopyVisitor(fileCopyServiceMock); + + await visitor.visitLinkElementAsync(linkElement); + + expect(fileCopyServiceMock.copyFilesOfParent).not.toHaveBeenCalled(); + }); + }); + + describe('when copying a LinkElement with preview image', () => { + it('should call fileCopyService', async () => { + const { fileCopyServiceMock, imageUrl } = setup({ withFileCopy: true }); + + const linkElement = linkElementFactory.build({ imageUrl }); + const visitor = new RecursiveCopyVisitor(fileCopyServiceMock); + + await visitor.visitLinkElementAsync(linkElement); + + expect(fileCopyServiceMock.copyFilesOfParent).toHaveBeenCalledWith( + expect.objectContaining({ sourceParentId: linkElement.id }) + ); + }); + + it('should replace fileId in imageUrl', async () => { + const { fileCopyServiceMock, imageUrl, newFileId } = setup({ withFileCopy: true }); + + const linkElement = linkElementFactory.build({ imageUrl }); + const visitor = new RecursiveCopyVisitor(fileCopyServiceMock); + + await visitor.visitLinkElementAsync(linkElement); + const copy = visitor.copyMap.get(linkElement.id) as LinkElement; + + expect(copy.imageUrl).toEqual(expect.stringContaining(newFileId as string)); + }); + }); + + describe('when copying a LinkElement with an unmatched image url', () => { + it('should remove the imageUrl', async () => { + const { fileCopyServiceMock } = setup({ withFileCopy: true }); + + const linkElement = linkElementFactory.build({ imageUrl: `https://abc.de/file/unknown-file-id` }); + const visitor = new RecursiveCopyVisitor(fileCopyServiceMock); + + await visitor.visitLinkElementAsync(linkElement); + const copy = visitor.copyMap.get(linkElement.id) as LinkElement; + + expect(copy.imageUrl).toEqual(''); + }); + }); + }); +}); From 31fe72a87f608f5a3b47792b62f090e5c7c15376 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Tue, 21 Nov 2023 15:50:53 +0100 Subject: [PATCH 3/4] recursivce-delete-visitor: delete fiels of linkElement --- apps/server/src/modules/board/repo/recursive-delete.vistor.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/server/src/modules/board/repo/recursive-delete.vistor.ts b/apps/server/src/modules/board/repo/recursive-delete.vistor.ts index 0a1b08e663a..6c8301f6b6f 100644 --- a/apps/server/src/modules/board/repo/recursive-delete.vistor.ts +++ b/apps/server/src/modules/board/repo/recursive-delete.vistor.ts @@ -49,6 +49,7 @@ export class RecursiveDeleteVisitor implements BoardCompositeVisitorAsync { } async visitLinkElementAsync(linkElement: LinkElement): Promise { + await this.filesStorageClientAdapterService.deleteFilesOfParent(linkElement.id); this.deleteNode(linkElement); await this.visitChildrenAsync(linkElement); From 6efecc50acbf73d51d45ef7eda422cb789c2cef0 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Wed, 22 Nov 2023 09:56:05 +0100 Subject: [PATCH 4/4] chore: add test for added deleteFilesOfParent --- .../board/repo/recursive-delete.visitor.spec.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/server/src/modules/board/repo/recursive-delete.visitor.spec.ts b/apps/server/src/modules/board/repo/recursive-delete.visitor.spec.ts index 4b0688f0a0d..544d492fe65 100644 --- a/apps/server/src/modules/board/repo/recursive-delete.visitor.spec.ts +++ b/apps/server/src/modules/board/repo/recursive-delete.visitor.spec.ts @@ -1,9 +1,9 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { FileRecordParentType } from '@infra/rabbitmq'; import { EntityManager } from '@mikro-orm/mongodb'; import { FileDto, FilesStorageClientAdapterService } from '@modules/files-storage-client'; import { ContextExternalToolService } from '@modules/tool/context-external-tool/service'; import { Test, TestingModule } from '@nestjs/testing'; -import { FileRecordParentType } from '@infra/rabbitmq'; import { columnBoardFactory, columnFactory, @@ -171,6 +171,14 @@ describe(RecursiveDeleteVisitor.name, () => { expect(em.remove).toHaveBeenCalledWith(em.getReference(linkElement.constructor, linkElement.id)); expect(em.remove).toHaveBeenCalledWith(em.getReference(childLinkElement.constructor, childLinkElement.id)); }); + + it('should call deleteFilesOfParent', async () => { + const { linkElement } = setup(); + + await service.visitLinkElementAsync(linkElement); + + expect(filesStorageClientAdapterService.deleteFilesOfParent).toHaveBeenCalledWith(linkElement.id); + }); }); describe('visitSubmissionContainerElementAsync', () => {