From e50d2b4780302f5fecf5c2b5be71f27e2d294bbb Mon Sep 17 00:00:00 2001 From: virgilchiriac <17074330+virgilchiriac@users.noreply.github.com> Date: Fri, 14 Jun 2024 14:11:30 +0200 Subject: [PATCH] BC-6871 - Refactor board persistence and domain model (#5030) --------- Co-authored-by: Uwe Ilgenstein --- .github/workflows/test.yml | 2 +- .../database-management.service.spec.ts | 6 +- .../mikro-orm/Migration20240415124640.ts | 4 +- .../mikro-orm/Migration20240517135008.ts | 4 +- .../mikro-orm/Migration20240528140356.ts | 31 + .../authorization/authorization.module.ts | 4 +- ...o.rule.spec.ts => board-node.rule.spec.ts} | 317 +++--- .../{board-do.rule.ts => board-node.rule.ts} | 52 +- .../authorization/domain/rules/index.ts | 2 +- .../authorization-reference.service.spec.ts | 10 +- .../domain/service/reference.loader.spec.ts | 10 +- .../domain/service/reference.loader.ts | 14 +- .../domain/service/rule-manager.spec.ts | 19 +- .../domain/service/rule-manager.ts | 6 +- .../src/modules/board/board-api.module.ts | 5 +- .../board/board-collaboration.module.ts | 2 - .../board-collaboration.testing.module.ts | 2 - .../src/modules/board/board-ws-api.module.ts | 3 +- apps/server/src/modules/board/board.module.ts | 73 +- .../api-test/board-context.api.spec.ts | 21 +- .../api-test/board-copy.api.spec.ts | 21 +- .../api-test/board-create.api.spec.ts | 10 +- .../api-test/board-delete.api.spec.ts | 122 +-- .../api-test/board-lookup.api.spec.ts | 28 +- .../api-test/board-update-title.api.spec.ts | 23 +- .../api-test/board-visibility.api.spec.ts | 21 +- .../api-test/card-create.api.spec.ts | 16 +- .../api-test/card-delete.api.spec.ts | 144 +-- .../api-test/card-lookup.api.spec.ts | 162 ++- .../controller/api-test/card-move.api.spec.ts | 176 ++-- .../api-test/card-update-height.api.spec.ts | 23 +- .../api-test/card-update-title.api.spec.ts | 45 +- .../api-test/column-create.api.spec.ts | 13 +- .../api-test/column-delete.api.spec.ts | 132 +-- .../api-test/column-move.api.spec.ts | 129 +-- .../api-test/column-update-title.api.spec.ts | 28 +- .../content-element-create.api.spec.ts | 101 +- .../content-element-delete.api.spec.ts | 47 +- .../api-test/content-element-move.api.spec.ts | 152 +-- .../content-element-update-content.spec.ts | 78 +- .../drawing-item-check-permission.api.spec.ts | 33 +- .../submission-item-create.api.spec.ts | 48 +- .../submission-item-delete.api.spec.ts | 77 +- .../submission-item-lookup.api.spec.ts | 104 +- .../submission-item-update.api.spec.ts | 82 +- .../board/controller/board.controller.ts | 2 +- .../dto/board/board-context.reponse.ts | 2 +- .../controller/dto/board/board.response.ts | 4 +- .../controller/dto/board/column.response.ts | 4 +- .../dto/board/create-board.body.params.ts | 5 +- .../dto/card/create-card.body.params.ts | 2 +- ...laborative-text-editor-element.response.ts | 2 +- .../create-content-element.body.params.ts | 2 +- .../dto/element/drawing-element.response.ts | 2 +- .../element/external-tool-element.response.ts | 2 +- .../dto/element/file-element.response.ts | 2 +- .../dto/element/link-element.response.ts | 2 +- .../dto/element/rich-text-element.response.ts | 2 +- .../submission-container-element.response.ts | 2 +- .../update-element-content.body.params.ts | 2 +- .../mapper/base-mapper.interface.ts | 4 +- .../mapper/board-response.mapper.ts | 2 +- .../controller/mapper/card-response.mapper.ts | 2 +- ...ive-text-editor-element-response.mapper.ts | 3 +- .../mapper/column-response.mapper.ts | 4 +- .../content-element-response.factory.spec.ts | 2 +- .../content-element-response.factory.ts | 4 +- .../mapper/create-board-response.mapper.ts | 2 +- .../mapper/drawing-element-response.mapper.ts | 3 +- .../external-tool-element-response.mapper.ts | 2 +- .../mapper/file-element-response.mapper.ts | 2 +- .../mapper/link-element-response.mapper.ts | 2 +- .../rich-text-element-response.mapper.ts | 2 +- ...ssion-container-element-response.mapper.ts | 4 +- .../mapper/submission-item-response.mapper.ts | 2 +- .../api-test/media-board.api.spec.ts | 145 ++- .../api-test/media-element.api.spec.ts | 98 +- .../api-test/media-line.api.spec.ts | 110 +- .../media-board/dto/color.body.params.ts | 2 +- .../media-board/dto/layout.body.params.ts | 11 +- .../dto/media-available-line.response.ts | 2 +- .../media-board/dto/media-board.response.ts | 6 +- .../media-board/dto/media-line.response.ts | 2 +- .../media-available-line-response.mapper.ts | 2 +- .../mapper/media-board-response.mapper.ts | 4 +- ...a-external-tool-element-response.mapper.ts | 2 +- .../mapper/media-line-response.mapper.ts | 9 +- .../media-board/media-board.controller.ts | 2 +- .../media-board/media-element.controller.ts | 2 +- .../domain/board-node-authorizable.do.ts | 51 + .../board/domain/board-node.do.spec.ts | 334 ++++++ .../src/modules/board/domain/board-node.do.ts | 114 ++ .../board/domain/board-node.factory.ts | 115 ++ .../src/modules/board/domain/card.do.ts | 26 + .../collaborative-text-editor.do.spec.ts | 35 + .../domain/collaborative-text-editor.do.ts | 11 + .../modules/board/domain/colum-board.do.ts | 40 + .../src/modules/board/domain/column.do.ts | 19 + .../board/domain/drawing-element.do.spec.ts | 44 + .../board/domain/drawing-element.do.ts | 19 + .../domain/external-tool-element.do.spec.ts | 44 + .../board/domain/external-tool-element.do.ts | 19 + .../board/domain/file-element.do.spec.ts | 54 + .../modules/board/domain/file-element.do.ts | 26 + apps/server/src/modules/board/domain/index.ts | 19 +- .../modules/board/domain/interface/index.ts | 2 - .../domain/interface/layout-type.enum.ts | 4 - .../board/domain/link-element.do.spec.ts | 74 ++ .../modules/board/domain/link-element.do.ts | 42 + .../board/domain}/media-board/index.ts | 12 +- .../media-available-line-element.do.ts | 1 + .../media-board/media-available-line.do.ts | 4 +- .../media-board-node-factory.spec.ts | 47 + .../media-board/media-board-node-factory.ts | 48 + .../domain/media-board/media-board.do.ts | 42 + .../media-external-tool-element.do.spec.ts | 39 + .../media-external-tool-element.do.ts | 18 + .../board/domain/media-board/media-line.do.ts | 40 + .../media-board/types/any-media-board-node.ts | 15 + .../board/domain/media-board/types/index.ts | 2 + .../types}/media-colors.enum.ts | 0 .../src/modules/board/domain/path-utils.ts | 9 + .../board/domain/rich-text-element.do.spec.ts | 55 + .../board/domain/rich-text-element.do.ts | 28 + .../domain/submission-container-element.do.ts | 21 + .../board/domain/submission-item.do.spec.ts | 87 ++ .../board/domain/submission-item.do.ts | 33 + .../modules/board/domain/type-mapping.spec.ts | 52 + .../src/modules/board/domain/type-mapping.ts | 51 + .../board/domain/types/any-board-node.ts | 16 + .../board/domain/types/any-content-element.ts | 30 + .../domain}/types/board-external-reference.ts | 4 +- .../board/domain}/types/board-layout.enum.ts | 1 + .../board/domain/types/board-node-props.ts | 100 ++ .../domain/types/board-node-type.enum.ts} | 0 .../types/content-element-type.enum.ts} | 0 .../src/modules/board/domain/types/index.ts | 7 + .../board-collaboration.gateway.spec.ts | 35 +- .../gateway/board-collaboration.gateway.ts | 20 +- .../create-content-element.message.param.ts | 2 +- apps/server/src/modules/board/index.ts | 33 +- ...alid-board-type.loggable-exception.spec.ts | 2 +- .../invalid-board-type.loggable-exception.ts | 2 +- .../modules/board/media-board-api.module.ts | 14 +- .../src/modules/board/media-board.module.ts | 8 +- .../board/repo/board-do.builder-impl.spec.ts | 410 -------- .../board/repo/board-do.builder-impl.ts | 310 ------ .../modules/board/repo/board-do.repo.spec.ts | 661 ------------ .../src/modules/board/repo/board-do.repo.ts | 139 --- .../board/repo/board-node.repo.spec.ts | 367 ++++--- .../src/modules/board/repo/board-node.repo.ts | 193 +++- .../repo/entity/board-node.entity.spec.ts | 70 ++ .../board/repo/entity/board-node.entity.ts | 110 ++ .../board/repo/entity/embeddables/context.ts | 26 + .../board/repo/entity/embeddables/index.ts | 1 + .../src/modules/board/repo/entity/index.ts | 1 + apps/server/src/modules/board/repo/index.ts | 3 +- .../repo/recursive-delete.visitor.spec.ts | 528 ---------- .../board/repo/recursive-delete.vistor.ts | 147 --- .../board/repo/recursive-save.visitor.spec.ts | 404 ------- .../board/repo/recursive-save.visitor.ts | 294 ------ .../src/modules/board/repo/tree-builder.ts | 46 + .../repo/types/board-node-entity-props.ts | 58 + .../src/modules/board/repo/types/index.ts | 1 + .../service/board-common-tool.service.spec.ts | 96 ++ .../service/board-common-tool.service.ts | 35 + .../board-do-authorizable.service.spec.ts | 277 ----- .../service/board-do-authorizable.service.ts | 104 -- .../board-do-copy.service.spec.ts | 990 ------------------ .../board-do-copy.service.ts | 34 - .../service/board-do-copy-service/index.ts | 3 - .../recursive-copy.visitor.spec.ts | 351 ------- .../recursive-copy.visitor.ts | 369 ------- ...specific-file-copy-service.factory.spec.ts | 112 -- ...hool-specific-file-copy-service.factory.ts | 16 - .../school-specific-file-copy.interface.ts | 19 - .../school-specific-file-copy.service.ts | 30 - .../swap-internal-links.visitor.spec.ts | 168 --- .../swap-internal-links.visitor.ts | 82 -- .../board/service/board-do.service.spec.ts | 260 ----- .../modules/board/service/board-do.service.ts | 49 - .../board-node-authorizable.service.spec.ts | 140 +++ .../board-node-authorizable.service.ts | 43 + .../board-node-permission.service.spec.ts | 166 +++ .../service/board-node-permission.service.ts | 46 + .../board/service/board-node.service.spec.ts | 150 +++ .../board/service/board-node.service.ts | 133 +++ .../board/service/card.service.spec.ts | 299 ------ .../src/modules/board/service/card.service.ts | 97 -- .../service/column-board-copy.service.spec.ts | 341 ------ .../service/column-board-copy.service.ts | 91 -- .../service/column-board.service.spec.ts | 271 ++--- .../board/service/column-board.service.ts | 99 +- .../board/service/column.service.spec.ts | 189 ---- .../modules/board/service/column.service.ts | 66 -- .../content-element-update.visitor.spec.ts | 291 ----- .../service/content-element-update.visitor.ts | 143 --- .../service/content-element.service.spec.ts | 429 -------- .../board/service/content-element.service.ts | 77 -- ...user-deleted-event-handler.service.spec.ts | 29 +- .../user-deleted-event-handler.service.ts | 20 +- .../server/src/modules/board/service/index.ts | 12 +- .../internal/board-context.service.spec.ts | 206 ++++ .../service/internal/board-context.service.ts | 70 ++ .../internal/board-node-copy-context.spec.ts | 45 + .../internal/board-node-copy-context.ts | 32 + .../board-node-copy-general.service.spec.ts | 266 +++++ .../board-node-copy-specific.service.spec.ts | 600 +++++++++++ .../internal/board-node-copy.service.ts | 410 ++++++++ .../board-node-delete-hooks.service.spec.ts | 165 +++ .../board-node-delete-hooks.service.ts | 96 ++ .../column-board-copy.service.spec.ts | 110 ++ .../internal/column-board-copy.service.ts | 66 ++ .../column-board-link.service.spec.ts | 100 ++ .../internal/column-board-link.service.ts | 28 + .../column-board-reference.service.ts | 16 + .../internal/column-board-title.service.ts | 26 + .../content-element-update.service.spec.ts | 139 +++ .../content-element-update.service.ts | 98 ++ .../modules/board/service/internal/index.ts | 9 + .../board/service/media-board/index.ts | 2 - .../media-available-line.service.spec.ts | 33 +- .../media-available-line.service.ts | 21 +- .../media-board/media-board.service.spec.ts | 346 ++---- .../media-board/media-board.service.ts | 128 ++- .../media-board/media-element.service.spec.ts | 308 ------ .../media-board/media-element.service.ts | 94 -- .../media-board/media-line.service.spec.ts | 243 ----- .../service/media-board/media-line.service.ts | 68 -- .../service/submission-item.service.spec.ts | 198 ---- .../board/service/submission-item.service.ts | 79 -- .../board-node-authorizable.factory.ts | 19 + .../board/testing/card.factory.ts} | 17 +- .../collaborative-text-editor.factory.ts | 18 + .../board/testing/column-board.factory.ts} | 21 +- .../board/testing/column.factory.ts} | 11 +- .../board/testing/drawing-element.factory.ts} | 11 +- .../entity/board-node-entity.factory.ts | 170 +++ .../testing/entity/card-entity.factory.ts | 21 + ...ollaborative-text-editor-entity.factory.ts | 19 + .../entity/column-board-entity.factory.ts | 42 + .../testing/entity/column-entity.factory.ts | 19 + .../entity/drawing-element-entity.factory.ts | 21 + .../external-tool-element-entity.factory.ts | 21 + .../entity/file-element-entity.factory.ts | 20 + .../src/modules/board/testing/entity/index.ts | 14 + .../entity/link-element-entity.factory.ts | 22 + .../entity/media-board-entity.factory.ts | 41 + ...ia-external-tool-element-entity.factory.ts | 21 + .../entity/media-line-entity.factory.ts | 21 + .../rich-text-element-entity.factory.ts | 22 + ...ission-container-element-entity.factory.ts | 21 + .../entity/submission-item-entity.factory.ts | 18 + .../testing/external-tool-element.factory.ts} | 8 +- .../board/testing/file-element.factory.ts} | 12 +- .../server/src/modules/board/testing/index.ts | 18 + .../board/testing/link-element.factory.ts | 19 + .../media-available-line-element.factory.ts} | 4 +- .../testing/media-available-line.factory.ts} | 5 +- .../board/testing/media-board.factory.ts | 31 + .../media-external-tool-element.factory.ts} | 11 +- .../board/testing/media-line.factory.ts | 21 + .../testing/rich-text-element.factory.ts} | 12 +- .../submission-container-element.factory.ts} | 12 +- .../board/testing/submission-item.factory.ts | 17 + apps/server/src/modules/board/uc/base.uc.ts | 43 - .../src/modules/board/uc/board.uc.spec.ts | 405 +++++-- apps/server/src/modules/board/uc/board.uc.ts | 101 +- .../src/modules/board/uc/card.uc.spec.ts | 264 ++--- apps/server/src/modules/board/uc/card.uc.ts | 103 +- .../src/modules/board/uc/column.uc.spec.ts | 146 ++- apps/server/src/modules/board/uc/column.uc.ts | 69 +- .../src/modules/board/uc/element.uc.spec.ts | 305 +++--- .../server/src/modules/board/uc/element.uc.ts | 80 +- apps/server/src/modules/board/uc/index.ts | 1 - .../media-available-line.uc.spec.ts | 151 ++- .../uc/media-board/media-available-line.uc.ts | 46 +- .../uc/media-board/media-board.uc.spec.ts | 85 +- .../board/uc/media-board/media-board.uc.ts | 67 +- .../uc/media-board/media-element.uc.spec.ts | 158 +-- .../board/uc/media-board/media-element.uc.ts | 96 +- .../uc/media-board/media-line.uc.spec.ts | 138 +-- .../board/uc/media-board/media-line.uc.ts | 64 +- .../board/uc/submission-item.uc.spec.ts | 419 +++++--- .../modules/board/uc/submission-item.uc.ts | 81 +- .../api/collaborative-text-editor.uc.ts | 12 +- .../get-collaborative-text-editor.api.spec.ts | 53 +- .../repo/scope/deletion-request-scope.ts | 2 +- .../src/modules/group/repo/group.scope.ts | 3 +- .../board-column-board.response.ts | 2 +- .../src/modules/learnroom/learnroom.module.ts | 2 + .../common-cartridge-import.mapper.spec.ts | 12 +- .../mapper/common-cartridge-import.mapper.ts | 10 +- .../mapper/common-cartridge.mapper.spec.ts | 11 +- .../mapper/common-cartridge.mapper.ts | 2 +- .../mapper/room-board-response.mapper.spec.ts | 2 +- .../src/modules/learnroom/repo/index.ts | 2 + .../mikro-orm/column-board-node.repo.spec.ts | 60 ++ .../repo/mikro-orm/column-board-node.repo.ts | 28 + .../service/board-copy.service.spec.ts | 38 +- .../learnroom/service/board-copy.service.ts | 27 +- .../common-cartridge-export.service.spec.ts | 19 +- .../common-cartridge-export.service.ts | 36 +- .../common-cartridge-import.service.spec.ts | 58 +- .../common-cartridge-import.service.ts | 69 +- .../learnroom/service/rooms.service.spec.ts | 44 +- .../learnroom/service/rooms.service.ts | 20 +- .../learnroom/types/room-board.types.ts | 2 +- .../src/modules/learnroom/uc/rooms.uc.ts | 2 +- .../api-test/database-management.api.spec.ts | 5 + .../url-handler/board-url-handler.spec.ts | 3 +- .../service/url-handler/board-url-handler.ts | 3 +- .../service/share-token.service.spec.ts | 10 +- .../sharing/service/share-token.service.ts | 4 +- .../modules/sharing/uc/share-token.uc.spec.ts | 34 +- .../src/modules/sharing/uc/share-token.uc.ts | 14 +- .../common-tool-metadata.service.spec.ts | 16 +- .../service/common-tool-metadata.service.ts | 8 +- .../common/uc/tool-permission-helper.spec.ts | 53 +- .../tool/common/uc/tool-permission-helper.ts | 15 +- .../controller/api-test/tool.api.spec.ts | 30 +- .../api-test/tool-school.api.spec.ts | 31 +- .../tool-launch.controller.api.spec.ts | 21 +- .../auto-context-name.strategy.spec.ts | 34 +- .../auto-context-name.strategy.ts | 17 +- .../board/board-composite.do.spec.ts | 159 --- .../domainobject/board/board-composite.do.ts | 69 -- .../domain/domainobject/board/card.do.spec.ts | 69 -- .../domain/domainobject/board/card.do.ts | 58 - ...llaborative-text-editor-element.do.spec.ts | 66 -- .../collaborative-text-editor-element.do.ts | 20 - .../board/column-board.do.spec.ts | 58 - .../domainobject/board/column-board.do.ts | 58 - .../domainobject/board/column.do.spec.ts | 36 - .../domain/domainobject/board/column.do.ts | 36 - .../board/content-element.factory.spec.ts | 65 -- .../board/content-element.factory.ts | 134 --- .../board/drawing-element.do.spec.ts | 37 - .../domainobject/board/drawing-element.do.ts | 32 - .../board/external-tool-element.do.spec.ts | 49 - .../board/external-tool-element.do.ts | 32 - .../board/file-element.do.spec.ts | 113 -- .../domainobject/board/file-element.do.ts | 41 - .../shared/domain/domainobject/board/index.ts | 16 - .../board/link-element.do.spec.ts | 37 - .../domainobject/board/link-element.do.ts | 59 -- .../board/media-board/media-board.do.ts | 55 - .../media-external-tool-element.do.spec.ts | 13 - .../media-external-tool-element.do.ts | 31 - .../board/media-board/media-line.do.ts | 56 - .../board/rich-text-element.do.spec.ts | 37 - .../board/rich-text-element.do.ts | 42 - .../submission-container-element.do.spec.ts | 44 - .../board/submission-container-element.do.ts | 34 - .../board/submission-item.do.spec.ts | 58 - .../domainobject/board/submission-item.do.ts | 48 - .../board/submission-item.factory.spec.ts | 20 - .../board/submission-item.factory.ts | 16 - .../domainobject/board/types/any-board-do.ts | 8 - .../board/types/any-content-element-do.ts | 30 - .../board/types/any-media-board-do.ts | 4 - .../types/any-media-content-element-do.ts | 10 - .../board/types/board-composite-visitor.ts | 57 - .../board/types/board-do-authorizable.ts | 51 - .../board/types/column-board-info.ts | 8 - .../domain/domainobject/board/types/index.ts | 10 - .../src/shared/domain/domainobject/index.ts | 1 - .../shared/domain/entity/all-entities.spec.ts | 6 +- .../src/shared/domain/entity/all-entities.ts | 42 +- .../entity/boardnode/boardnode.entity.spec.ts | 65 -- .../entity/boardnode/boardnode.entity.ts | 72 -- .../entity/boardnode/card-node.entity.ts | 26 - ...orative-text-editor-element-node.entity.ts | 18 - .../column-board-node.entity.spec.ts | 23 - .../boardnode/column-board-node.entity.ts | 73 -- .../entity/boardnode/column-node.entity.ts | 18 - .../drawing-element-node.entity.spec.ts | 59 -- .../boardnode/drawing-element-node.entity.ts | 25 - .../external-tool-element-node.entity.spec.ts | 61 -- .../external-tool-element-node.entity.ts | 26 - .../file-element-node.entity.spec.ts | 51 - .../boardnode/file-element-node.entity.ts | 31 - .../shared/domain/entity/boardnode/index.ts | 15 - .../link-element-node.entity.spec.ts | 54 - .../boardnode/link-element-node.entity.ts | 36 - .../entity/boardnode/media-board/index.ts | 6 - .../media-board/media-board-node.entity.ts | 63 -- ...media-external-tool-element-node.entity.ts | 26 - .../media-board/media-line-node.entity.ts | 37 - .../rich-text-element-node.entity.spec.ts | 52 - .../rich-text-element-node.entity.ts | 31 - .../boardnode/root-board-node.entity.ts | 9 - ...sion-container-element-node.entity.spec.ts | 53 - ...ubmission-container-element-node.entity.ts | 26 - .../submission-item-node.entity.spec.ts | 54 - .../boardnode/submission-item-node.entity.ts | 35 - .../boardnode/types/board-do.builder.ts | 46 - .../domain/entity/boardnode/types/index.ts | 2 - .../entity/column-board-node.entity.spec.ts | 20 + .../domain/entity/column-board-node.entity.ts | 61 ++ apps/server/src/shared/domain/entity/index.ts | 2 +- .../legacy-board/boardElement.entity.spec.ts | 2 +- .../legacy-board/column-board-boardelement.ts | 2 +- .../legacy-board/legacy-board.entity.spec.ts | 3 +- .../legacy-board/legacy-board.entity.ts | 4 +- .../legacy-boardelement.entity.ts | 2 +- .../importuser.repo.integration.spec.ts | 10 +- .../repo/legacy-board/legacy-board.repo.ts | 2 +- .../shared/repo/types/object-id.type.spec.ts | 79 ++ .../src/shared/repo/types/object-id.type.ts | 21 + .../server/src/shared/repo/user/user.scope.ts | 3 +- .../src/shared/testing/create-collections.ts | 23 + .../testing/factory/axios-error.factory.ts | 2 +- .../shared/testing/factory/base.factory.ts | 6 +- .../testing/factory/boardelement.factory.ts | 2 +- .../factory/boardnode/card-node.factory.ts | 10 - .../collaborative-text-editor-node.factory.ts | 10 - .../factory/boardnode/column-node.factory.ts | 9 - .../boardnode/drawing-element-node.factory.ts | 13 - .../external-tool-element-node.factory.ts | 9 - .../boardnode/file-element-node.factory.ts | 13 - .../shared/testing/factory/boardnode/index.ts | 13 - .../boardnode/link-element-node.factory.ts | 14 - .../boardnode/media-board-node.factory.ts | 18 - ...edia-external-tool-element-node.factory.ts | 12 - .../boardnode/media-line-node.factory.ts | 14 - .../rich-text-element-node.factory.ts | 14 - ...bmission-container-element-node.factory.ts | 12 - .../boardnode/submission-item-node.factory.ts | 16 - .../column-board-node.factory.ts | 11 +- .../board/board-do-authorizable.factory.ts | 19 - ...borative-text-editor-element.do.factory.ts | 15 - .../factory/domainobject/board/index.ts | 18 - .../board/link-element.do.factory.ts | 15 - .../board/media-board.do.factory.ts | 34 - .../board/media-line.do.factory.ts | 25 - .../board/submission-item.do.factory.ts | 19 - .../testing/factory/domainobject/index.ts | 1 - .../external-tool-pseudonym.factory.ts | 2 +- .../src/shared/testing/factory/index.ts | 2 +- .../shared/testing/factory/ltitool.factory.ts | 2 +- .../testing/factory/pseudonym.factory.ts | 2 +- .../shared/testing/factory/team.factory.ts | 4 +- .../testing/factory/teamuser.factory.ts | 6 +- .../testing/factory/tldraw.ws.factory.ts | 2 +- .../factory/video-conference.do.factory.ts | 2 +- .../factory/video-conference.factory.ts | 2 +- apps/server/src/shared/testing/index.ts | 1 + .../shared/testing/test-socket-api-client.ts | 4 +- backup/setup/migrations.json | 9 + 450 files changed, 10482 insertions(+), 16312 deletions(-) create mode 100644 apps/server/src/migrations/mikro-orm/Migration20240528140356.ts rename apps/server/src/modules/authorization/domain/rules/{board-do.rule.spec.ts => board-node.rule.spec.ts} (64%) rename apps/server/src/modules/authorization/domain/rules/{board-do.rule.ts => board-node.rule.ts} (75%) create mode 100644 apps/server/src/modules/board/domain/board-node-authorizable.do.ts create mode 100644 apps/server/src/modules/board/domain/board-node.do.spec.ts create mode 100644 apps/server/src/modules/board/domain/board-node.do.ts create mode 100644 apps/server/src/modules/board/domain/board-node.factory.ts create mode 100644 apps/server/src/modules/board/domain/card.do.ts create mode 100644 apps/server/src/modules/board/domain/collaborative-text-editor.do.spec.ts create mode 100644 apps/server/src/modules/board/domain/collaborative-text-editor.do.ts create mode 100644 apps/server/src/modules/board/domain/colum-board.do.ts create mode 100644 apps/server/src/modules/board/domain/column.do.ts create mode 100644 apps/server/src/modules/board/domain/drawing-element.do.spec.ts create mode 100644 apps/server/src/modules/board/domain/drawing-element.do.ts create mode 100644 apps/server/src/modules/board/domain/external-tool-element.do.spec.ts create mode 100644 apps/server/src/modules/board/domain/external-tool-element.do.ts create mode 100644 apps/server/src/modules/board/domain/file-element.do.spec.ts create mode 100644 apps/server/src/modules/board/domain/file-element.do.ts delete mode 100644 apps/server/src/modules/board/domain/interface/index.ts delete mode 100644 apps/server/src/modules/board/domain/interface/layout-type.enum.ts create mode 100644 apps/server/src/modules/board/domain/link-element.do.spec.ts create mode 100644 apps/server/src/modules/board/domain/link-element.do.ts rename apps/server/src/{shared/domain/domainobject/board => modules/board/domain}/media-board/index.ts (53%) rename apps/server/src/{shared/domain/domainobject/board => modules/board/domain}/media-board/media-available-line-element.do.ts (98%) rename apps/server/src/{shared/domain/domainobject/board => modules/board/domain}/media-board/media-available-line.do.ts (89%) create mode 100644 apps/server/src/modules/board/domain/media-board/media-board-node-factory.spec.ts create mode 100644 apps/server/src/modules/board/domain/media-board/media-board-node-factory.ts create mode 100644 apps/server/src/modules/board/domain/media-board/media-board.do.ts create mode 100644 apps/server/src/modules/board/domain/media-board/media-external-tool-element.do.spec.ts create mode 100644 apps/server/src/modules/board/domain/media-board/media-external-tool-element.do.ts create mode 100644 apps/server/src/modules/board/domain/media-board/media-line.do.ts create mode 100644 apps/server/src/modules/board/domain/media-board/types/any-media-board-node.ts create mode 100644 apps/server/src/modules/board/domain/media-board/types/index.ts rename apps/server/src/modules/board/domain/{interface => media-board/types}/media-colors.enum.ts (100%) create mode 100644 apps/server/src/modules/board/domain/path-utils.ts create mode 100644 apps/server/src/modules/board/domain/rich-text-element.do.spec.ts create mode 100644 apps/server/src/modules/board/domain/rich-text-element.do.ts create mode 100644 apps/server/src/modules/board/domain/submission-container-element.do.ts create mode 100644 apps/server/src/modules/board/domain/submission-item.do.spec.ts create mode 100644 apps/server/src/modules/board/domain/submission-item.do.ts create mode 100644 apps/server/src/modules/board/domain/type-mapping.spec.ts create mode 100644 apps/server/src/modules/board/domain/type-mapping.ts create mode 100644 apps/server/src/modules/board/domain/types/any-board-node.ts create mode 100644 apps/server/src/modules/board/domain/types/any-content-element.ts rename apps/server/src/{shared/domain/domainobject/board => modules/board/domain}/types/board-external-reference.ts (63%) rename apps/server/src/{shared/domain/domainobject/board => modules/board/domain}/types/board-layout.enum.ts (80%) create mode 100644 apps/server/src/modules/board/domain/types/board-node-props.ts rename apps/server/src/{shared/domain/entity/boardnode/types/board-node-type.ts => modules/board/domain/types/board-node-type.enum.ts} (100%) rename apps/server/src/{shared/domain/domainobject/board/types/content-elements.enum.ts => modules/board/domain/types/content-element-type.enum.ts} (100%) create mode 100644 apps/server/src/modules/board/domain/types/index.ts delete mode 100644 apps/server/src/modules/board/repo/board-do.builder-impl.spec.ts delete mode 100644 apps/server/src/modules/board/repo/board-do.builder-impl.ts delete mode 100644 apps/server/src/modules/board/repo/board-do.repo.spec.ts delete mode 100644 apps/server/src/modules/board/repo/board-do.repo.ts create mode 100644 apps/server/src/modules/board/repo/entity/board-node.entity.spec.ts create mode 100644 apps/server/src/modules/board/repo/entity/board-node.entity.ts create mode 100644 apps/server/src/modules/board/repo/entity/embeddables/context.ts create mode 100644 apps/server/src/modules/board/repo/entity/embeddables/index.ts create mode 100644 apps/server/src/modules/board/repo/entity/index.ts delete mode 100644 apps/server/src/modules/board/repo/recursive-delete.visitor.spec.ts delete mode 100644 apps/server/src/modules/board/repo/recursive-delete.vistor.ts delete mode 100644 apps/server/src/modules/board/repo/recursive-save.visitor.spec.ts delete mode 100644 apps/server/src/modules/board/repo/recursive-save.visitor.ts create mode 100644 apps/server/src/modules/board/repo/tree-builder.ts create mode 100644 apps/server/src/modules/board/repo/types/board-node-entity-props.ts create mode 100644 apps/server/src/modules/board/repo/types/index.ts create mode 100644 apps/server/src/modules/board/service/board-common-tool.service.spec.ts create mode 100644 apps/server/src/modules/board/service/board-common-tool.service.ts delete mode 100644 apps/server/src/modules/board/service/board-do-authorizable.service.spec.ts delete mode 100644 apps/server/src/modules/board/service/board-do-authorizable.service.ts delete mode 100644 apps/server/src/modules/board/service/board-do-copy-service/board-do-copy.service.spec.ts delete mode 100644 apps/server/src/modules/board/service/board-do-copy-service/board-do-copy.service.ts delete mode 100644 apps/server/src/modules/board/service/board-do-copy-service/index.ts delete mode 100644 apps/server/src/modules/board/service/board-do-copy-service/recursive-copy.visitor.spec.ts delete mode 100644 apps/server/src/modules/board/service/board-do-copy-service/recursive-copy.visitor.ts delete mode 100644 apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy-service.factory.spec.ts delete mode 100644 apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy-service.factory.ts delete mode 100644 apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy.interface.ts delete mode 100644 apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy.service.ts delete mode 100644 apps/server/src/modules/board/service/board-do-copy-service/swap-internal-links.visitor.spec.ts delete mode 100644 apps/server/src/modules/board/service/board-do-copy-service/swap-internal-links.visitor.ts delete mode 100644 apps/server/src/modules/board/service/board-do.service.spec.ts delete mode 100644 apps/server/src/modules/board/service/board-do.service.ts create mode 100644 apps/server/src/modules/board/service/board-node-authorizable.service.spec.ts create mode 100644 apps/server/src/modules/board/service/board-node-authorizable.service.ts create mode 100644 apps/server/src/modules/board/service/board-node-permission.service.spec.ts create mode 100644 apps/server/src/modules/board/service/board-node-permission.service.ts create mode 100644 apps/server/src/modules/board/service/board-node.service.spec.ts create mode 100644 apps/server/src/modules/board/service/board-node.service.ts delete mode 100644 apps/server/src/modules/board/service/card.service.spec.ts delete mode 100644 apps/server/src/modules/board/service/card.service.ts delete mode 100644 apps/server/src/modules/board/service/column-board-copy.service.spec.ts delete mode 100644 apps/server/src/modules/board/service/column-board-copy.service.ts delete mode 100644 apps/server/src/modules/board/service/column.service.spec.ts delete mode 100644 apps/server/src/modules/board/service/column.service.ts delete mode 100644 apps/server/src/modules/board/service/content-element-update.visitor.spec.ts delete mode 100644 apps/server/src/modules/board/service/content-element-update.visitor.ts delete mode 100644 apps/server/src/modules/board/service/content-element.service.spec.ts delete mode 100644 apps/server/src/modules/board/service/content-element.service.ts create mode 100644 apps/server/src/modules/board/service/internal/board-context.service.spec.ts create mode 100644 apps/server/src/modules/board/service/internal/board-context.service.ts create mode 100644 apps/server/src/modules/board/service/internal/board-node-copy-context.spec.ts create mode 100644 apps/server/src/modules/board/service/internal/board-node-copy-context.ts create mode 100644 apps/server/src/modules/board/service/internal/board-node-copy-general.service.spec.ts create mode 100644 apps/server/src/modules/board/service/internal/board-node-copy-specific.service.spec.ts create mode 100644 apps/server/src/modules/board/service/internal/board-node-copy.service.ts create mode 100644 apps/server/src/modules/board/service/internal/board-node-delete-hooks.service.spec.ts create mode 100644 apps/server/src/modules/board/service/internal/board-node-delete-hooks.service.ts create mode 100644 apps/server/src/modules/board/service/internal/column-board-copy.service.spec.ts create mode 100644 apps/server/src/modules/board/service/internal/column-board-copy.service.ts create mode 100644 apps/server/src/modules/board/service/internal/column-board-link.service.spec.ts create mode 100644 apps/server/src/modules/board/service/internal/column-board-link.service.ts create mode 100644 apps/server/src/modules/board/service/internal/column-board-reference.service.ts create mode 100644 apps/server/src/modules/board/service/internal/column-board-title.service.ts create mode 100644 apps/server/src/modules/board/service/internal/content-element-update.service.spec.ts create mode 100644 apps/server/src/modules/board/service/internal/content-element-update.service.ts create mode 100644 apps/server/src/modules/board/service/internal/index.ts delete mode 100644 apps/server/src/modules/board/service/media-board/media-element.service.spec.ts delete mode 100644 apps/server/src/modules/board/service/media-board/media-element.service.ts delete mode 100644 apps/server/src/modules/board/service/media-board/media-line.service.spec.ts delete mode 100644 apps/server/src/modules/board/service/media-board/media-line.service.ts delete mode 100644 apps/server/src/modules/board/service/submission-item.service.spec.ts delete mode 100644 apps/server/src/modules/board/service/submission-item.service.ts create mode 100644 apps/server/src/modules/board/testing/board-node-authorizable.factory.ts rename apps/server/src/{shared/testing/factory/domainobject/board/card.do.factory.ts => modules/board/testing/card.factory.ts} (51%) create mode 100644 apps/server/src/modules/board/testing/collaborative-text-editor.factory.ts rename apps/server/src/{shared/testing/factory/domainobject/board/column-board.do.factory.ts => modules/board/testing/column-board.factory.ts} (57%) rename apps/server/src/{shared/testing/factory/domainobject/board/column.do.factory.ts => modules/board/testing/column.factory.ts} (61%) rename apps/server/src/{shared/testing/factory/domainobject/board/drawing-element.do.factory.ts => modules/board/testing/drawing-element.factory.ts} (56%) create mode 100644 apps/server/src/modules/board/testing/entity/board-node-entity.factory.ts create mode 100644 apps/server/src/modules/board/testing/entity/card-entity.factory.ts create mode 100644 apps/server/src/modules/board/testing/entity/collaborative-text-editor-entity.factory.ts create mode 100644 apps/server/src/modules/board/testing/entity/column-board-entity.factory.ts create mode 100644 apps/server/src/modules/board/testing/entity/column-entity.factory.ts create mode 100644 apps/server/src/modules/board/testing/entity/drawing-element-entity.factory.ts create mode 100644 apps/server/src/modules/board/testing/entity/external-tool-element-entity.factory.ts create mode 100644 apps/server/src/modules/board/testing/entity/file-element-entity.factory.ts create mode 100644 apps/server/src/modules/board/testing/entity/index.ts create mode 100644 apps/server/src/modules/board/testing/entity/link-element-entity.factory.ts create mode 100644 apps/server/src/modules/board/testing/entity/media-board-entity.factory.ts create mode 100644 apps/server/src/modules/board/testing/entity/media-external-tool-element-entity.factory.ts create mode 100644 apps/server/src/modules/board/testing/entity/media-line-entity.factory.ts create mode 100644 apps/server/src/modules/board/testing/entity/rich-text-element-entity.factory.ts create mode 100644 apps/server/src/modules/board/testing/entity/submission-container-element-entity.factory.ts create mode 100644 apps/server/src/modules/board/testing/entity/submission-item-entity.factory.ts rename apps/server/src/{shared/testing/factory/domainobject/board/external-tool-element.do.factory.ts => modules/board/testing/external-tool-element.factory.ts} (53%) rename apps/server/src/{shared/testing/factory/domainobject/board/file-element.do.factory.ts => modules/board/testing/file-element.factory.ts} (53%) create mode 100644 apps/server/src/modules/board/testing/index.ts create mode 100644 apps/server/src/modules/board/testing/link-element.factory.ts rename apps/server/src/{shared/testing/factory/domainobject/board/media-available-line-element.do.factory.ts => modules/board/testing/media-available-line-element.factory.ts} (82%) rename apps/server/src/{shared/testing/factory/domainobject/board/media-available-line.do.factory.ts => modules/board/testing/media-available-line.factory.ts} (69%) create mode 100644 apps/server/src/modules/board/testing/media-board.factory.ts rename apps/server/src/{shared/testing/factory/domainobject/board/media-external-tool-element.do.factory.ts => modules/board/testing/media-external-tool-element.factory.ts} (59%) create mode 100644 apps/server/src/modules/board/testing/media-line.factory.ts rename apps/server/src/{shared/testing/factory/domainobject/board/rich-text-element.do.factory.ts => modules/board/testing/rich-text-element.factory.ts} (66%) rename apps/server/src/{shared/testing/factory/domainobject/board/submission-container-element.do.factory.ts => modules/board/testing/submission-container-element.factory.ts} (58%) create mode 100644 apps/server/src/modules/board/testing/submission-item.factory.ts delete mode 100644 apps/server/src/modules/board/uc/base.uc.ts create mode 100644 apps/server/src/modules/learnroom/repo/index.ts create mode 100644 apps/server/src/modules/learnroom/repo/mikro-orm/column-board-node.repo.spec.ts create mode 100644 apps/server/src/modules/learnroom/repo/mikro-orm/column-board-node.repo.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/board-composite.do.spec.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/board-composite.do.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/card.do.spec.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/card.do.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/collaborative-text-editor-element.do.spec.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/collaborative-text-editor-element.do.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/column-board.do.spec.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/column-board.do.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/column.do.spec.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/column.do.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/content-element.factory.spec.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/content-element.factory.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/drawing-element.do.spec.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/drawing-element.do.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/external-tool-element.do.spec.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/external-tool-element.do.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/file-element.do.spec.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/file-element.do.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/index.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/link-element.do.spec.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/link-element.do.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/media-board/media-board.do.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/media-board/media-external-tool-element.do.spec.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/media-board/media-external-tool-element.do.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/media-board/media-line.do.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/rich-text-element.do.spec.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/rich-text-element.do.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/submission-container-element.do.spec.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/submission-container-element.do.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/submission-item.do.spec.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/submission-item.do.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/submission-item.factory.spec.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/submission-item.factory.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/types/any-board-do.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/types/any-content-element-do.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/types/any-media-board-do.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/types/any-media-content-element-do.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/types/board-composite-visitor.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/types/board-do-authorizable.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/types/column-board-info.ts delete mode 100644 apps/server/src/shared/domain/domainobject/board/types/index.ts delete mode 100644 apps/server/src/shared/domain/entity/boardnode/boardnode.entity.spec.ts delete mode 100644 apps/server/src/shared/domain/entity/boardnode/boardnode.entity.ts delete mode 100644 apps/server/src/shared/domain/entity/boardnode/card-node.entity.ts delete mode 100644 apps/server/src/shared/domain/entity/boardnode/collaborative-text-editor-element-node.entity.ts delete mode 100644 apps/server/src/shared/domain/entity/boardnode/column-board-node.entity.spec.ts delete mode 100644 apps/server/src/shared/domain/entity/boardnode/column-board-node.entity.ts delete mode 100644 apps/server/src/shared/domain/entity/boardnode/column-node.entity.ts delete mode 100644 apps/server/src/shared/domain/entity/boardnode/drawing-element-node.entity.spec.ts delete mode 100644 apps/server/src/shared/domain/entity/boardnode/drawing-element-node.entity.ts delete mode 100644 apps/server/src/shared/domain/entity/boardnode/external-tool-element-node.entity.spec.ts delete mode 100644 apps/server/src/shared/domain/entity/boardnode/external-tool-element-node.entity.ts delete mode 100644 apps/server/src/shared/domain/entity/boardnode/file-element-node.entity.spec.ts delete mode 100644 apps/server/src/shared/domain/entity/boardnode/file-element-node.entity.ts delete mode 100644 apps/server/src/shared/domain/entity/boardnode/index.ts delete mode 100644 apps/server/src/shared/domain/entity/boardnode/link-element-node.entity.spec.ts delete mode 100644 apps/server/src/shared/domain/entity/boardnode/link-element-node.entity.ts delete mode 100644 apps/server/src/shared/domain/entity/boardnode/media-board/index.ts delete mode 100644 apps/server/src/shared/domain/entity/boardnode/media-board/media-board-node.entity.ts delete mode 100644 apps/server/src/shared/domain/entity/boardnode/media-board/media-external-tool-element-node.entity.ts delete mode 100644 apps/server/src/shared/domain/entity/boardnode/media-board/media-line-node.entity.ts delete mode 100644 apps/server/src/shared/domain/entity/boardnode/rich-text-element-node.entity.spec.ts delete mode 100644 apps/server/src/shared/domain/entity/boardnode/rich-text-element-node.entity.ts delete mode 100644 apps/server/src/shared/domain/entity/boardnode/root-board-node.entity.ts delete mode 100644 apps/server/src/shared/domain/entity/boardnode/submission-container-element-node.entity.spec.ts delete mode 100644 apps/server/src/shared/domain/entity/boardnode/submission-container-element-node.entity.ts delete mode 100644 apps/server/src/shared/domain/entity/boardnode/submission-item-node.entity.spec.ts delete mode 100644 apps/server/src/shared/domain/entity/boardnode/submission-item-node.entity.ts delete mode 100644 apps/server/src/shared/domain/entity/boardnode/types/board-do.builder.ts delete mode 100644 apps/server/src/shared/domain/entity/boardnode/types/index.ts create mode 100644 apps/server/src/shared/domain/entity/column-board-node.entity.spec.ts create mode 100644 apps/server/src/shared/domain/entity/column-board-node.entity.ts create mode 100644 apps/server/src/shared/repo/types/object-id.type.spec.ts create mode 100644 apps/server/src/shared/repo/types/object-id.type.ts create mode 100644 apps/server/src/shared/testing/create-collections.ts delete mode 100644 apps/server/src/shared/testing/factory/boardnode/card-node.factory.ts delete mode 100644 apps/server/src/shared/testing/factory/boardnode/collaborative-text-editor-node.factory.ts delete mode 100644 apps/server/src/shared/testing/factory/boardnode/column-node.factory.ts delete mode 100644 apps/server/src/shared/testing/factory/boardnode/drawing-element-node.factory.ts delete mode 100644 apps/server/src/shared/testing/factory/boardnode/external-tool-element-node.factory.ts delete mode 100644 apps/server/src/shared/testing/factory/boardnode/file-element-node.factory.ts delete mode 100644 apps/server/src/shared/testing/factory/boardnode/index.ts delete mode 100644 apps/server/src/shared/testing/factory/boardnode/link-element-node.factory.ts delete mode 100644 apps/server/src/shared/testing/factory/boardnode/media-board-node.factory.ts delete mode 100644 apps/server/src/shared/testing/factory/boardnode/media-external-tool-element-node.factory.ts delete mode 100644 apps/server/src/shared/testing/factory/boardnode/media-line-node.factory.ts delete mode 100644 apps/server/src/shared/testing/factory/boardnode/rich-text-element-node.factory.ts delete mode 100644 apps/server/src/shared/testing/factory/boardnode/submission-container-element-node.factory.ts delete mode 100644 apps/server/src/shared/testing/factory/boardnode/submission-item-node.factory.ts rename apps/server/src/shared/testing/factory/{boardnode => }/column-board-node.factory.ts (69%) delete mode 100644 apps/server/src/shared/testing/factory/domainobject/board/board-do-authorizable.factory.ts delete mode 100644 apps/server/src/shared/testing/factory/domainobject/board/collaborative-text-editor-element.do.factory.ts delete mode 100644 apps/server/src/shared/testing/factory/domainobject/board/index.ts delete mode 100644 apps/server/src/shared/testing/factory/domainobject/board/link-element.do.factory.ts delete mode 100644 apps/server/src/shared/testing/factory/domainobject/board/media-board.do.factory.ts delete mode 100644 apps/server/src/shared/testing/factory/domainobject/board/media-line.do.factory.ts delete mode 100644 apps/server/src/shared/testing/factory/domainobject/board/submission-item.do.factory.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0c415f14e0a..2d759947fea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,7 +59,7 @@ jobs: - name: npm ci run: npm ci --prefer-offline --no-audit - name: nest:test:cov - test all with coverage - timeout-minutes: 11 + timeout-minutes: 15 run: export RUN_WITHOUT_JEST_COVERAGE='true' && export NODE_OPTIONS='--max_old_space_size=4096' && ./node_modules/.bin/jest --shard=${{ matrix.shard }}/${{ strategy.job-total }} --coverage --force-exit - name: save-coverage run: mv coverage/lcov.info coverage/${{matrix.shard}}.info diff --git a/apps/server/src/infra/database/management/database-management.service.spec.ts b/apps/server/src/infra/database/management/database-management.service.spec.ts index 5def01912c4..5ac485c9222 100644 --- a/apps/server/src/infra/database/management/database-management.service.spec.ts +++ b/apps/server/src/infra/database/management/database-management.service.spec.ts @@ -1,7 +1,8 @@ import { MongoMemoryDatabaseModule } from '@infra/database'; import { MikroORM } from '@mikro-orm/core'; -import { ObjectId } from '@mikro-orm/mongodb'; +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; +import { createCollections } from '@shared/testing'; import { DatabaseManagementService } from './database-management.service'; const randomChars = () => new ObjectId().toHexString(); @@ -17,6 +18,9 @@ describe('DatabaseManagementService', () => { service = module.get(DatabaseManagementService); orm = module.get(MikroORM); + + const em = module.get(EntityManager); + await createCollections(em); }); afterAll(async () => { diff --git a/apps/server/src/migrations/mikro-orm/Migration20240415124640.ts b/apps/server/src/migrations/mikro-orm/Migration20240415124640.ts index 8cb380e903f..6688de3d288 100644 --- a/apps/server/src/migrations/mikro-orm/Migration20240415124640.ts +++ b/apps/server/src/migrations/mikro-orm/Migration20240415124640.ts @@ -1,6 +1,6 @@ import { Migration } from '@mikro-orm/migrations-mongodb'; -import { BoardLayout } from '@shared/domain/domainobject'; -import { BoardNodeType } from '@shared/domain/entity'; +import { BoardLayout } from '@modules/board'; +import { BoardNodeType } from '@modules/board/domain'; export class Migration20240415124640 extends Migration { async up(): Promise { diff --git a/apps/server/src/migrations/mikro-orm/Migration20240517135008.ts b/apps/server/src/migrations/mikro-orm/Migration20240517135008.ts index ecf7ceeeba6..4e50940a337 100644 --- a/apps/server/src/migrations/mikro-orm/Migration20240517135008.ts +++ b/apps/server/src/migrations/mikro-orm/Migration20240517135008.ts @@ -1,5 +1,5 @@ import { Migration } from '@mikro-orm/migrations-mongodb'; -import { MediaBoardColors, MediaBoardLayoutType } from '@modules/board/domain'; +import { MediaBoardColors, BoardLayout } from '@modules/board/domain'; export class Migration20240517135008 extends Migration { async up(): Promise { @@ -7,7 +7,7 @@ export class Migration20240517135008 extends Migration { 'boardnodes', { type: 'media-board' }, { - layout: MediaBoardLayoutType.LIST, + layout: BoardLayout.LIST, mediaAvailableLineBackgroundColor: MediaBoardColors.TRANSPARENT, mediaAvailableLineCollapsed: false, } diff --git a/apps/server/src/migrations/mikro-orm/Migration20240528140356.ts b/apps/server/src/migrations/mikro-orm/Migration20240528140356.ts new file mode 100644 index 00000000000..6f4195a96ad --- /dev/null +++ b/apps/server/src/migrations/mikro-orm/Migration20240528140356.ts @@ -0,0 +1,31 @@ +import { Migration } from '@mikro-orm/migrations-mongodb'; + +export class Migration20240528140356 extends Migration { + async up(): Promise { + const mediaBoards = await this.driver.nativeUpdate( + 'boardnodes', + { type: 'media-board' }, + { + $rename: { mediaAvailableLineBackgroundColor: 'backgroundColor', mediaAvailableLineCollapsed: 'collapsed' }, + } + ); + + if (mediaBoards.affectedRows > 0) { + console.info(`Additional attributes added to ${mediaBoards.affectedRows} Media boards`); + } + } + + async down(): Promise { + const mediaBoards = await this.driver.nativeUpdate( + 'boardnodes', + { type: 'media-board' }, + { + $rename: { backgroundColor: 'mediaAvailableLineBackgroundColor', collapsed: 'mediaAvailableLineCollapsed' }, + } + ); + + if (mediaBoards.affectedRows > 0) { + console.info(`Additional attributes removed from ${mediaBoards.affectedRows} Media boards`); + } + } +} diff --git a/apps/server/src/modules/authorization/authorization.module.ts b/apps/server/src/modules/authorization/authorization.module.ts index dfd6038cd8b..ac9f3261b9c 100644 --- a/apps/server/src/modules/authorization/authorization.module.ts +++ b/apps/server/src/modules/authorization/authorization.module.ts @@ -4,7 +4,7 @@ import { UserRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; import { AuthorizationHelper, AuthorizationService, RuleManager } from './domain'; import { - BoardDoRule, + BoardNodeRule, ContextExternalToolRule, CourseGroupRule, CourseRule, @@ -35,7 +35,7 @@ import { FeathersAuthorizationService, FeathersAuthProvider } from './feathers'; RuleManager, AuthorizationHelper, // rules - BoardDoRule, + BoardNodeRule, ContextExternalToolRule, CourseGroupRule, CourseRule, diff --git a/apps/server/src/modules/authorization/domain/rules/board-do.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/board-node.rule.spec.ts similarity index 64% rename from apps/server/src/modules/authorization/domain/rules/board-do.rule.spec.ts rename to apps/server/src/modules/authorization/domain/rules/board-node.rule.spec.ts index a40d1c349c6..c18ac7c5edc 100644 --- a/apps/server/src/modules/authorization/domain/rules/board-do.rule.spec.ts +++ b/apps/server/src/modules/authorization/domain/rules/board-node.rule.spec.ts @@ -1,32 +1,30 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardDoAuthorizable, BoardRoles } from '@shared/domain/domainobject'; import { Permission } from '@shared/domain/interface'; +import { roleFactory, setupEntities, userFactory } from '@shared/testing'; +import { BoardNodeAuthorizable, BoardRoles } from '@modules/board'; import { columnBoardFactory, drawingElementFactory, fileElementFactory, - roleFactory, - setupEntities, submissionItemFactory, - userFactory, -} from '@shared/testing'; +} from '@src/modules/board/testing'; import { AuthorizationHelper } from '../service/authorization.helper'; import { Action } from '../type'; -import { BoardDoRule } from './board-do.rule'; +import { BoardNodeRule } from './board-node.rule'; -describe(BoardDoRule.name, () => { - let service: BoardDoRule; +describe(BoardNodeRule.name, () => { + let service: BoardNodeRule; let authorizationHelper: AuthorizationHelper; beforeAll(async () => { await setupEntities(); const module: TestingModule = await Test.createTestingModule({ - providers: [BoardDoRule, AuthorizationHelper], + providers: [BoardNodeRule, AuthorizationHelper], }).compile(); - service = await module.get(BoardDoRule); + service = await module.get(BoardNodeRule); authorizationHelper = await module.get(AuthorizationHelper); }); @@ -34,21 +32,21 @@ describe(BoardDoRule.name, () => { describe('when entity is applicable', () => { const setup = () => { const user = userFactory.build(); - const anyBoardDo = fileElementFactory.build(); + const anyBoardNode = fileElementFactory.build(); const columnBoard = columnBoardFactory.build(); - const boardDoAuthorizable = new BoardDoAuthorizable({ + const boardNodeAuthorizable = new BoardNodeAuthorizable({ users: [], id: new ObjectId().toHexString(), - boardDo: anyBoardDo, - rootDo: columnBoard, + boardNode: anyBoardNode, + rootNode: columnBoard, }); - return { user, boardDoAuthorizable }; + return { user, boardNodeAuthorizable }; }; it('should return true', () => { - const { user, boardDoAuthorizable } = setup(); + const { user, boardNodeAuthorizable } = setup(); - const result = service.isApplicable(user, boardDoAuthorizable); + const result = service.isApplicable(user, boardNodeAuthorizable); expect(result).toStrictEqual(true); }); @@ -77,31 +75,34 @@ describe(BoardDoRule.name, () => { const permissionB = 'b' as Permission; const role = roleFactory.build({ permissions: [permissionA, permissionB] }); const user = userFactory.buildWithId({ roles: [role] }); - const anyBoardDo = fileElementFactory.build(); + const anyBoardNode = fileElementFactory.build(); const columnBoard = columnBoardFactory.build(); - const boardDoAuthorizable = new BoardDoAuthorizable({ + const boardNodeAuthorizable = new BoardNodeAuthorizable({ users: [{ userId: user.id, roles: [BoardRoles.EDITOR] }], id: new ObjectId().toHexString(), - boardDo: anyBoardDo, - rootDo: columnBoard, + boardNode: anyBoardNode, + rootNode: columnBoard, }); - return { user, boardDoAuthorizable }; + return { user, boardNodeAuthorizable }; }; it('should call hasAllPermissions on AuthorizationHelper', () => { - const { user, boardDoAuthorizable } = setup(); + const { user, boardNodeAuthorizable } = setup(); const spy = jest.spyOn(authorizationHelper, 'hasAllPermissions'); - service.hasPermission(user, boardDoAuthorizable, { action: Action.read, requiredPermissions: [] }); + service.hasPermission(user, boardNodeAuthorizable, { action: Action.read, requiredPermissions: [] }); expect(spy).toBeCalledWith(user, []); }); it('should return "true"', () => { - const { user, boardDoAuthorizable } = setup(); + const { user, boardNodeAuthorizable } = setup(); - const res = service.hasPermission(user, boardDoAuthorizable, { action: Action.read, requiredPermissions: [] }); + const res = service.hasPermission(user, boardNodeAuthorizable, { + action: Action.read, + requiredPermissions: [], + }); expect(res).toBe(true); }); @@ -111,22 +112,22 @@ describe(BoardDoRule.name, () => { const setup = () => { const permissionA = 'a' as Permission; const user = userFactory.buildWithId(); - const anyBoardDo = fileElementFactory.build(); + const anyBoardNode = fileElementFactory.build(); const columnBoard = columnBoardFactory.build(); - const boardDoAuthorizable = new BoardDoAuthorizable({ + const boardNodeAuthorizable = new BoardNodeAuthorizable({ users: [{ userId: user.id, roles: [BoardRoles.READER] }], id: new ObjectId().toHexString(), - boardDo: anyBoardDo, - rootDo: columnBoard, + boardNode: anyBoardNode, + rootNode: columnBoard, }); - return { user, permissionA, boardDoAuthorizable }; + return { user, permissionA, boardNodeAuthorizable }; }; it('should return "false"', () => { - const { user, permissionA, boardDoAuthorizable } = setup(); + const { user, permissionA, boardNodeAuthorizable } = setup(); - const res = service.hasPermission(user, boardDoAuthorizable, { + const res = service.hasPermission(user, boardNodeAuthorizable, { action: Action.write, requiredPermissions: [permissionA], }); @@ -140,22 +141,22 @@ describe(BoardDoRule.name, () => { const role = roleFactory.build(); const user = userFactory.buildWithId({ roles: [role] }); const userWithoutPermision = userFactory.buildWithId({ roles: [role] }); - const anyBoardDo = fileElementFactory.build(); + const anyBoardNode = fileElementFactory.build(); const columnBoard = columnBoardFactory.build(); - const boardDoAuthorizable = new BoardDoAuthorizable({ + const boardNodeAuthorizable = new BoardNodeAuthorizable({ users: [{ userId: user.id, roles: [BoardRoles.EDITOR] }], id: new ObjectId().toHexString(), - boardDo: anyBoardDo, - rootDo: columnBoard, + boardNode: anyBoardNode, + rootNode: columnBoard, }); - return { userWithoutPermision, boardDoAuthorizable }; + return { userWithoutPermision, boardNodeAuthorizable }; }; it('should return "false"', () => { - const { userWithoutPermision, boardDoAuthorizable } = setup(); + const { userWithoutPermision, boardNodeAuthorizable } = setup(); - const res = service.hasPermission(userWithoutPermision, boardDoAuthorizable, { + const res = service.hasPermission(userWithoutPermision, boardNodeAuthorizable, { action: Action.read, requiredPermissions: [], }); @@ -167,22 +168,22 @@ describe(BoardDoRule.name, () => { describe('when user does not have the desired role', () => { const setup = () => { const user = userFactory.buildWithId(); - const anyBoardDo = fileElementFactory.build(); + const anyBoardNode = fileElementFactory.build(); const columnBoard = columnBoardFactory.build(); - const boardDoAuthorizable = new BoardDoAuthorizable({ + const boardNodeAuthorizable = new BoardNodeAuthorizable({ users: [{ userId: user.id, roles: [] }], id: new ObjectId().toHexString(), - boardDo: anyBoardDo, - rootDo: columnBoard, + boardNode: anyBoardNode, + rootNode: columnBoard, }); - return { user, boardDoAuthorizable }; + return { user, boardNodeAuthorizable }; }; it('should return "false"', () => { - const { user, boardDoAuthorizable } = setup(); + const { user, boardNodeAuthorizable } = setup(); - const res = service.hasPermission(user, boardDoAuthorizable, { + const res = service.hasPermission(user, boardNodeAuthorizable, { action: Action.read, requiredPermissions: [], }); @@ -195,21 +196,21 @@ describe(BoardDoRule.name, () => { describe('when user is Editor', () => { const setup = () => { const user = userFactory.buildWithId(); - const anyBoardDo = fileElementFactory.build(); + const anyBoardNode = fileElementFactory.build(); const columnBoard = columnBoardFactory.build({ isVisible: false }); - const boardDoAuthorizable = new BoardDoAuthorizable({ + const boardNodeAuthorizable = new BoardNodeAuthorizable({ users: [{ userId: user.id, roles: [BoardRoles.EDITOR] }], id: new ObjectId().toHexString(), - boardDo: anyBoardDo, - rootDo: columnBoard, + boardNode: anyBoardNode, + rootNode: columnBoard, }); - return { user, boardDoAuthorizable }; + return { user, boardNodeAuthorizable }; }; it('it should return true if trying to "write" ', () => { - const { user, boardDoAuthorizable } = setup(); + const { user, boardNodeAuthorizable } = setup(); - const res = service.hasPermission(user, boardDoAuthorizable, { + const res = service.hasPermission(user, boardNodeAuthorizable, { action: Action.write, requiredPermissions: [], }); @@ -217,9 +218,9 @@ describe(BoardDoRule.name, () => { expect(res).toBe(true); }); it('it should return true if trying to "read" ', () => { - const { user, boardDoAuthorizable } = setup(); + const { user, boardNodeAuthorizable } = setup(); - const res = service.hasPermission(user, boardDoAuthorizable, { + const res = service.hasPermission(user, boardNodeAuthorizable, { action: Action.read, requiredPermissions: [], }); @@ -230,21 +231,21 @@ describe(BoardDoRule.name, () => { describe('when user is Reader', () => { const setup = () => { const user = userFactory.buildWithId(); - const anyBoardDo = fileElementFactory.build(); + const anyBoardNode = fileElementFactory.build(); const columnBoard = columnBoardFactory.build({ isVisible: false }); - const boardDoAuthorizable = new BoardDoAuthorizable({ + const boardNodeAuthorizable = new BoardNodeAuthorizable({ users: [{ userId: user.id, roles: [BoardRoles.READER] }], id: new ObjectId().toHexString(), - boardDo: anyBoardDo, - rootDo: columnBoard, + boardNode: anyBoardNode, + rootNode: columnBoard, }); - return { user, boardDoAuthorizable }; + return { user, boardNodeAuthorizable }; }; it('it should return false if trying to "write" ', () => { - const { user, boardDoAuthorizable } = setup(); + const { user, boardNodeAuthorizable } = setup(); - const res = service.hasPermission(user, boardDoAuthorizable, { + const res = service.hasPermission(user, boardNodeAuthorizable, { action: Action.write, requiredPermissions: [], }); @@ -252,9 +253,9 @@ describe(BoardDoRule.name, () => { expect(res).toBe(false); }); it('it should return false if trying to "read" ', () => { - const { user, boardDoAuthorizable } = setup(); + const { user, boardNodeAuthorizable } = setup(); - const res = service.hasPermission(user, boardDoAuthorizable, { + const res = service.hasPermission(user, boardNodeAuthorizable, { action: Action.write, requiredPermissions: [], }); @@ -270,19 +271,19 @@ describe(BoardDoRule.name, () => { const user = userFactory.buildWithId(); const submissionItem = submissionItemFactory.build(); const columnBoard = columnBoardFactory.build(); - const boardDoAuthorizable = new BoardDoAuthorizable({ + const boardNodeAuthorizable = new BoardNodeAuthorizable({ users: [{ userId: user.id, roles: [BoardRoles.EDITOR] }], id: new ObjectId().toHexString(), - boardDo: submissionItem, - rootDo: columnBoard, + boardNode: submissionItem, + rootNode: columnBoard, }); - return { user, boardDoAuthorizable }; + return { user, boardNodeAuthorizable }; }; it('it should return false if trying to "write" ', () => { - const { user, boardDoAuthorizable } = setup(); + const { user, boardNodeAuthorizable } = setup(); - const res = service.hasPermission(user, boardDoAuthorizable, { + const res = service.hasPermission(user, boardNodeAuthorizable, { action: Action.write, requiredPermissions: [], }); @@ -290,9 +291,9 @@ describe(BoardDoRule.name, () => { expect(res).toBe(false); }); it('it should return true if trying to "read"', () => { - const { user, boardDoAuthorizable } = setup(); + const { user, boardNodeAuthorizable } = setup(); - const res = service.hasPermission(user, boardDoAuthorizable, { + const res = service.hasPermission(user, boardNodeAuthorizable, { action: Action.read, requiredPermissions: [], }); @@ -305,19 +306,19 @@ describe(BoardDoRule.name, () => { const user = userFactory.buildWithId(); const submissionItem = submissionItemFactory.build({ userId: user.id }); const columnBoard = columnBoardFactory.build(); - const boardDoAuthorizable = new BoardDoAuthorizable({ + const boardNodeAuthorizable = new BoardNodeAuthorizable({ users: [{ userId: user.id, roles: [BoardRoles.READER] }], id: new ObjectId().toHexString(), - boardDo: submissionItem, - rootDo: columnBoard, + boardNode: submissionItem, + rootNode: columnBoard, }); - return { user, boardDoAuthorizable }; + return { user, boardNodeAuthorizable }; }; it('it should return "true" if trying to "write" ', () => { - const { user, boardDoAuthorizable } = setup(); + const { user, boardNodeAuthorizable } = setup(); - const res = service.hasPermission(user, boardDoAuthorizable, { + const res = service.hasPermission(user, boardNodeAuthorizable, { action: Action.write, requiredPermissions: [], }); @@ -325,9 +326,9 @@ describe(BoardDoRule.name, () => { expect(res).toBe(true); }); it('it should return "true" if trying to "read"', () => { - const { user, boardDoAuthorizable } = setup(); + const { user, boardNodeAuthorizable } = setup(); - const res = service.hasPermission(user, boardDoAuthorizable, { + const res = service.hasPermission(user, boardNodeAuthorizable, { action: Action.read, requiredPermissions: [], }); @@ -340,19 +341,19 @@ describe(BoardDoRule.name, () => { const user = userFactory.buildWithId(); const submissionItem = submissionItemFactory.build({ userId: new ObjectId().toHexString() }); const columnBoard = columnBoardFactory.build(); - const boardDoAuthorizable = new BoardDoAuthorizable({ + const boardNodeAuthorizable = new BoardNodeAuthorizable({ users: [{ userId: user.id, roles: [BoardRoles.READER] }], id: new ObjectId().toHexString(), - boardDo: submissionItem, - rootDo: columnBoard, + boardNode: submissionItem, + rootNode: columnBoard, }); - return { user, boardDoAuthorizable }; + return { user, boardNodeAuthorizable }; }; it('it should return "false" if trying to "write" ', () => { - const { user, boardDoAuthorizable } = setup(); + const { user, boardNodeAuthorizable } = setup(); - const res = service.hasPermission(user, boardDoAuthorizable, { + const res = service.hasPermission(user, boardNodeAuthorizable, { action: Action.write, requiredPermissions: [], }); @@ -360,9 +361,9 @@ describe(BoardDoRule.name, () => { expect(res).toBe(false); }); it('it should return "false" if trying to "read"', () => { - const { user, boardDoAuthorizable } = setup(); + const { user, boardNodeAuthorizable } = setup(); - const res = service.hasPermission(user, boardDoAuthorizable, { + const res = service.hasPermission(user, boardNodeAuthorizable, { action: Action.read, requiredPermissions: [], }); @@ -379,20 +380,20 @@ describe(BoardDoRule.name, () => { const submissionItem = submissionItemFactory.build(); const fileElement = fileElementFactory.build(); const columnBoard = columnBoardFactory.build(); - const boardDoAuthorizable = new BoardDoAuthorizable({ + const boardNodeAuthorizable = new BoardNodeAuthorizable({ users: [{ userId: user.id, roles: [BoardRoles.EDITOR] }], id: new ObjectId().toHexString(), - boardDo: fileElement, - parentDo: submissionItem, - rootDo: columnBoard, + boardNode: fileElement, + parentNode: submissionItem, + rootNode: columnBoard, }); - return { user, boardDoAuthorizable }; + return { user, boardNodeAuthorizable }; }; it('it should return false if trying to "write" ', () => { - const { user, boardDoAuthorizable } = setup(); + const { user, boardNodeAuthorizable } = setup(); - const res = service.hasPermission(user, boardDoAuthorizable, { + const res = service.hasPermission(user, boardNodeAuthorizable, { action: Action.write, requiredPermissions: [], }); @@ -400,9 +401,9 @@ describe(BoardDoRule.name, () => { expect(res).toBe(false); }); it('it should return true if trying to "read"', () => { - const { user, boardDoAuthorizable } = setup(); + const { user, boardNodeAuthorizable } = setup(); - const res = service.hasPermission(user, boardDoAuthorizable, { + const res = service.hasPermission(user, boardNodeAuthorizable, { action: Action.read, requiredPermissions: [], }); @@ -416,20 +417,20 @@ describe(BoardDoRule.name, () => { const submissionItem = submissionItemFactory.build({ userId: user.id }); const fileElement = fileElementFactory.build(); const columnBoard = columnBoardFactory.build(); - const boardDoAuthorizable = new BoardDoAuthorizable({ + const boardNodeAuthorizable = new BoardNodeAuthorizable({ users: [{ userId: user.id, roles: [BoardRoles.READER] }], id: new ObjectId().toHexString(), - boardDo: fileElement, - parentDo: submissionItem, - rootDo: columnBoard, + boardNode: fileElement, + parentNode: submissionItem, + rootNode: columnBoard, }); - return { user, boardDoAuthorizable }; + return { user, boardNodeAuthorizable }; }; it('it should return "true" if trying to "write" ', () => { - const { user, boardDoAuthorizable } = setup(); + const { user, boardNodeAuthorizable } = setup(); - const res = service.hasPermission(user, boardDoAuthorizable, { + const res = service.hasPermission(user, boardNodeAuthorizable, { action: Action.write, requiredPermissions: [], }); @@ -437,9 +438,9 @@ describe(BoardDoRule.name, () => { expect(res).toBe(true); }); it('it should return "true" if trying to "read"', () => { - const { user, boardDoAuthorizable } = setup(); + const { user, boardNodeAuthorizable } = setup(); - const res = service.hasPermission(user, boardDoAuthorizable, { + const res = service.hasPermission(user, boardNodeAuthorizable, { action: Action.read, requiredPermissions: [], }); @@ -453,20 +454,20 @@ describe(BoardDoRule.name, () => { const anyBoardDo = fileElementFactory.build(); const submissionItem = submissionItemFactory.build({ userId: new ObjectId().toHexString() }); const columnBoard = columnBoardFactory.build(); - const boardDoAuthorizable = new BoardDoAuthorizable({ + const boardNodeAuthorizable = new BoardNodeAuthorizable({ users: [{ userId: user.id, roles: [BoardRoles.READER] }], id: new ObjectId().toHexString(), - boardDo: anyBoardDo, - parentDo: submissionItem, - rootDo: columnBoard, + boardNode: anyBoardDo, + parentNode: submissionItem, + rootNode: columnBoard, }); - return { user, boardDoAuthorizable }; + return { user, boardNodeAuthorizable }; }; it('it should return "false" if trying to "write" ', () => { - const { user, boardDoAuthorizable } = setup(); + const { user, boardNodeAuthorizable } = setup(); - const res = service.hasPermission(user, boardDoAuthorizable, { + const res = service.hasPermission(user, boardNodeAuthorizable, { action: Action.write, requiredPermissions: [], }); @@ -474,9 +475,9 @@ describe(BoardDoRule.name, () => { expect(res).toBe(false); }); it('it should return "false" if trying to "read"', () => { - const { user, boardDoAuthorizable } = setup(); + const { user, boardNodeAuthorizable } = setup(); - const res = service.hasPermission(user, boardDoAuthorizable, { + const res = service.hasPermission(user, boardNodeAuthorizable, { action: Action.read, requiredPermissions: [], }); @@ -496,15 +497,15 @@ describe(BoardDoRule.name, () => { const { user, submissionItem } = setup(); const anyBoardDo = fileElementFactory.build(); const columnBoard = columnBoardFactory.build(); - const boardDoAuthorizable = new BoardDoAuthorizable({ + const boardNodeAuthorizable = new BoardNodeAuthorizable({ users: [{ userId: user.id, roles: [BoardRoles.EDITOR] }], id: new ObjectId().toHexString(), - boardDo: anyBoardDo, - parentDo: submissionItem, - rootDo: columnBoard, + boardNode: anyBoardDo, + parentNode: submissionItem, + rootNode: columnBoard, }); - const res = service.hasPermission(user, boardDoAuthorizable, { + const res = service.hasPermission(user, boardNodeAuthorizable, { action: Action.write, requiredPermissions: [], }); @@ -515,15 +516,15 @@ describe(BoardDoRule.name, () => { it('when boardDo is not allowed type, it should return false', () => { const { user, submissionItem, notAllowedChildElement } = setup(); const columnBoard = columnBoardFactory.build(); - const boardDoAuthorizable = new BoardDoAuthorizable({ + const boardNodeAuthorizable = new BoardNodeAuthorizable({ users: [{ userId: user.id, roles: [BoardRoles.EDITOR] }], id: new ObjectId().toHexString(), - parentDo: submissionItem, - boardDo: notAllowedChildElement, - rootDo: columnBoard, + parentNode: submissionItem, + boardNode: notAllowedChildElement, + rootNode: columnBoard, }); - const res = service.hasPermission(user, boardDoAuthorizable, { + const res = service.hasPermission(user, boardNodeAuthorizable, { action: Action.write, requiredPermissions: [], }); @@ -540,19 +541,19 @@ describe(BoardDoRule.name, () => { const user = userFactory.buildWithId(); const drawingElement = drawingElementFactory.build(); const columnBoard = columnBoardFactory.build(); - const boardDoAuthorizable = new BoardDoAuthorizable({ + const boardNodeAuthorizable = new BoardNodeAuthorizable({ users: [{ userId: user.id, roles: [BoardRoles.EDITOR] }], id: new ObjectId().toHexString(), - boardDo: drawingElement, - rootDo: columnBoard, + boardNode: drawingElement, + rootNode: columnBoard, }); - return { user, boardDoAuthorizable }; + return { user, boardNodeAuthorizable }; }; it('should return true if trying to "read"', () => { - const { user, boardDoAuthorizable } = setup(); + const { user, boardNodeAuthorizable } = setup(); - const res = service.hasPermission(user, boardDoAuthorizable, { + const res = service.hasPermission(user, boardNodeAuthorizable, { action: Action.read, requiredPermissions: [], }); @@ -560,9 +561,9 @@ describe(BoardDoRule.name, () => { expect(res).toBe(true); }); it('should return true if trying to "write" ', () => { - const { user, boardDoAuthorizable } = setup(); + const { user, boardNodeAuthorizable } = setup(); - const res = service.hasPermission(user, boardDoAuthorizable, { + const res = service.hasPermission(user, boardNodeAuthorizable, { action: Action.write, requiredPermissions: [], }); @@ -575,19 +576,19 @@ describe(BoardDoRule.name, () => { const user = userFactory.buildWithId(); const drawingElement = drawingElementFactory.build(); const columnBoard = columnBoardFactory.build(); - const boardDoAuthorizable = new BoardDoAuthorizable({ + const boardNodeAuthorizable = new BoardNodeAuthorizable({ users: [{ userId: user.id, roles: [BoardRoles.READER] }], id: new ObjectId().toHexString(), - boardDo: drawingElement, - rootDo: columnBoard, + boardNode: drawingElement, + rootNode: columnBoard, }); - return { user, boardDoAuthorizable }; + return { user, boardNodeAuthorizable }; }; it('should return true if trying to "read"', () => { - const { user, boardDoAuthorizable } = setup(); + const { user, boardNodeAuthorizable } = setup(); - const res = service.hasPermission(user, boardDoAuthorizable, { + const res = service.hasPermission(user, boardNodeAuthorizable, { action: Action.read, requiredPermissions: [], }); @@ -595,9 +596,9 @@ describe(BoardDoRule.name, () => { expect(res).toBe(true); }); it('should return false if trying to "write" ', () => { - const { user, boardDoAuthorizable } = setup(); + const { user, boardNodeAuthorizable } = setup(); - const res = service.hasPermission(user, boardDoAuthorizable, { + const res = service.hasPermission(user, boardNodeAuthorizable, { action: Action.write, requiredPermissions: [], }); @@ -612,19 +613,19 @@ describe(BoardDoRule.name, () => { const user = userFactory.asTeacher().buildWithId(); const drawingElement = drawingElementFactory.build(); const columnBoard = columnBoardFactory.build(); - const boardDoAuthorizable = new BoardDoAuthorizable({ + const boardNodeAuthorizable = new BoardNodeAuthorizable({ users: [{ userId: user.id, roles: [BoardRoles.EDITOR] }], id: new ObjectId().toHexString(), - boardDo: drawingElement, - rootDo: columnBoard, + boardNode: drawingElement, + rootNode: columnBoard, }); - return { user, boardDoAuthorizable }; + return { user, boardNodeAuthorizable }; }; it('should return true if trying to "read"', () => { - const { user, boardDoAuthorizable } = setup(); + const { user, boardNodeAuthorizable } = setup(); - const res = service.hasPermission(user, boardDoAuthorizable, { + const res = service.hasPermission(user, boardNodeAuthorizable, { action: Action.read, requiredPermissions: [Permission.FILESTORAGE_VIEW], }); @@ -632,9 +633,9 @@ describe(BoardDoRule.name, () => { expect(res).toBe(true); }); it('should return true if trying to "write" ', () => { - const { user, boardDoAuthorizable } = setup(); + const { user, boardNodeAuthorizable } = setup(); - const res = service.hasPermission(user, boardDoAuthorizable, { + const res = service.hasPermission(user, boardNodeAuthorizable, { action: Action.write, requiredPermissions: [Permission.FILESTORAGE_CREATE], }); @@ -647,19 +648,19 @@ describe(BoardDoRule.name, () => { const user = userFactory.asStudent().buildWithId(); const drawingElement = drawingElementFactory.build(); const columnBoard = columnBoardFactory.build(); - const boardDoAuthorizable = new BoardDoAuthorizable({ + const boardNodeAuthorizable = new BoardNodeAuthorizable({ users: [{ userId: user.id, roles: [BoardRoles.READER] }], id: new ObjectId().toHexString(), - boardDo: drawingElement, - rootDo: columnBoard, + boardNode: drawingElement, + rootNode: columnBoard, }); - return { user, boardDoAuthorizable }; + return { user, boardNodeAuthorizable }; }; it('should return true if trying to "read"', () => { - const { user, boardDoAuthorizable } = setup(); + const { user, boardNodeAuthorizable } = setup(); - const res = service.hasPermission(user, boardDoAuthorizable, { + const res = service.hasPermission(user, boardNodeAuthorizable, { action: Action.read, requiredPermissions: [Permission.FILESTORAGE_VIEW], }); @@ -667,9 +668,9 @@ describe(BoardDoRule.name, () => { expect(res).toBe(true); }); it('should ALSO return true if trying to "write" ', () => { - const { user, boardDoAuthorizable } = setup(); + const { user, boardNodeAuthorizable } = setup(); - const res = service.hasPermission(user, boardDoAuthorizable, { + const res = service.hasPermission(user, boardNodeAuthorizable, { action: Action.write, requiredPermissions: [Permission.FILESTORAGE_CREATE], }); diff --git a/apps/server/src/modules/authorization/domain/rules/board-do.rule.ts b/apps/server/src/modules/authorization/domain/rules/board-node.rule.ts similarity index 75% rename from apps/server/src/modules/authorization/domain/rules/board-do.rule.ts rename to apps/server/src/modules/authorization/domain/rules/board-node.rule.ts index 726fcd12bd7..6b530225360 100644 --- a/apps/server/src/modules/authorization/domain/rules/board-do.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/board-node.rule.ts @@ -1,29 +1,31 @@ import { Injectable } from '@nestjs/common'; +import { User } from '@shared/domain/entity/user.entity'; +import { Permission } from '@shared/domain/interface'; +import { EntityId } from '@shared/domain/types'; import { + BoardNodeAuthorizable, + BoardRoles, ColumnBoard, isDrawingElement, isSubmissionItem, isSubmissionItemContent, SubmissionItem, -} from '@shared/domain/domainobject'; -import { BoardDoAuthorizable, BoardRoles, UserWithBoardRoles } from '@shared/domain/domainobject/board/types'; -import { User } from '@shared/domain/entity/user.entity'; -import { Permission } from '@shared/domain/interface'; -import { EntityId } from '@shared/domain/types'; + UserWithBoardRoles, +} from '@modules/board'; import { AuthorizationHelper } from '../service/authorization.helper'; import { Action, AuthorizationContext, Rule } from '../type'; @Injectable() -export class BoardDoRule implements Rule { +export class BoardNodeRule implements Rule { constructor(private readonly authorizationHelper: AuthorizationHelper) {} public isApplicable(user: User, object: unknown): boolean { - const isMatched = object instanceof BoardDoAuthorizable; + const isMatched = object instanceof BoardNodeAuthorizable; return isMatched; } - public hasPermission(user: User, object: BoardDoAuthorizable, context: AuthorizationContext): boolean { + public hasPermission(user: User, object: BoardNodeAuthorizable, context: AuthorizationContext): boolean { const hasPermission = this.authorizationHelper.hasAllPermissions(user, context.requiredPermissions); if (!hasPermission) { return false; @@ -34,7 +36,11 @@ export class BoardDoRule implements Rule { return false; } - if (object.rootDo instanceof ColumnBoard && !object.rootDo.isVisible && !this.isBoardEditor(userWithBoardRoles)) { + if ( + object.rootNode instanceof ColumnBoard && + !object.rootNode.isVisible && + !this.isBoardEditor(userWithBoardRoles) + ) { return false; } @@ -66,18 +72,18 @@ export class BoardDoRule implements Rule { } private shouldProcessDrawingElementFile( - boardDoAuthorizable: BoardDoAuthorizable, + boardNodeAuthorizable: BoardNodeAuthorizable, context: AuthorizationContext ): boolean { const requiresFileStoragePermission = context.requiredPermissions.includes(Permission.FILESTORAGE_CREATE) || context.requiredPermissions.includes(Permission.FILESTORAGE_VIEW); - return isDrawingElement(boardDoAuthorizable.boardDo) && requiresFileStoragePermission; + return isDrawingElement(boardNodeAuthorizable.boardNode) && requiresFileStoragePermission; } - private shouldProcessDrawingElement(boardDoAuthorizable: BoardDoAuthorizable): boolean { - return isDrawingElement(boardDoAuthorizable.boardDo); + private shouldProcessDrawingElement(boardNodeAuthorizable: BoardNodeAuthorizable): boolean { + return isDrawingElement(boardNodeAuthorizable.boardNode); } private hasPermissionForDrawingElementFile(userWithBoardRoles: UserWithBoardRoles): boolean { @@ -97,35 +103,35 @@ export class BoardDoRule implements Rule { return this.isBoardReader(userWithBoardRoles); } - private shouldProcessSubmissionItem(boardDoAuthorizable: BoardDoAuthorizable): boolean { - return isSubmissionItem(boardDoAuthorizable.boardDo) || isSubmissionItem(boardDoAuthorizable.parentDo); + private shouldProcessSubmissionItem(boardNodeAuthorizable: BoardNodeAuthorizable): boolean { + return isSubmissionItem(boardNodeAuthorizable.boardNode) || isSubmissionItem(boardNodeAuthorizable.parentNode); } private hasPermissionForSubmissionItem( user: User, userWithBoardRoles: UserWithBoardRoles, - boardDoAuthorizable: BoardDoAuthorizable, + boardNodeAuthorizable: BoardNodeAuthorizable, context: AuthorizationContext ): boolean { // permission for elements under a submission item, are handled by the parent submission item - if (isSubmissionItem(boardDoAuthorizable.parentDo)) { - if (!isSubmissionItemContent(boardDoAuthorizable.boardDo)) { + if (isSubmissionItem(boardNodeAuthorizable.parentNode)) { + if (!isSubmissionItemContent(boardNodeAuthorizable.boardNode)) { return false; } - boardDoAuthorizable.boardDo = boardDoAuthorizable.parentDo; - boardDoAuthorizable.parentDo = undefined; + boardNodeAuthorizable.boardNode = boardNodeAuthorizable.parentNode; + boardNodeAuthorizable.parentNode = undefined; } - if (!isSubmissionItem(boardDoAuthorizable.boardDo)) { + if (!isSubmissionItem(boardNodeAuthorizable.boardNode)) { /* istanbul ignore next */ throw new Error('BoardDoAuthorizable.boardDo is not a submission item'); } if (context.action === Action.write) { - return this.hasSubmissionItemWritePermission(userWithBoardRoles, boardDoAuthorizable.boardDo); + return this.hasSubmissionItemWritePermission(userWithBoardRoles, boardNodeAuthorizable.boardNode); } - return this.hasSubmissionItemReadPermission(userWithBoardRoles, boardDoAuthorizable.boardDo); + return this.hasSubmissionItemReadPermission(userWithBoardRoles, boardNodeAuthorizable.boardNode); } private hasSubmissionItemWritePermission( diff --git a/apps/server/src/modules/authorization/domain/rules/index.ts b/apps/server/src/modules/authorization/domain/rules/index.ts index f18bdbd9e20..4c85a3247ad 100644 --- a/apps/server/src/modules/authorization/domain/rules/index.ts +++ b/apps/server/src/modules/authorization/domain/rules/index.ts @@ -2,7 +2,7 @@ * Rules are currently placed in authorization module to avoid dependency cycles. * In future they must be moved to the feature modules and register it in registration service. */ -export * from './board-do.rule'; +export * from './board-node.rule'; export * from './context-external-tool.rule'; export * from './course-group.rule'; export * from './course.rule'; diff --git a/apps/server/src/modules/authorization/domain/service/authorization-reference.service.spec.ts b/apps/server/src/modules/authorization/domain/service/authorization-reference.service.spec.ts index 61fd573fb88..71b977d1d1b 100644 --- a/apps/server/src/modules/authorization/domain/service/authorization-reference.service.spec.ts +++ b/apps/server/src/modules/authorization/domain/service/authorization-reference.service.spec.ts @@ -3,8 +3,8 @@ import { NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { courseFactory, setupEntities, userFactory } from '@shared/testing'; import { ObjectId } from '@mikro-orm/mongodb'; +import { AuthorizationService } from '@modules/authorization'; import { AuthorizableReferenceType } from '../type'; -import { AuthorizationService } from './authorization.service'; import { ReferenceLoader } from './reference.loader'; import { AuthorizationContextBuilder } from '../mapper'; import { ForbiddenLoggableException } from '../error'; @@ -21,14 +21,14 @@ describe('AuthorizationReferenceService', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ AuthorizationReferenceService, - { - provide: AuthorizationService, - useValue: createMock(), - }, { provide: ReferenceLoader, useValue: createMock(), }, + { + provide: AuthorizationService, + useValue: createMock(), + }, ], }).compile(); diff --git a/apps/server/src/modules/authorization/domain/service/reference.loader.spec.ts b/apps/server/src/modules/authorization/domain/service/reference.loader.spec.ts index 997afd6ce99..38f6e3eeaac 100644 --- a/apps/server/src/modules/authorization/domain/service/reference.loader.spec.ts +++ b/apps/server/src/modules/authorization/domain/service/reference.loader.spec.ts @@ -1,6 +1,5 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; -import { BoardDoAuthorizableService } from '@modules/board'; import { InstanceService } from '@modules/instance'; import { LessonService } from '@modules/lesson'; import { ContextExternalToolAuthorizableService, ExternalToolAuthorizableService } from '@modules/tool'; @@ -18,6 +17,7 @@ import { UserRepo, } from '@shared/repo'; import { setupEntities, userFactory } from '@shared/testing'; +import { BoardNodeAuthorizableService } from '@src/modules/board'; import { AuthorizableReferenceType } from '../type'; import { ReferenceLoader } from './reference.loader'; @@ -32,7 +32,7 @@ describe('reference.loader', () => { let teamsRepo: DeepMocked; let submissionRepo: DeepMocked; let schoolExternalToolRepo: DeepMocked; - let boardNodeAuthorizableService: DeepMocked; + let boardNodeAuthorizableService: DeepMocked; let contextExternalToolAuthorizableService: DeepMocked; let externalToolAuthorizableService: DeepMocked; let instanceService: DeepMocked; @@ -81,8 +81,8 @@ describe('reference.loader', () => { useValue: createMock(), }, { - provide: BoardDoAuthorizableService, - useValue: createMock(), + provide: BoardNodeAuthorizableService, + useValue: createMock(), }, { provide: ContextExternalToolAuthorizableService, @@ -109,7 +109,7 @@ describe('reference.loader', () => { teamsRepo = await module.get(TeamsRepo); submissionRepo = await module.get(SubmissionRepo); schoolExternalToolRepo = await module.get(SchoolExternalToolRepo); - boardNodeAuthorizableService = await module.get(BoardDoAuthorizableService); + boardNodeAuthorizableService = await module.get(BoardNodeAuthorizableService); contextExternalToolAuthorizableService = await module.get(ContextExternalToolAuthorizableService); externalToolAuthorizableService = await module.get(ExternalToolAuthorizableService); instanceService = await module.get(InstanceService); diff --git a/apps/server/src/modules/authorization/domain/service/reference.loader.ts b/apps/server/src/modules/authorization/domain/service/reference.loader.ts index 1ed06a1c7cb..bd45203e954 100644 --- a/apps/server/src/modules/authorization/domain/service/reference.loader.ts +++ b/apps/server/src/modules/authorization/domain/service/reference.loader.ts @@ -1,8 +1,10 @@ -import { BoardDoAuthorizableService } from '@modules/board'; - -import { LessonService } from '@modules/lesson'; -import { ContextExternalToolAuthorizableService } from '@modules/tool'; +// TODO fix modules circular dependency +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { BoardNodeAuthorizableService } from '@modules/board/service'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { ContextExternalToolAuthorizableService } from '@modules/tool/context-external-tool/service'; import { ExternalToolAuthorizableService } from '@modules/tool/external-tool/service'; +import { LessonService } from '@modules/lesson'; import { Injectable, NotImplementedException } from '@nestjs/common'; import { AuthorizableObject } from '@shared/domain/domain-object'; import { BaseDO } from '@shared/domain/domainobject'; @@ -21,7 +23,7 @@ import { InstanceService } from '../../../instance'; import { AuthorizableReferenceType } from '../type'; type RepoType = - | BoardDoAuthorizableService + | BoardNodeAuthorizableService | ContextExternalToolAuthorizableService | CourseGroupRepo | CourseRepo @@ -54,7 +56,7 @@ export class ReferenceLoader { private readonly teamsRepo: TeamsRepo, private readonly submissionRepo: SubmissionRepo, private readonly schoolExternalToolRepo: SchoolExternalToolRepo, - private readonly boardNodeAuthorizableService: BoardDoAuthorizableService, + private readonly boardNodeAuthorizableService: BoardNodeAuthorizableService, private readonly contextExternalToolAuthorizableService: ContextExternalToolAuthorizableService, private readonly externalToolAuthorizableService: ExternalToolAuthorizableService, private readonly instanceService: InstanceService diff --git a/apps/server/src/modules/authorization/domain/service/rule-manager.spec.ts b/apps/server/src/modules/authorization/domain/service/rule-manager.spec.ts index 67e6e428b9a..1856634692a 100644 --- a/apps/server/src/modules/authorization/domain/service/rule-manager.spec.ts +++ b/apps/server/src/modules/authorization/domain/service/rule-manager.spec.ts @@ -2,9 +2,11 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { InternalServerErrorException, NotImplementedException } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { courseFactory, setupEntities, userFactory } from '@shared/testing'; +// IMPORTANT: RuleManager has to be imported before the rules to prevent import cycles! +import { RuleManager } from '.'; import { AuthorizationContextBuilder } from '../mapper'; import { - BoardDoRule, + BoardNodeRule, ContextExternalToolRule, CourseGroupRule, CourseRule, @@ -23,7 +25,6 @@ import { UserRule, } from '../rules'; import { ExternalToolRule } from '../rules/external-tool.rule'; -import { RuleManager } from './rule-manager'; describe('RuleManager', () => { let service: RuleManager; @@ -36,7 +37,7 @@ describe('RuleManager', () => { let teamRule: DeepMocked; let submissionRule: DeepMocked; let schoolExternalToolRule: DeepMocked; - let boardDoRule: DeepMocked; + let boardNodeRule: DeepMocked; let contextExternalToolRule: DeepMocked; let userLoginMigrationRule: DeepMocked; let schoolRule: DeepMocked; @@ -62,7 +63,7 @@ describe('RuleManager', () => { { provide: TeamRule, useValue: createMock() }, { provide: SubmissionRule, useValue: createMock() }, { provide: SchoolExternalToolRule, useValue: createMock() }, - { provide: BoardDoRule, useValue: createMock() }, + { provide: BoardNodeRule, useValue: createMock() }, { provide: ContextExternalToolRule, useValue: createMock() }, { provide: UserLoginMigrationRule, useValue: createMock() }, { provide: SchoolRule, useValue: createMock() }, @@ -83,7 +84,7 @@ describe('RuleManager', () => { teamRule = await module.get(TeamRule); submissionRule = await module.get(SubmissionRule); schoolExternalToolRule = await module.get(SchoolExternalToolRule); - boardDoRule = await module.get(BoardDoRule); + boardNodeRule = await module.get(BoardNodeRule); contextExternalToolRule = await module.get(ContextExternalToolRule); userLoginMigrationRule = await module.get(UserLoginMigrationRule); schoolRule = await module.get(SchoolRule); @@ -119,7 +120,7 @@ describe('RuleManager', () => { teamRule.isApplicable.mockReturnValueOnce(false); submissionRule.isApplicable.mockReturnValueOnce(false); schoolExternalToolRule.isApplicable.mockReturnValueOnce(false); - boardDoRule.isApplicable.mockReturnValueOnce(false); + boardNodeRule.isApplicable.mockReturnValueOnce(false); contextExternalToolRule.isApplicable.mockReturnValueOnce(false); userLoginMigrationRule.isApplicable.mockReturnValueOnce(false); schoolRule.isApplicable.mockReturnValueOnce(false); @@ -146,7 +147,7 @@ describe('RuleManager', () => { expect(teamRule.isApplicable).toBeCalled(); expect(submissionRule.isApplicable).toBeCalled(); expect(schoolExternalToolRule.isApplicable).toBeCalled(); - expect(boardDoRule.isApplicable).toBeCalled(); + expect(boardNodeRule.isApplicable).toBeCalled(); expect(contextExternalToolRule.isApplicable).toBeCalled(); expect(userLoginMigrationRule.isApplicable).toBeCalled(); expect(schoolRule.isApplicable).toBeCalled(); @@ -181,7 +182,7 @@ describe('RuleManager', () => { teamRule.isApplicable.mockReturnValueOnce(false); submissionRule.isApplicable.mockReturnValueOnce(false); schoolExternalToolRule.isApplicable.mockReturnValueOnce(false); - boardDoRule.isApplicable.mockReturnValueOnce(false); + boardNodeRule.isApplicable.mockReturnValueOnce(false); contextExternalToolRule.isApplicable.mockReturnValueOnce(false); userLoginMigrationRule.isApplicable.mockReturnValueOnce(false); schoolRule.isApplicable.mockReturnValueOnce(false); @@ -216,7 +217,7 @@ describe('RuleManager', () => { teamRule.isApplicable.mockReturnValueOnce(false); submissionRule.isApplicable.mockReturnValueOnce(false); schoolExternalToolRule.isApplicable.mockReturnValueOnce(false); - boardDoRule.isApplicable.mockReturnValueOnce(false); + boardNodeRule.isApplicable.mockReturnValueOnce(false); contextExternalToolRule.isApplicable.mockReturnValueOnce(false); userLoginMigrationRule.isApplicable.mockReturnValueOnce(false); schoolRule.isApplicable.mockReturnValueOnce(false); diff --git a/apps/server/src/modules/authorization/domain/service/rule-manager.ts b/apps/server/src/modules/authorization/domain/service/rule-manager.ts index 143c5edbdc0..921caca0721 100644 --- a/apps/server/src/modules/authorization/domain/service/rule-manager.ts +++ b/apps/server/src/modules/authorization/domain/service/rule-manager.ts @@ -3,7 +3,7 @@ import { AuthorizableObject } from '@shared/domain/domain-object'; // fix import import { BaseDO } from '@shared/domain/domainobject'; import { User } from '@shared/domain/entity'; import { - BoardDoRule, + BoardNodeRule, ContextExternalToolRule, CourseGroupRule, CourseRule, @@ -29,7 +29,7 @@ export class RuleManager { private readonly rules: Rule[]; constructor( - private readonly boardDoRule: BoardDoRule, + private readonly boardNodeRule: BoardNodeRule, private readonly contextExternalToolRule: ContextExternalToolRule, private readonly courseGroupRule: CourseGroupRule, private readonly courseRule: CourseRule, @@ -49,7 +49,7 @@ export class RuleManager { private readonly instanceRule: InstanceRule ) { this.rules = [ - this.boardDoRule, + this.boardNodeRule, this.contextExternalToolRule, this.courseGroupRule, this.courseRule, diff --git a/apps/server/src/modules/board/board-api.module.ts b/apps/server/src/modules/board/board-api.module.ts index 52dd8ad82b1..3bc604d9ecf 100644 --- a/apps/server/src/modules/board/board-api.module.ts +++ b/apps/server/src/modules/board/board-api.module.ts @@ -2,7 +2,6 @@ import { AuthorizationModule } from '@modules/authorization'; import { forwardRef, Module } from '@nestjs/common'; import { CourseRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; -import { BoardModule } from './board.module'; import { BoardController, BoardSubmissionController, @@ -10,11 +9,13 @@ import { ColumnController, ElementController, } from './controller'; +import { BoardModule } from './board.module'; +import { BoardNodePermissionService } from './service'; import { BoardUc, CardUc, ColumnUc, ElementUc, SubmissionItemUc } from './uc'; @Module({ imports: [BoardModule, LoggerModule, forwardRef(() => AuthorizationModule)], controllers: [BoardController, ColumnController, CardController, ElementController, BoardSubmissionController], - providers: [BoardUc, ColumnUc, CardUc, ElementUc, SubmissionItemUc, CourseRepo], + providers: [BoardUc, BoardNodePermissionService, ColumnUc, CardUc, ElementUc, SubmissionItemUc, CourseRepo], }) export class BoardApiModule {} diff --git a/apps/server/src/modules/board/board-collaboration.module.ts b/apps/server/src/modules/board/board-collaboration.module.ts index a24757136d2..d53c3b46113 100644 --- a/apps/server/src/modules/board/board-collaboration.module.ts +++ b/apps/server/src/modules/board/board-collaboration.module.ts @@ -5,7 +5,6 @@ import { ConfigModule } from '@nestjs/config'; import { ALL_ENTITIES } from '@shared/domain/entity'; import { createConfigModuleOptions, DB_PASSWORD, DB_URL, DB_USERNAME } from '@src/config'; import { CoreModule } from '@src/core'; -import { ConsoleWriterModule } from '@src/infra/console'; import { RabbitMQWrapperModule } from '@src/infra/rabbitmq'; import { AuthorizationModule } from '../authorization'; import { config } from './board-collaboration.config'; @@ -18,7 +17,6 @@ import { AuthenticationModule } from '../authentication'; CoreModule, ConfigModule.forRoot(createConfigModuleOptions(config)), RabbitMQWrapperModule, - ConsoleWriterModule, MikroOrmModule.forRoot({ ...defaultMikroOrmOptions, type: 'mongo', diff --git a/apps/server/src/modules/board/board-collaboration.testing.module.ts b/apps/server/src/modules/board/board-collaboration.testing.module.ts index 8870a5ba3e0..60fa20fd169 100644 --- a/apps/server/src/modules/board/board-collaboration.testing.module.ts +++ b/apps/server/src/modules/board/board-collaboration.testing.module.ts @@ -4,7 +4,6 @@ import { ConfigModule } from '@nestjs/config'; import { ALL_ENTITIES } from '@shared/domain/entity'; import { createConfigModuleOptions } from '@src/config'; import { CoreModule } from '@src/core'; -import { ConsoleWriterModule } from '@src/infra/console'; import { MongoMemoryDatabaseModule } from '@src/infra/database'; import { RabbitMQWrapperModule } from '@src/infra/rabbitmq'; import { AuthenticationModule } from '../authentication'; @@ -23,7 +22,6 @@ const config = () => { CoreModule, ConfigModule.forRoot(createConfigModuleOptions(config)), RabbitMQWrapperModule, - ConsoleWriterModule, MongoMemoryDatabaseModule.forRoot({ ...defaultMikroOrmOptions, entities: ALL_ENTITIES, diff --git a/apps/server/src/modules/board/board-ws-api.module.ts b/apps/server/src/modules/board/board-ws-api.module.ts index 6955e56e43f..2883c658eda 100644 --- a/apps/server/src/modules/board/board-ws-api.module.ts +++ b/apps/server/src/modules/board/board-ws-api.module.ts @@ -5,10 +5,11 @@ import { AuthorizationModule } from '../authorization'; import { BoardModule } from './board.module'; import { BoardCollaborationGateway } from './gateway/board-collaboration.gateway'; import { BoardUc, CardUc, ColumnUc, ElementUc } from './uc'; +import { BoardNodePermissionService } from './service'; @Module({ imports: [BoardModule, forwardRef(() => AuthorizationModule), LoggerModule], - providers: [BoardCollaborationGateway, CardUc, ColumnUc, ElementUc, BoardUc, CourseRepo], + providers: [BoardCollaborationGateway, BoardNodePermissionService, CardUc, ColumnUc, ElementUc, BoardUc, CourseRepo], exports: [], }) export class BoardWsApiModule {} diff --git a/apps/server/src/modules/board/board.module.ts b/apps/server/src/modules/board/board.module.ts index 27b29f0ccbf..3e7d5cf3c49 100644 --- a/apps/server/src/modules/board/board.module.ts +++ b/apps/server/src/modules/board/board.module.ts @@ -1,4 +1,3 @@ -import { ConsoleWriterModule } from '@infra/console'; import { CollaborativeTextEditorModule } from '@modules/collaborative-text-editor'; import { CopyHelperModule } from '@modules/copy-helper'; import { FilesStorageClientModule } from '@modules/files-storage-client'; @@ -9,29 +8,31 @@ import { UserModule } from '@modules/user'; import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { CqrsModule } from '@nestjs/cqrs'; -import { ContentElementFactory } from '@shared/domain/domainobject'; import { CourseRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; -import { BoardDoRepo, BoardNodeRepo, RecursiveDeleteVisitor } from './repo'; +import { BoardNodeFactory } from './domain'; +import { BoardNodeRepo } from './repo'; import { - BoardDoAuthorizableService, - BoardDoService, - CardService, + BoardCommonToolService, + BoardNodeAuthorizableService, + BoardNodeService, ColumnBoardService, - ColumnService, - ContentElementService, MediaBoardService, - MediaElementService, - MediaLineService, - SubmissionItemService, UserDeletedEventHandlerService, } from './service'; -import { BoardDoCopyService, SchoolSpecificFileCopyServiceFactory } from './service/board-do-copy-service'; -import { ColumnBoardCopyService } from './service/column-board-copy.service'; +import { + BoardNodeCopyService, + ColumnBoardCopyService, + ColumnBoardLinkService, + ColumnBoardReferenceService, + ColumnBoardTitleService, + ContentElementUpdateService, + BoardNodeDeleteHooksService, + BoardContextService, +} from './service/internal'; @Module({ imports: [ - ConsoleWriterModule, CopyHelperModule, FilesStorageClientModule, LoggerModule, @@ -45,41 +46,31 @@ import { ColumnBoardCopyService } from './service/column-board-copy.service'; ], providers: [ // TODO: move BoardDoAuthorizableService, BoardDoRepo, BoardDoService, BoardNodeRepo in separate module and move mediaboard related services in mediaboard module - BoardDoAuthorizableService, - BoardDoRepo, - BoardDoService, + BoardContextService, + BoardNodeAuthorizableService, BoardNodeRepo, - CardService, + BoardNodeService, + BoardNodeFactory, + BoardNodeCopyService, + BoardCommonToolService, + BoardNodeDeleteHooksService, ColumnBoardService, - ColumnService, - ContentElementService, - ContentElementFactory, + ContentElementUpdateService, CourseRepo, // TODO: import learnroom module instead. This is currently not possible due to dependency cycle with authorisation service - RecursiveDeleteVisitor, - SubmissionItemService, - BoardDoCopyService, ColumnBoardCopyService, - SchoolSpecificFileCopyServiceFactory, - MediaBoardService, - MediaLineService, - MediaElementService, + ColumnBoardLinkService, + ColumnBoardReferenceService, + ColumnBoardTitleService, UserDeletedEventHandlerService, + // TODO replace by import of MediaBoardModule (fix dependency cycle) + MediaBoardService, ], exports: [ - BoardDoAuthorizableService, - CardService, + BoardNodeAuthorizableService, + BoardNodeFactory, + BoardNodeService, + BoardCommonToolService, ColumnBoardService, - ColumnService, - ContentElementService, - SubmissionItemService, - ColumnBoardCopyService, - /** - * @deprecated - exported only deprecated learnraum module - */ - BoardNodeRepo, - MediaBoardService, - MediaLineService, - MediaElementService, ], }) export class BoardModule {} diff --git a/apps/server/src/modules/board/controller/api-test/board-context.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-context.api.spec.ts index 5c14248de95..9cf69fa7e76 100644 --- a/apps/server/src/modules/board/controller/api-test/board-context.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-context.api.spec.ts @@ -2,15 +2,9 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { ServerTestModule } from '@modules/server'; import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; -import { - TestApiClient, - UserAndAccountTestFactory, - cleanupCollections, - columnBoardNodeFactory, - courseFactory, -} from '@shared/testing'; -import { BoardContextResponse } from '../dto/board/board-context.reponse'; +import { TestApiClient, UserAndAccountTestFactory, cleanupCollections, courseFactory } from '@shared/testing'; +import { columnBoardEntityFactory } from '../../testing'; +import { BoardExternalReferenceType } from '../../domain'; const baseRouteName = '/boards'; @@ -45,7 +39,7 @@ describe('board get context (api)', () => { const course = courseFactory.build({ teachers: [teacherUser] }); await em.persistAndFlush([teacherUser, teacherAccount, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); @@ -70,12 +64,7 @@ describe('board get context (api)', () => { const response = await loggedInClient.get(`${columnBoardNode.id}/context`); - const expectedBody: BoardContextResponse = { - id: columnBoardNode.context.id, - type: columnBoardNode.context.type, - }; - - expect(response.body).toEqual(expectedBody); + expect(response.body).toEqual({ id: columnBoardNode.context?.id, type: columnBoardNode.context?.type }); }); }); }); diff --git a/apps/server/src/modules/board/controller/api-test/board-copy.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-copy.api.spec.ts index 646aad24947..236b2d49bd1 100644 --- a/apps/server/src/modules/board/controller/api-test/board-copy.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-copy.api.spec.ts @@ -2,16 +2,11 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { ServerTestModule } from '@modules/server/server.module'; import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; -import { ColumnBoardNode } from '@shared/domain/entity'; -import { - TestApiClient, - UserAndAccountTestFactory, - cleanupCollections, - columnBoardNodeFactory, - courseFactory, -} from '@shared/testing'; -import { CopyApiResponse, CopyElementType, CopyStatusEnum } from '@src/modules/copy-helper'; +import { TestApiClient, UserAndAccountTestFactory, cleanupCollections, courseFactory } from '@shared/testing'; +import { CopyApiResponse, CopyElementType, CopyStatusEnum } from '@modules/copy-helper'; +import { BoardNodeEntity } from '../../repo'; +import { columnBoardEntityFactory } from '../../testing'; +import { BoardExternalReferenceType } from '../../domain'; const baseRouteName = '/boards'; @@ -46,7 +41,7 @@ describe(`board copy (api)`, () => { const course = courseFactory.build({ teachers: [teacherUser] }); await em.persistAndFlush([teacherUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); @@ -81,7 +76,7 @@ describe(`board copy (api)`, () => { expect(body).toEqual(expectedBody); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const result = await em.findOneOrFail(ColumnBoardNode, body.id!); + const result = await em.findOneOrFail(BoardNodeEntity, body.id!); expect(result).toBeDefined(); }); @@ -114,7 +109,7 @@ describe(`board copy (api)`, () => { const course = courseFactory.build({ students: [studentUser] }); await em.persistAndFlush([studentUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); diff --git a/apps/server/src/modules/board/controller/api-test/board-create.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-create.api.spec.ts index 433a00fef5c..63e184fa898 100644 --- a/apps/server/src/modules/board/controller/api-test/board-create.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-create.api.spec.ts @@ -2,9 +2,9 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { ServerTestModule } from '@modules/server/server.module'; import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardExternalReferenceType, BoardLayout } from '@shared/domain/domainobject'; -import { ColumnBoardNode } from '@shared/domain/entity'; import { cleanupCollections, courseFactory, TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; +import { BoardNodeEntity } from '../../repo'; +import { BoardExternalReferenceType, BoardLayout } from '../../domain'; import { CreateBoardBodyParams } from '../dto'; const baseRouteName = '/boards'; @@ -62,7 +62,7 @@ describe(`create board (api)`, () => { expect(response.status).toEqual(201); expect(boardId).toBeDefined(); - const dbResult = await em.findOneOrFail(ColumnBoardNode, boardId); + const dbResult = await em.findOneOrFail(BoardNodeEntity, boardId); expect(dbResult.title).toEqual(title); }); @@ -82,7 +82,7 @@ describe(`create board (api)`, () => { expect(response.status).toEqual(201); expect(boardId).toBeDefined(); - const dbResult = await em.findOneOrFail(ColumnBoardNode, boardId); + const dbResult = await em.findOneOrFail(BoardNodeEntity, boardId); expect(dbResult.layout).toEqual(BoardLayout.COLUMNS); }); }); @@ -102,7 +102,7 @@ describe(`create board (api)`, () => { expect(response.status).toEqual(201); expect(boardId).toBeDefined(); - const dbResult = await em.findOneOrFail(ColumnBoardNode, boardId); + const dbResult = await em.findOneOrFail(BoardNodeEntity, boardId); expect(dbResult.layout).toEqual(BoardLayout.LIST); }); }); diff --git a/apps/server/src/modules/board/controller/api-test/board-delete.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-delete.api.spec.ts index 835192909f1..cb71b2a3863 100644 --- a/apps/server/src/modules/board/controller/api-test/board-delete.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-delete.api.spec.ts @@ -1,71 +1,32 @@ import { EntityManager } from '@mikro-orm/mongodb'; -import { ICurrentUser } from '@modules/authentication'; -import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; import { ServerTestModule } from '@modules/server'; -import { ExecutionContext, INestApplication } from '@nestjs/common'; +import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { ApiValidationError } from '@shared/common'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; -import { ColumnBoardNode, ColumnNode } from '@shared/domain/entity'; -import { EntityId } from '@shared/domain/types'; -import { - cleanupCollections, - columnBoardNodeFactory, - columnNodeFactory, - courseFactory, - mapUserToCurrentUser, - userFactory, -} from '@shared/testing'; -import { Request } from 'express'; -import request from 'supertest'; -import { BoardResponse } from '../dto'; +import { cleanupCollections, courseFactory, TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; +import { BoardExternalReferenceType } from '../../domain'; +import { BoardNodeEntity } from '../../repo'; +import { columnBoardEntityFactory, columnEntityFactory } from '../../testing'; const baseRouteName = '/boards'; -class API { - app: INestApplication; - - constructor(app: INestApplication) { - this.app = app; - } - - async delete(boardId: EntityId) { - const response = await request(this.app.getHttpServer()) - .delete(`${baseRouteName}/${boardId}`) - .set('Accept', 'application/json'); - - return { - result: response.body as BoardResponse, - error: response.body as ApiValidationError, - status: response.status, - }; - } -} - describe(`board delete (api)`, () => { let app: INestApplication; let em: EntityManager; - let currentUser: ICurrentUser; - let api: API; + let testApiClient: TestApiClient; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [ServerTestModule], - }) - .overrideGuard(JwtAuthGuard) - .useValue({ - canActivate(context: ExecutionContext) { - const req: Request = context.switchToHttp().getRequest(); - req.user = currentUser; - return true; - }, - }) - .compile(); + }).compile(); app = module.createNestApplication(); await app.init(); em = module.get(EntityManager); - api = new API(app); + testApiClient = new TestApiClient(app, baseRouteName); + }); + + beforeEach(async () => { + await cleanupCollections(em); }); afterAll(async () => { @@ -73,63 +34,68 @@ describe(`board delete (api)`, () => { }); const setup = async () => { - await cleanupCollections(em); + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); - const user = userFactory.build(); - const course = courseFactory.build({ teachers: [user] }); - await em.persistAndFlush([user, course]); + const course = courseFactory.build({ teachers: [teacherUser] }); + await em.persistAndFlush([teacherUser, teacherAccount, course]); - const columnBoardNode = columnBoardNodeFactory.build({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - await em.persistAndFlush([columnBoardNode]); - - const columnNode = columnNodeFactory.build({ parent: columnBoardNode }); - await em.persistAndFlush([columnNode]); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); + await em.persistAndFlush([columnBoardNode, columnNode]); em.clear(); - return { user, columnBoardNode, columnNode }; + const loggedInClient = await testApiClient.login(teacherAccount); + + return { loggedInClient, columnBoardNode, columnNode }; }; describe('with valid user', () => { it('should return status 204', async () => { - const { user, columnBoardNode } = await setup(); - currentUser = mapUserToCurrentUser(user); + const { loggedInClient, columnBoardNode } = await setup(); - const response = await api.delete(columnBoardNode.id); + const response = await loggedInClient.delete(columnBoardNode.id); expect(response.status).toEqual(204); }); it('should actually delete the board', async () => { - const { user, columnBoardNode } = await setup(); - currentUser = mapUserToCurrentUser(user); + const { loggedInClient, columnBoardNode } = await setup(); - await api.delete(columnBoardNode.id); + await loggedInClient.delete(columnBoardNode.id); - await expect(em.findOneOrFail(ColumnBoardNode, columnBoardNode.id)).rejects.toThrow(); + await expect(em.findOneOrFail(BoardNodeEntity, columnBoardNode.id)).rejects.toThrow(); }); it('should actually delete columns of the board', async () => { - const { user, columnNode, columnBoardNode } = await setup(); - currentUser = mapUserToCurrentUser(user); + const { loggedInClient, columnNode, columnBoardNode } = await setup(); - await api.delete(columnBoardNode.id); + await loggedInClient.delete(columnBoardNode.id); - await expect(em.findOneOrFail(ColumnNode, columnNode.id)).rejects.toThrow(); + await expect(em.findOneOrFail(BoardNodeEntity, columnNode.id)).rejects.toThrow(); }); }); describe('with invalid user', () => { - it('should return status 403', async () => { - const { columnBoardNode } = await setup(); + const setupNoAccess = async () => { + const vars = await setup(); - const invalidUser = userFactory.build(); - await em.persistAndFlush([invalidUser]); - currentUser = mapUserToCurrentUser(invalidUser); + const { studentAccount: noAccessAccount, studentUser: noAccessUser } = UserAndAccountTestFactory.buildStudent(); + await em.persistAndFlush([noAccessAccount, noAccessUser]); + const loggedInClient = await testApiClient.login(noAccessAccount); + + return { + ...vars, + loggedInClient, + }; + }; + + it('should return status 403', async () => { + const { loggedInClient, columnBoardNode } = await setupNoAccess(); - const response = await api.delete(columnBoardNode.id); + const response = await loggedInClient.delete(columnBoardNode.id); expect(response.status).toEqual(403); }); diff --git a/apps/server/src/modules/board/controller/api-test/board-lookup.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-lookup.api.spec.ts index 460242684bf..25ae2ea4866 100644 --- a/apps/server/src/modules/board/controller/api-test/board-lookup.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-lookup.api.spec.ts @@ -2,16 +2,10 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { ServerTestModule } from '@modules/server/server.module'; import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardExternalReferenceType, BoardLayout } from '@shared/domain/domainobject'; -import { - cardNodeFactory, - cleanupCollections, - columnBoardNodeFactory, - columnNodeFactory, - courseFactory, - TestApiClient, - UserAndAccountTestFactory, -} from '@shared/testing'; +import { cleanupCollections, courseFactory, TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; +import { BoardExternalReferenceType, BoardLayout } from '../../domain'; + +import { cardEntityFactory, columnEntityFactory, columnBoardEntityFactory } from '../../testing'; import { BoardResponse } from '../dto'; const baseRouteName = '/boards'; @@ -47,14 +41,14 @@ describe(`board lookup (api)`, () => { const course = courseFactory.build({ teachers: [teacherUser] }); await em.persistAndFlush([teacherUser, course, teacherAccount]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); - const cardNode1 = cardNodeFactory.buildWithId({ parent: columnNode }); - const cardNode2 = cardNodeFactory.buildWithId({ parent: columnNode }); - const cardNode3 = cardNodeFactory.buildWithId({ parent: columnNode }); - const notOfThisBoardCardNode = cardNodeFactory.buildWithId(); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); + const cardNode1 = cardEntityFactory.withParent(columnNode).build(); + const cardNode2 = cardEntityFactory.withParent(columnNode).build(); + const cardNode3 = cardEntityFactory.withParent(columnNode).build(); + const notOfThisBoardCardNode = cardEntityFactory.build(); await em.persistAndFlush([columnBoardNode, columnNode, cardNode1, cardNode2, cardNode3, notOfThisBoardCardNode]); em.clear(); @@ -116,7 +110,7 @@ describe(`board lookup (api)`, () => { const course = courseFactory.build(); await em.persistAndFlush([teacherUser, course, teacherAccount]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); await em.persistAndFlush([columnBoardNode]); diff --git a/apps/server/src/modules/board/controller/api-test/board-update-title.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-update-title.api.spec.ts index f90654fd22d..93254af4884 100644 --- a/apps/server/src/modules/board/controller/api-test/board-update-title.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-update-title.api.spec.ts @@ -3,15 +3,10 @@ import { ServerTestModule } from '@modules/server/server.module'; import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; -import { ColumnBoardNode } from '@shared/domain/entity'; -import { - TestApiClient, - UserAndAccountTestFactory, - cleanupCollections, - columnBoardNodeFactory, - courseFactory, -} from '@shared/testing'; +import { TestApiClient, UserAndAccountTestFactory, cleanupCollections, courseFactory } from '@shared/testing'; +import { BoardNodeEntity } from '../../repo'; +import { columnBoardEntityFactory } from '../../testing'; +import { BoardExternalReferenceType } from '../../domain'; const baseRouteName = '/boards'; @@ -46,7 +41,7 @@ describe(`board update title (api)`, () => { const course = courseFactory.build({ teachers: [teacherUser] }); await em.persistAndFlush([teacherUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); @@ -75,7 +70,7 @@ describe(`board update title (api)`, () => { await loggedInClient.patch(`${columnBoardNode.id}/title`, { title: newTitle }); - const result = await em.findOneOrFail(ColumnBoardNode, columnBoardNode.id); + const result = await em.findOneOrFail(BoardNodeEntity, columnBoardNode.id); expect(result.title).toEqual(newTitle); }); @@ -87,7 +82,7 @@ describe(`board update title (api)`, () => { const sanitizedTitle = 'foo bar'; await loggedInClient.patch(`${columnBoardNode.id}/title`, { title: unsanitizedTitle }); - const result = await em.findOneOrFail(ColumnBoardNode, columnBoardNode.id); + const result = await em.findOneOrFail(BoardNodeEntity, columnBoardNode.id); expect(result.title).toEqual(sanitizedTitle); }); @@ -133,7 +128,7 @@ describe(`board update title (api)`, () => { await em.persistAndFlush([studentUser, course]); const title = 'old title'; - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ title, context: { id: course.id, type: BoardExternalReferenceType.Course }, }); @@ -155,7 +150,7 @@ describe(`board update title (api)`, () => { expect(response.status).toEqual(403); - const result = await em.findOneOrFail(ColumnBoardNode, columnBoardNode.id); + const result = await em.findOneOrFail(BoardNodeEntity, columnBoardNode.id); expect(result.title).toEqual(title); }); }); diff --git a/apps/server/src/modules/board/controller/api-test/board-visibility.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-visibility.api.spec.ts index 5b94f16f2db..5af52adbc7b 100644 --- a/apps/server/src/modules/board/controller/api-test/board-visibility.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-visibility.api.spec.ts @@ -2,15 +2,10 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { ServerTestModule } from '@modules/server/server.module'; import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { ColumnBoardNode } from '@shared/domain/entity'; -import { - TestApiClient, - UserAndAccountTestFactory, - cleanupCollections, - columnBoardNodeFactory, - courseFactory, -} from '@shared/testing'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; +import { TestApiClient, UserAndAccountTestFactory, cleanupCollections, courseFactory } from '@shared/testing'; +import { BoardNodeEntity } from '../../repo'; +import { columnBoardEntityFactory } from '../../testing'; +import { BoardExternalReferenceType } from '../../domain'; const baseRouteName = '/boards'; @@ -45,7 +40,7 @@ describe(`board update visibility (api)`, () => { const course = courseFactory.build({ teachers: [teacherUser] }); await em.persistAndFlush([teacherUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ isVisible: false, context: { id: course.id, type: BoardExternalReferenceType.Course }, }); @@ -75,7 +70,7 @@ describe(`board update visibility (api)`, () => { await loggedInClient.patch(`${columnBoardNode.id}/visibility`, { isVisible }); - const result = await em.findOneOrFail(ColumnBoardNode, columnBoardNode.id); + const result = await em.findOneOrFail(BoardNodeEntity, columnBoardNode.id); expect(result.isVisible).toEqual(isVisible); }); @@ -88,7 +83,7 @@ describe(`board update visibility (api)`, () => { const course = courseFactory.build({ students: [studentUser] }); await em.persistAndFlush([studentUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ isVisible: false, context: { id: course.id, type: BoardExternalReferenceType.Course }, }); @@ -114,7 +109,7 @@ describe(`board update visibility (api)`, () => { const { loggedInClient, columnBoardNode } = await setup(); const isVisible = true; await loggedInClient.patch(`${columnBoardNode.id}/visibility`, { isVisible }); - const result = await em.findOneOrFail(ColumnBoardNode, columnBoardNode.id); + const result = await em.findOneOrFail(BoardNodeEntity, columnBoardNode.id); expect(result.isVisible).toEqual(false); }); }); diff --git a/apps/server/src/modules/board/controller/api-test/card-create.api.spec.ts b/apps/server/src/modules/board/controller/api-test/card-create.api.spec.ts index fb1a5f0aeb9..4b21832f71e 100644 --- a/apps/server/src/modules/board/controller/api-test/card-create.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/card-create.api.spec.ts @@ -5,17 +5,11 @@ import { ServerTestModule } from '@modules/server/server.module'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; -import { BoardExternalReferenceType, ContentElementType } from '@shared/domain/domainobject'; -import { - cleanupCollections, - columnBoardNodeFactory, - columnNodeFactory, - courseFactory, - mapUserToCurrentUser, - userFactory, -} from '@shared/testing'; +import { cleanupCollections, courseFactory, mapUserToCurrentUser, userFactory } from '@shared/testing'; import { Request } from 'express'; import request from 'supertest'; +import { columnBoardEntityFactory, columnEntityFactory } from '../../testing'; +import { BoardExternalReferenceType, ContentElementType } from '../../domain'; import { CardResponse } from '../dto'; const baseRouteName = '/columns'; @@ -77,11 +71,11 @@ describe(`card create (api)`, () => { const course = courseFactory.build({ teachers: [user] }); await em.persistAndFlush([user, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); await em.persistAndFlush([columnBoardNode, columnNode]); em.clear(); diff --git a/apps/server/src/modules/board/controller/api-test/card-delete.api.spec.ts b/apps/server/src/modules/board/controller/api-test/card-delete.api.spec.ts index 2380deddfed..636d41153c3 100644 --- a/apps/server/src/modules/board/controller/api-test/card-delete.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/card-delete.api.spec.ts @@ -1,70 +1,37 @@ import { EntityManager } from '@mikro-orm/mongodb'; -import { ICurrentUser } from '@modules/authentication'; -import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; import { ServerTestModule } from '@modules/server'; -import { ExecutionContext, INestApplication } from '@nestjs/common'; +import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { ApiValidationError } from '@shared/common'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; -import { CardNode, RichTextElementNode } from '@shared/domain/entity'; +import { cleanupCollections, courseFactory, TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; +import { BoardExternalReferenceType } from '../../domain'; +import { BoardNodeEntity } from '../../repo'; import { - cardNodeFactory, - cleanupCollections, - columnBoardNodeFactory, - columnNodeFactory, - courseFactory, - mapUserToCurrentUser, - richTextElementNodeFactory, - userFactory, -} from '@shared/testing'; -import { Request } from 'express'; -import request from 'supertest'; + cardEntityFactory, + columnBoardEntityFactory, + columnEntityFactory, + richTextElementEntityFactory, +} from '../../testing'; const baseRouteName = '/cards'; -class API { - app: INestApplication; - - constructor(app: INestApplication) { - this.app = app; - } - - async delete(cardId: string) { - const response = await request(this.app.getHttpServer()) - .delete(`${baseRouteName}/${cardId}`) - .set('Accept', 'application/json'); - - return { - error: response.body as ApiValidationError, - status: response.status, - }; - } -} - describe(`card delete (api)`, () => { let app: INestApplication; let em: EntityManager; - let currentUser: ICurrentUser; - let api: API; + let testApiClient: TestApiClient; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [ServerTestModule], - }) - .overrideGuard(JwtAuthGuard) - .useValue({ - canActivate(context: ExecutionContext) { - const req: Request = context.switchToHttp().getRequest(); - req.user = currentUser; - return true; - }, - }) - .compile(); + }).compile(); app = module.createNestApplication(); await app.init(); em = module.get(EntityManager); - api = new API(app); + testApiClient = new TestApiClient(app, baseRouteName); + }); + + beforeEach(async () => { + await cleanupCollections(em); }); afterAll(async () => { @@ -72,86 +39,89 @@ describe(`card delete (api)`, () => { }); const setup = async () => { - await cleanupCollections(em); - const user = userFactory.build(); - const course = courseFactory.build({ teachers: [user] }); - await em.persistAndFlush([user, course]); + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const course = courseFactory.build({ teachers: [teacherUser] }); + await em.persistAndFlush([teacherUser, teacherAccount, course]); + + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); - const richTextElementNode = richTextElementNodeFactory.buildWithId({ parent: cardNode }); - const siblingCardNode = cardNodeFactory.buildWithId({ parent: columnNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); + const cardNode = cardEntityFactory.withParent(columnNode).build({ position: 0 }); + const richTextElementNode = richTextElementEntityFactory.withParent(cardNode).build(); + const siblingCardNode = cardEntityFactory.withParent(columnNode).build({ position: 1 }); await em.persistAndFlush([columnBoardNode, columnNode, cardNode, siblingCardNode, richTextElementNode]); em.clear(); - return { user, cardNode, columnBoardNode, columnNode, siblingCardNode, richTextElementNode }; + const loggedInClient = await testApiClient.login(teacherAccount); + + return { loggedInClient, cardNode, columnBoardNode, columnNode, siblingCardNode, richTextElementNode }; }; describe('with valid user', () => { it('should return status 204', async () => { - const { user, cardNode } = await setup(); - currentUser = mapUserToCurrentUser(user); + const { loggedInClient, cardNode } = await setup(); - const response = await api.delete(cardNode.id); + const response = await loggedInClient.delete(cardNode.id); expect(response.status).toEqual(204); }); it('should actually delete card', async () => { - const { user, cardNode } = await setup(); - currentUser = mapUserToCurrentUser(user); + const { loggedInClient, cardNode } = await setup(); - await api.delete(cardNode.id); + await loggedInClient.delete(cardNode.id); - await expect(em.findOneOrFail(CardNode, cardNode.id)).rejects.toThrow(); + await expect(em.findOneOrFail(BoardNodeEntity, cardNode.id)).rejects.toThrow(); }); it('should actually delete elements of the card', async () => { - const { user, cardNode, richTextElementNode } = await setup(); - currentUser = mapUserToCurrentUser(user); + const { loggedInClient, cardNode, richTextElementNode } = await setup(); - await api.delete(cardNode.id); + await loggedInClient.delete(cardNode.id); - await expect(em.findOneOrFail(RichTextElementNode, richTextElementNode.id)).rejects.toThrow(); + await expect(em.findOneOrFail(BoardNodeEntity, richTextElementNode.id)).rejects.toThrow(); }); it('should not delete siblings', async () => { - const { user, cardNode, siblingCardNode } = await setup(); - currentUser = mapUserToCurrentUser(user); + const { loggedInClient, cardNode, siblingCardNode } = await setup(); - await api.delete(cardNode.id); + await loggedInClient.delete(cardNode.id); - const siblingFromDb = await em.findOneOrFail(CardNode, siblingCardNode.id); + const siblingFromDb = await em.findOneOrFail(BoardNodeEntity, siblingCardNode.id); expect(siblingFromDb).toBeDefined(); }); it('should update position of the siblings', async () => { - const { user, cardNode, siblingCardNode } = await setup(); - currentUser = mapUserToCurrentUser(user); + const { loggedInClient, cardNode, siblingCardNode } = await setup(); - cardNode.position = 0; - siblingCardNode.position = 1; + await loggedInClient.delete(cardNode.id); - await api.delete(cardNode.id); - - const siblingFromDb = await em.findOneOrFail(CardNode, siblingCardNode.id); + const siblingFromDb = await em.findOneOrFail(BoardNodeEntity, siblingCardNode.id); expect(siblingFromDb.position).toEqual(0); }); }); describe('with invalid user', () => { - it('should return status 403', async () => { - const { cardNode } = await setup(); + const setupNoAccess = async () => { + const vars = await setup(); - const invalidUser = userFactory.build(); - await em.persistAndFlush([invalidUser]); - currentUser = mapUserToCurrentUser(invalidUser); + const { studentAccount: noAccessAccount, studentUser: noAccessUser } = UserAndAccountTestFactory.buildStudent(); + await em.persistAndFlush([noAccessAccount, noAccessUser]); + const loggedInClient = await testApiClient.login(noAccessAccount); + + return { + ...vars, + loggedInClient, + }; + }; + + it('should return status 403', async () => { + const { loggedInClient, cardNode } = await setupNoAccess(); - const response = await api.delete(cardNode.id); + const response = await loggedInClient.delete(cardNode.id); expect(response.status).toEqual(403); }); diff --git a/apps/server/src/modules/board/controller/api-test/card-lookup.api.spec.ts b/apps/server/src/modules/board/controller/api-test/card-lookup.api.spec.ts index 89f2d5fd816..8ff95ec8562 100644 --- a/apps/server/src/modules/board/controller/api-test/card-lookup.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/card-lookup.api.spec.ts @@ -1,74 +1,37 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; -import { ICurrentUser } from '@modules/authentication'; -import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; import { ServerTestModule } from '@modules/server/server.module'; -import { ExecutionContext, INestApplication } from '@nestjs/common'; +import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { ApiValidationError } from '@shared/common'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; +import { cleanupCollections, courseFactory, TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; +import { BoardExternalReferenceType } from '../../domain'; import { - cardNodeFactory, - cleanupCollections, - columnBoardNodeFactory, - columnNodeFactory, - courseFactory, - mapUserToCurrentUser, - richTextElementNodeFactory, - roleFactory, - schoolEntityFactory, - userFactory, -} from '@shared/testing'; -import { Request } from 'express'; -import request from 'supertest'; -import { CardIdsParams, CardListResponse } from '../dto'; + cardEntityFactory, + columnBoardEntityFactory, + columnEntityFactory, + richTextElementEntityFactory, +} from '../../testing'; +import { CardListResponse } from '../dto'; const baseRouteName = '/cards'; -class API { - app: INestApplication; - - constructor(app: INestApplication) { - this.app = app; - } - - async get(query: CardIdsParams) { - const response = await request(this.app.getHttpServer()) - .get(baseRouteName) - .set('Accept', 'application/json') - .query(query || {}); - - return { - result: response.body as CardListResponse, - error: response.body as ApiValidationError, - status: response.status, - }; - } -} - describe(`card lookup (api)`, () => { let app: INestApplication; let em: EntityManager; - let currentUser: ICurrentUser; - let api: API; + let testApiClient: TestApiClient; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [ServerTestModule], - }) - .overrideGuard(JwtAuthGuard) - .useValue({ - canActivate(context: ExecutionContext) { - const req: Request = context.switchToHttp().getRequest(); - req.user = currentUser; - return true; - }, - }) - .compile(); + }).compile(); app = module.createNestApplication(); await app.init(); em = module.get(EntityManager); - api = new API(app); + testApiClient = new TestApiClient(app, baseRouteName); + }); + + beforeEach(async () => { + await cleanupCollections(em); }); afterAll(async () => { @@ -76,58 +39,61 @@ describe(`card lookup (api)`, () => { }); const setup = async () => { - await cleanupCollections(em); - const school = schoolEntityFactory.build(); - const roles = roleFactory.buildList(1, { - // permissions: [Permission.COURSE_CREATE], - }); - const user = userFactory.build({ school, roles }); - const course = courseFactory.buildWithId({ teachers: [user] }); - await em.persistAndFlush([user, course]); + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); + const course = courseFactory.buildWithId({ teachers: [teacherUser] }); + await em.persistAndFlush([teacherUser, teacherAccount, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); - const cardNode1 = cardNodeFactory.buildWithId({ parent: columnNode }); - const cardNode2 = cardNodeFactory.buildWithId({ parent: columnNode }); - const cardNode3 = cardNodeFactory.buildWithId({ parent: columnNode }); - const richTextElement = richTextElementNodeFactory.buildWithId({ parent: cardNode1 }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); + const cardNode1 = cardEntityFactory.withParent(columnNode).build(); + const cardNode2 = cardEntityFactory.withParent(columnNode).build(); + const cardNode3 = cardEntityFactory.withParent(columnNode).build(); + const richTextElement = richTextElementEntityFactory.withParent(cardNode1).build(); await em.persistAndFlush([columnBoardNode, columnNode, cardNode1, cardNode2, cardNode3, richTextElement]); - await em.persistAndFlush([columnBoardNode, columnNode, cardNode1, cardNode2, cardNode3]); em.clear(); - currentUser = mapUserToCurrentUser(user); + const loggedInClient = await testApiClient.login(teacherAccount); - return { columnBoardNode, columnNode, card1: cardNode1, card2: cardNode2, card3: cardNode3, user, course }; + return { + loggedInClient, + columnBoardNode, + columnNode, + card1: cardNode1, + card2: cardNode2, + card3: cardNode3, + course, + }; }; describe('with valid card ids', () => { it('should return status 200', async () => { - const { user, card1 } = await setup(); - currentUser = mapUserToCurrentUser(user); + const { loggedInClient, card1 } = await setup(); - const response = await api.get({ ids: [card1.id] }); + const response = await loggedInClient.get().query({ ids: [card1.id] }); expect(response.status).toEqual(200); }); it('should return one card for a single id', async () => { - const { user, card1 } = await setup(); - currentUser = mapUserToCurrentUser(user); + const { loggedInClient, card1 } = await setup(); + + const response = await loggedInClient.get().query({ ids: [card1.id] }); - const { result } = await api.get({ ids: [card1.id] }); + const result = response.body as CardListResponse; expect(result.data).toHaveLength(1); expect(result.data[0].id).toBe(card1.id); }); it('should return multiple cards for multiple ids', async () => { - const { user, card1, card2 } = await setup(); - currentUser = mapUserToCurrentUser(user); + const { loggedInClient, card1, card2 } = await setup(); - const { result } = await api.get({ ids: [card1.id, card2.id] }); + const response = await loggedInClient.get().query({ ids: [card1.id, card2.id] }); + + const result = response.body as CardListResponse; expect(result.data).toHaveLength(2); const returnedIds = result.data.map((c) => c.id); @@ -138,24 +104,24 @@ describe(`card lookup (api)`, () => { describe('with invalid card ids', () => { it('should return empty array if card id does not exist', async () => { - const { user } = await setup(); - currentUser = mapUserToCurrentUser(user); + const { loggedInClient } = await setup(); const notExistingCardId = new ObjectId().toHexString(); - const { result, status } = await api.get({ ids: [notExistingCardId] }); + const response = await loggedInClient.get().query({ ids: [notExistingCardId] }); + const result = response.body as CardListResponse; - expect(status).toEqual(200); + expect(response.status).toEqual(200); expect(result.data).toHaveLength(0); }); it('should return only results of existing cards', async () => { - const { user, card1, card2 } = await setup(); - currentUser = mapUserToCurrentUser(user); + const { loggedInClient, card1, card2 } = await setup(); const notExistingCardId = new ObjectId().toHexString(); - const { result } = await api.get({ ids: [card1.id, notExistingCardId, card2.id] }); + const response = await loggedInClient.get().query({ ids: [card1.id, notExistingCardId, card2.id] }); + const result = response.body as CardListResponse; expect(result.data).toHaveLength(2); const returnedIds = result.data.map((c) => c.id); @@ -165,17 +131,27 @@ describe(`card lookup (api)`, () => { }); describe('with invalid user', () => { - it('should return status 200', async () => { - const { card1 } = await setup(); + const setupNoAccess = async () => { + const vars = await setup(); + + const { studentAccount: noAccessAccount, studentUser: noAccessUser } = UserAndAccountTestFactory.buildStudent(); + await em.persistAndFlush([noAccessAccount, noAccessUser]); + const loggedInClient = await testApiClient.login(noAccessAccount); - const invalidUser = userFactory.build(); - await em.persistAndFlush([invalidUser]); - currentUser = mapUserToCurrentUser(invalidUser); + return { + ...vars, + loggedInClient, + }; + }; + + it('should return status 200', async () => { + const { loggedInClient, card1 } = await setupNoAccess(); - const response = await api.get({ ids: [card1.id] }); + const response = await loggedInClient.get().query({ ids: [card1.id] }); + const result = response.body as CardListResponse; expect(response.status).toEqual(200); - expect(response.result).toEqual({ data: [] }); + expect(result).toEqual({ data: [] }); }); }); }); diff --git a/apps/server/src/modules/board/controller/api-test/card-move.api.spec.ts b/apps/server/src/modules/board/controller/api-test/card-move.api.spec.ts index 558883302bf..6157c340669 100644 --- a/apps/server/src/modules/board/controller/api-test/card-move.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/card-move.api.spec.ts @@ -1,70 +1,33 @@ import { EntityManager } from '@mikro-orm/mongodb'; -import { ICurrentUser } from '@modules/authentication'; -import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; import { ServerTestModule } from '@modules/server/server.module'; -import { ExecutionContext, INestApplication } from '@nestjs/common'; +import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { ApiValidationError } from '@shared/common'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; -import { CardNode } from '@shared/domain/entity'; -import { - cardNodeFactory, - cleanupCollections, - columnBoardNodeFactory, - columnNodeFactory, - courseFactory, - mapUserToCurrentUser, - userFactory, -} from '@shared/testing'; -import { Request } from 'express'; -import request from 'supertest'; +import { cleanupCollections, courseFactory, TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; +import { BoardExternalReferenceType, pathOfChildren } from '../../domain'; +import { BoardNodeEntity } from '../../repo'; +import { cardEntityFactory, columnBoardEntityFactory, columnEntityFactory } from '../../testing'; +import { MoveCardBodyParams } from '../dto'; const baseRouteName = '/cards'; -class API { - app: INestApplication; - - constructor(app: INestApplication) { - this.app = app; - } - - async move(cardId: string, toColumnId: string, toPosition: number) { - const response = await request(this.app.getHttpServer()) - .put(`${baseRouteName}/${cardId}/position`) - .set('Accept', 'application/json') - .send({ toColumnId, toPosition }); - - return { - error: response.body as ApiValidationError, - status: response.status, - }; - } -} - describe(`card move (api)`, () => { let app: INestApplication; let em: EntityManager; - let currentUser: ICurrentUser; - let api: API; + let testApiClient: TestApiClient; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [ServerTestModule], - }) - .overrideGuard(JwtAuthGuard) - .useValue({ - canActivate(context: ExecutionContext) { - const req: Request = context.switchToHttp().getRequest(); - req.user = currentUser; - return true; - }, - }) - .compile(); + }).compile(); app = module.createNestApplication(); await app.init(); em = module.get(EntityManager); - api = new API(app); + testApiClient = new TestApiClient(app, baseRouteName); + }); + + beforeEach(async () => { + await cleanupCollections(em); }); afterAll(async () => { @@ -72,74 +35,85 @@ describe(`card move (api)`, () => { }); const setup = async () => { - await cleanupCollections(em); - const user = userFactory.build(); - const course = courseFactory.build({ teachers: [user] }); - await em.persistAndFlush([user, course]); + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const course = courseFactory.build({ teachers: [teacherUser] }); + await em.persistAndFlush([teacherUser, teacherAccount, course]); + + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const parentColumn = columnNodeFactory.buildWithId({ parent: columnBoardNode }); - const cardNode1 = cardNodeFactory.buildWithId({ parent: parentColumn }); - const cardNode2 = cardNodeFactory.buildWithId({ parent: parentColumn }); - const targetColumn = columnNodeFactory.buildWithId({ parent: columnBoardNode }); - const targetColumnCards = cardNodeFactory.buildListWithId(4, { parent: targetColumn }); - - await em.persistAndFlush([ - user, - cardNode1, - cardNode2, - parentColumn, - targetColumn, - columnBoardNode, - ...targetColumnCards, - ]); + const parentColumn = columnEntityFactory.withParent(columnBoardNode).build(); + const cardNode1 = cardEntityFactory.withParent(parentColumn).build(); + const cardNode2 = cardEntityFactory.withParent(parentColumn).build(); + const targetColumn = columnEntityFactory.withParent(columnBoardNode).build(); + const targetColumnCards = cardEntityFactory.withParent(targetColumn).buildList(4); + + await em.persistAndFlush([cardNode1, cardNode2, parentColumn, targetColumn, columnBoardNode, ...targetColumnCards]); em.clear(); - return { user, cardNode1, cardNode2, parentColumn, targetColumn, columnBoardNode, targetColumnCards }; + const loggedInClient = await testApiClient.login(teacherAccount); + + return { loggedInClient, cardNode1, cardNode2, parentColumn, targetColumn, columnBoardNode, targetColumnCards }; }; describe('with valid user', () => { it('should return status 204', async () => { - const { user, cardNode1, targetColumn } = await setup(); - currentUser = mapUserToCurrentUser(user); + const { loggedInClient, cardNode1, targetColumn } = await setup(); + + const params: MoveCardBodyParams = { + toColumnId: targetColumn.id, + toPosition: 3, + }; - const response = await api.move(cardNode1.id, targetColumn.id, 3); + const response = await loggedInClient.put(`${cardNode1.id}/position`, params); expect(response.status).toEqual(204); }); it('should actually move the card', async () => { - const { user, cardNode1, targetColumn } = await setup(); - currentUser = mapUserToCurrentUser(user); + const { loggedInClient, cardNode1, targetColumn } = await setup(); + + const params: MoveCardBodyParams = { + toColumnId: targetColumn.id, + toPosition: 3, + }; - await api.move(cardNode1.id, targetColumn.id, 3); - const result = await em.findOneOrFail(CardNode, cardNode1.id); + await loggedInClient.put(`${cardNode1.id}/position`, params); - expect(result.parentId).toEqual(targetColumn.id); + const result = await em.findOneOrFail(BoardNodeEntity, cardNode1.id); + + expect(result.path).toEqual(pathOfChildren(targetColumn)); expect(result.position).toEqual(3); }); describe('when moving a card within the same column', () => { it('should keep the card parent', async () => { - const { user, cardNode2, parentColumn } = await setup(); - currentUser = mapUserToCurrentUser(user); + const { loggedInClient, cardNode2, parentColumn } = await setup(); + + const params: MoveCardBodyParams = { + toColumnId: parentColumn.id, + toPosition: 3, + }; - await api.move(cardNode2.id, parentColumn.id, 0); + await loggedInClient.put(`${cardNode2.id}/position`, params); - const result = await em.findOneOrFail(CardNode, cardNode2.id); - expect(result.parentId).toEqual(parentColumn.id); + const result = await em.findOneOrFail(BoardNodeEntity, cardNode2.id); + expect(result.path).toEqual(pathOfChildren(parentColumn)); }); it('should update the card positions', async () => { - const { user, cardNode1, cardNode2, parentColumn } = await setup(); - currentUser = mapUserToCurrentUser(user); + const { loggedInClient, cardNode1, cardNode2, parentColumn } = await setup(); - await api.move(cardNode2.id, parentColumn.id, 0); + const params: MoveCardBodyParams = { + toColumnId: parentColumn.id, + toPosition: 0, + }; - const result1 = await em.findOneOrFail(CardNode, cardNode1.id); - const result2 = await em.findOneOrFail(CardNode, cardNode2.id); + await loggedInClient.put(`${cardNode2.id}/position`, params); + + const result1 = await em.findOneOrFail(BoardNodeEntity, cardNode1.id); + const result2 = await em.findOneOrFail(BoardNodeEntity, cardNode2.id); expect(result1.position).toEqual(1); expect(result2.position).toEqual(0); }); @@ -147,14 +121,28 @@ describe(`card move (api)`, () => { }); describe('with invalid user', () => { + const setupNoAccess = async () => { + const vars = await setup(); + + const { studentAccount: noAccessAccount, studentUser: noAccessUser } = UserAndAccountTestFactory.buildStudent(); + await em.persistAndFlush([noAccessAccount, noAccessUser]); + const loggedInClient = await testApiClient.login(noAccessAccount); + + return { + ...vars, + loggedInClient, + }; + }; + it('should return status 403', async () => { - const { cardNode1, targetColumn } = await setup(); + const { loggedInClient, cardNode1, targetColumn } = await setupNoAccess(); - const invalidUser = userFactory.build(); - await em.persistAndFlush([invalidUser]); - currentUser = mapUserToCurrentUser(invalidUser); + const params: MoveCardBodyParams = { + toColumnId: targetColumn.id, + toPosition: 3, + }; - const response = await api.move(cardNode1.id, targetColumn.id, 3); + const response = await loggedInClient.put(`${cardNode1.id}/position`, params); expect(response.status).toEqual(403); }); diff --git a/apps/server/src/modules/board/controller/api-test/card-update-height.api.spec.ts b/apps/server/src/modules/board/controller/api-test/card-update-height.api.spec.ts index a2ca9ce993d..a433c736383 100644 --- a/apps/server/src/modules/board/controller/api-test/card-update-height.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/card-update-height.api.spec.ts @@ -2,17 +2,10 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { ServerTestModule } from '@modules/server/server.module'; import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; -import { CardNode } from '@shared/domain/entity'; -import { - TestApiClient, - UserAndAccountTestFactory, - cardNodeFactory, - cleanupCollections, - columnBoardNodeFactory, - columnNodeFactory, - courseFactory, -} from '@shared/testing'; +import { TestApiClient, UserAndAccountTestFactory, cleanupCollections, courseFactory } from '@shared/testing'; +import { BoardNodeEntity } from '../../repo'; +import { cardEntityFactory, columnBoardEntityFactory, columnEntityFactory } from '../../testing'; +import { BoardExternalReferenceType } from '../../domain'; describe(`card update height (api)`, () => { let app: INestApplication; @@ -43,11 +36,11 @@ describe(`card update height (api)`, () => { const course = courseFactory.build({ teachers: [teacherUser] }); await em.persistAndFlush([teacherAccount, teacherUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); + const cardNode = cardEntityFactory.withParent(columnNode).build(); await em.persistAndFlush([cardNode, columnNode, columnBoardNode]); em.clear(); @@ -74,7 +67,7 @@ describe(`card update height (api)`, () => { await teacherClient.patch(`${cardNode.id}/height`, { height: newHeight }); - const result = await em.findOneOrFail(CardNode, cardNode.id); + const result = await em.findOneOrFail(BoardNodeEntity, cardNode.id); expect(result.height).toEqual(newHeight); }); diff --git a/apps/server/src/modules/board/controller/api-test/card-update-title.api.spec.ts b/apps/server/src/modules/board/controller/api-test/card-update-title.api.spec.ts index 91077ed9476..054d5b61aa1 100644 --- a/apps/server/src/modules/board/controller/api-test/card-update-title.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/card-update-title.api.spec.ts @@ -2,17 +2,10 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { ServerTestModule } from '@modules/server/server.module'; import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; -import { CardNode } from '@shared/domain/entity'; -import { - cardNodeFactory, - cleanupCollections, - columnBoardNodeFactory, - columnNodeFactory, - courseFactory, - TestApiClient, - UserAndAccountTestFactory, -} from '@shared/testing'; +import { cleanupCollections, courseFactory, TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; +import { BoardExternalReferenceType } from '../../domain'; +import { BoardNodeEntity } from '../../repo'; +import { cardEntityFactory, columnBoardEntityFactory, columnEntityFactory } from '../../testing'; const baseRouteName = '/cards'; @@ -42,20 +35,18 @@ describe(`card update title (api)`, () => { describe('with valid user', () => { const setup = async () => { - await cleanupCollections(em); - const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); const course = courseFactory.build({ teachers: [teacherUser] }); - await em.persistAndFlush([teacherUser, course]); + await em.persistAndFlush([teacherUser, teacherAccount, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); + const cardNode = cardEntityFactory.withParent(columnNode).build(); - await em.persistAndFlush([teacherAccount, teacherUser, cardNode, columnNode, columnBoardNode]); + await em.persistAndFlush([cardNode, columnNode, columnBoardNode]); em.clear(); const loggedInClient = await testApiClient.login(teacherAccount); @@ -79,7 +70,7 @@ describe(`card update title (api)`, () => { await loggedInClient.patch(`${cardNode.id}/title`, { title: newTitle }); - const result = await em.findOneOrFail(CardNode, cardNode.id); + const result = await em.findOneOrFail(BoardNodeEntity, cardNode.id); expect(result.title).toEqual(newTitle); }); @@ -91,7 +82,7 @@ describe(`card update title (api)`, () => { const sanitizedTitle = 'foo bar'; await loggedInClient.patch(`${cardNode.id}/title`, { title: unsanitizedTitle }); - const result = await em.findOneOrFail(CardNode, cardNode.id); + const result = await em.findOneOrFail(BoardNodeEntity, cardNode.id); expect(result.title).toEqual(sanitizedTitle); }); @@ -99,22 +90,20 @@ describe(`card update title (api)`, () => { describe('with non authorised user', () => { const setup = async () => { - await cleanupCollections(em); - const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); const course = courseFactory.build({ students: [studentUser] }); - await em.persistAndFlush([studentUser, course]); + await em.persistAndFlush([studentUser, studentAccount, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); const title = 'old title'; - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode, title }); + const cardNode = cardEntityFactory.withParent(columnNode).build({ title }); - await em.persistAndFlush([studentAccount, studentUser, cardNode, columnNode, columnBoardNode]); + await em.persistAndFlush([cardNode, columnNode, columnBoardNode]); em.clear(); const loggedInClient = await testApiClient.login(studentAccount); @@ -130,7 +119,7 @@ describe(`card update title (api)`, () => { const response = await loggedInClient.patch(`${cardNode.id}/title`, { title: newTitle }); expect(response.statusCode).toEqual(403); - const result = await em.findOneOrFail(CardNode, cardNode.id); + const result = await em.findOneOrFail(BoardNodeEntity, cardNode.id); expect(result.title).toEqual(title); }); }); diff --git a/apps/server/src/modules/board/controller/api-test/column-create.api.spec.ts b/apps/server/src/modules/board/controller/api-test/column-create.api.spec.ts index bd48042d962..c948f189ff3 100644 --- a/apps/server/src/modules/board/controller/api-test/column-create.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/column-create.api.spec.ts @@ -5,16 +5,11 @@ import { ServerTestModule } from '@modules/server/server.module'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; -import { - cleanupCollections, - columnBoardNodeFactory, - courseFactory, - mapUserToCurrentUser, - userFactory, -} from '@shared/testing'; +import { cleanupCollections, courseFactory, mapUserToCurrentUser, userFactory } from '@shared/testing'; import { Request } from 'express'; import request from 'supertest'; +import { columnBoardEntityFactory } from '../../testing'; +import { BoardExternalReferenceType } from '../../domain'; import { ColumnResponse } from '../dto'; const baseRouteName = '/boards'; @@ -75,7 +70,7 @@ describe(`board create (api)`, () => { const course = courseFactory.build({ teachers: [user] }); await em.persistAndFlush([user, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); diff --git a/apps/server/src/modules/board/controller/api-test/column-delete.api.spec.ts b/apps/server/src/modules/board/controller/api-test/column-delete.api.spec.ts index fc2af6374b1..dfad720936a 100644 --- a/apps/server/src/modules/board/controller/api-test/column-delete.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/column-delete.api.spec.ts @@ -1,69 +1,32 @@ import { EntityManager } from '@mikro-orm/mongodb'; -import { ICurrentUser } from '@modules/authentication'; -import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; import { ServerTestModule } from '@modules/server/server.module'; -import { ExecutionContext, INestApplication } from '@nestjs/common'; +import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { ApiValidationError } from '@shared/common'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; -import { CardNode, ColumnNode } from '@shared/domain/entity'; -import { - cardNodeFactory, - cleanupCollections, - columnBoardNodeFactory, - columnNodeFactory, - courseFactory, - mapUserToCurrentUser, - userFactory, -} from '@shared/testing'; -import { Request } from 'express'; -import request from 'supertest'; +import { cleanupCollections, courseFactory, TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; +import { BoardExternalReferenceType } from '../../domain'; +import { BoardNodeEntity } from '../../repo'; +import { cardEntityFactory, columnBoardEntityFactory, columnEntityFactory } from '../../testing'; const baseRouteName = '/columns'; -class API { - app: INestApplication; - - constructor(app: INestApplication) { - this.app = app; - } - - async delete(columnId: string) { - const response = await request(this.app.getHttpServer()) - .delete(`${baseRouteName}/${columnId}`) - .set('Accept', 'application/json'); - - return { - error: response.body as ApiValidationError, - status: response.status, - }; - } -} - describe(`column delete (api)`, () => { let app: INestApplication; let em: EntityManager; - let currentUser: ICurrentUser; - let api: API; + let testApiClient: TestApiClient; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [ServerTestModule], - }) - .overrideGuard(JwtAuthGuard) - .useValue({ - canActivate(context: ExecutionContext) { - const req: Request = context.switchToHttp().getRequest(); - req.user = currentUser; - return true; - }, - }) - .compile(); + }).compile(); app = module.createNestApplication(); await app.init(); em = module.get(EntityManager); - api = new API(app); + testApiClient = new TestApiClient(app, baseRouteName); + }); + + beforeEach(async () => { + await cleanupCollections(em); }); afterAll(async () => { @@ -71,74 +34,81 @@ describe(`column delete (api)`, () => { }); const setup = async () => { - await cleanupCollections(em); - const user = userFactory.build(); - const course = courseFactory.build({ teachers: [user] }); - await em.persistAndFlush([user, course]); + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); + + const course = courseFactory.build({ teachers: [teacherUser] }); + await em.persistAndFlush([teacherUser, teacherAccount, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); - const siblingColumnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); + const siblingColumnNode = columnEntityFactory.withParent(columnBoardNode).build(); + const cardNode = cardEntityFactory.withParent(columnNode).build(); - await em.persistAndFlush([user, cardNode, columnNode, columnBoardNode, siblingColumnNode]); + await em.persistAndFlush([cardNode, columnNode, columnBoardNode, siblingColumnNode]); em.clear(); - return { user, cardNode, columnNode, columnBoardNode, siblingColumnNode }; + const loggedInClient = await testApiClient.login(teacherAccount); + + return { loggedInClient, cardNode, columnNode, columnBoardNode, siblingColumnNode }; }; describe('with valid user', () => { it('should return status 204', async () => { - const { user, columnNode } = await setup(); - currentUser = mapUserToCurrentUser(user); + const { loggedInClient, columnNode } = await setup(); - const response = await api.delete(columnNode.id); + const response = await loggedInClient.delete(columnNode.id); expect(response.status).toEqual(204); }); it('should actually delete the column', async () => { - const { user, columnNode } = await setup(); - currentUser = mapUserToCurrentUser(user); + const { loggedInClient, columnNode } = await setup(); - await api.delete(columnNode.id); + await loggedInClient.delete(columnNode.id); - await expect(em.findOneOrFail(ColumnNode, columnNode.id)).rejects.toThrow(); + await expect(em.findOneOrFail(BoardNodeEntity, columnNode.id)).rejects.toThrow(); }); it('should actually delete cards of the column', async () => { - const { user, cardNode, columnNode } = await setup(); - currentUser = mapUserToCurrentUser(user); + const { loggedInClient, cardNode, columnNode } = await setup(); - await api.delete(columnNode.id); + await loggedInClient.delete(columnNode.id); - await expect(em.findOneOrFail(CardNode, cardNode.id)).rejects.toThrow(); + await expect(em.findOneOrFail(BoardNodeEntity, cardNode.id)).rejects.toThrow(); }); it('should not delete siblings', async () => { - const { user, columnNode, siblingColumnNode } = await setup(); - currentUser = mapUserToCurrentUser(user); + const { loggedInClient, columnNode, siblingColumnNode } = await setup(); - await api.delete(columnNode.id); + await loggedInClient.delete(columnNode.id); - await expect(em.findOneOrFail(ColumnNode, columnNode.id)).rejects.toThrow(); + await expect(em.findOneOrFail(BoardNodeEntity, columnNode.id)).rejects.toThrow(); - const siblingFromDb = await em.findOneOrFail(ColumnNode, siblingColumnNode.id); + const siblingFromDb = await em.findOneOrFail(BoardNodeEntity, siblingColumnNode.id); expect(siblingFromDb).toBeDefined(); }); }); describe('with invalid user', () => { - it('should return status 403', async () => { - const { columnNode } = await setup(); + const setupNoAccess = async () => { + const vars = await setup(); - const invalidUser = userFactory.build(); - await em.persistAndFlush([invalidUser]); - currentUser = mapUserToCurrentUser(invalidUser); + const { studentAccount: noAccessAccount, studentUser: noAccessUser } = UserAndAccountTestFactory.buildStudent(); + await em.persistAndFlush([noAccessAccount, noAccessUser]); + const loggedInClient = await testApiClient.login(noAccessAccount); + + return { + ...vars, + loggedInClient, + }; + }; + + it('should return status 403', async () => { + const { loggedInClient, columnNode } = await setupNoAccess(); - const response = await api.delete(columnNode.id); + const response = await loggedInClient.delete(columnNode.id); expect(response.status).toEqual(403); }); diff --git a/apps/server/src/modules/board/controller/api-test/column-move.api.spec.ts b/apps/server/src/modules/board/controller/api-test/column-move.api.spec.ts index 5db11857367..dac3478a50c 100644 --- a/apps/server/src/modules/board/controller/api-test/column-move.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/column-move.api.spec.ts @@ -1,70 +1,33 @@ import { EntityManager } from '@mikro-orm/mongodb'; -import { ICurrentUser } from '@modules/authentication'; -import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; import { ServerTestModule } from '@modules/server/server.module'; -import { ExecutionContext, INestApplication } from '@nestjs/common'; +import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { ApiValidationError } from '@shared/common'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; -import { ColumnNode } from '@shared/domain/entity'; -import { - cardNodeFactory, - cleanupCollections, - columnBoardNodeFactory, - columnNodeFactory, - courseFactory, - mapUserToCurrentUser, - userFactory, -} from '@shared/testing'; -import { Request } from 'express'; -import request from 'supertest'; +import { cleanupCollections, courseFactory, TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; +import { BoardExternalReferenceType } from '../../domain'; +import { BoardNodeEntity } from '../../repo'; +import { cardEntityFactory, columnBoardEntityFactory, columnEntityFactory } from '../../testing'; +import { MoveColumnBodyParams } from '../dto'; const baseRouteName = '/columns'; -class API { - app: INestApplication; - - constructor(app: INestApplication) { - this.app = app; - } - - async move(columnId: string, toBoardId: string, toPosition: number) { - const response = await request(this.app.getHttpServer()) - .put(`${baseRouteName}/${columnId}/position`) - .set('Accept', 'application/json') - .send({ toBoardId, toPosition }); - - return { - error: response.body as ApiValidationError, - status: response.status, - }; - } -} - describe(`column move (api)`, () => { let app: INestApplication; let em: EntityManager; - let currentUser: ICurrentUser; - let api: API; + let testApiClient: TestApiClient; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [ServerTestModule], - }) - .overrideGuard(JwtAuthGuard) - .useValue({ - canActivate(context: ExecutionContext) { - const req: Request = context.switchToHttp().getRequest(); - req.user = currentUser; - return true; - }, - }) - .compile(); + }).compile(); app = module.createNestApplication(); await app.init(); em = module.get(EntityManager); - api = new API(app); + testApiClient = new TestApiClient(app, baseRouteName); + }); + + beforeEach(async () => { + await cleanupCollections(em); }); afterAll(async () => { @@ -72,57 +35,81 @@ describe(`column move (api)`, () => { }); const setup = async () => { - await cleanupCollections(em); - const user = userFactory.build(); - const course = courseFactory.build({ teachers: [user] }); - await em.persistAndFlush([user, course]); + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const course = courseFactory.build({ teachers: [teacherUser] }); + await em.persistAndFlush([teacherUser, teacherAccount, course]); + + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); const columnNodes = new Array(10) .fill(1) - .map((_, i) => columnNodeFactory.buildWithId({ parent: columnBoardNode, position: i })); + .map((_, i) => columnEntityFactory.withParent(columnBoardNode).build({ position: i })); const columnToMove = columnNodes[2]; - const cardNode = cardNodeFactory.buildWithId({ parent: columnToMove }); + const cardNode = cardEntityFactory.withParent(columnToMove).build(); - await em.persistAndFlush([user, cardNode, ...columnNodes, columnBoardNode]); + await em.persistAndFlush([cardNode, ...columnNodes, columnBoardNode]); em.clear(); - return { user, cardNode, columnToMove, columnBoardNode }; + const loggedInClient = await testApiClient.login(teacherAccount); + + return { loggedInClient, cardNode, columnToMove, columnBoardNode }; }; describe('with valid user', () => { it('should return status 204', async () => { - const { user, columnToMove, columnBoardNode } = await setup(); - currentUser = mapUserToCurrentUser(user); + const { loggedInClient, columnToMove, columnBoardNode } = await setup(); - const response = await api.move(columnToMove.id, columnBoardNode.id, 5); + const params: MoveColumnBodyParams = { + toBoardId: columnBoardNode.id, + toPosition: 5, + }; + + const response = await loggedInClient.put(`${columnToMove.id}/position`, params); expect(response.status).toEqual(204); }); it('should actually move the column', async () => { - const { user, columnToMove, columnBoardNode } = await setup(); - currentUser = mapUserToCurrentUser(user); + const { loggedInClient, columnToMove, columnBoardNode } = await setup(); + + const params: MoveColumnBodyParams = { + toBoardId: columnBoardNode.id, + toPosition: 5, + }; - await api.move(columnToMove.id, columnBoardNode.id, 5); - const result = await em.findOneOrFail(ColumnNode, columnToMove.id); + await loggedInClient.put(`${columnToMove.id}/position`, params); + const result = await em.findOneOrFail(BoardNodeEntity, columnToMove.id); expect(result.position).toEqual(5); }); }); describe('with invalid user', () => { + const setupNoAccess = async () => { + const vars = await setup(); + + const { studentAccount: noAccessAccount, studentUser: noAccessUser } = UserAndAccountTestFactory.buildStudent(); + await em.persistAndFlush([noAccessAccount, noAccessUser]); + const loggedInClient = await testApiClient.login(noAccessAccount); + + return { + ...vars, + loggedInClient, + }; + }; + it('should return status 403', async () => { - const { columnToMove, columnBoardNode } = await setup(); + const { loggedInClient, columnToMove, columnBoardNode } = await setupNoAccess(); - const invalidUser = userFactory.build(); - await em.persistAndFlush([invalidUser]); - currentUser = mapUserToCurrentUser(invalidUser); + const params: MoveColumnBodyParams = { + toBoardId: columnBoardNode.id, + toPosition: 5, + }; - const response = await api.move(columnToMove.id, columnBoardNode.id, 5); + const response = await loggedInClient.put(`${columnToMove.id}/position`, params); expect(response.status).toEqual(403); }); diff --git a/apps/server/src/modules/board/controller/api-test/column-update-title.api.spec.ts b/apps/server/src/modules/board/controller/api-test/column-update-title.api.spec.ts index f7e37510e4b..806ba48bcd3 100644 --- a/apps/server/src/modules/board/controller/api-test/column-update-title.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/column-update-title.api.spec.ts @@ -2,16 +2,10 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { ServerTestModule } from '@modules/server/server.module'; import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; -import { ColumnNode } from '@shared/domain/entity'; -import { - TestApiClient, - UserAndAccountTestFactory, - cleanupCollections, - columnBoardNodeFactory, - columnNodeFactory, - courseFactory, -} from '@shared/testing'; +import { TestApiClient, UserAndAccountTestFactory, cleanupCollections, courseFactory } from '@shared/testing'; +import { BoardNodeEntity } from '../../repo'; +import { columnBoardEntityFactory, columnEntityFactory } from '../../testing/entity'; +import { BoardExternalReferenceType } from '../../domain'; const baseRouteName = '/columns'; @@ -48,10 +42,10 @@ describe(`column update title (api)`, () => { const course = courseFactory.build({ teachers: [teacherUser] }); await em.persistAndFlush([teacherUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); await em.persistAndFlush([teacherAccount, teacherUser, columnNode, columnBoardNode]); em.clear(); @@ -77,7 +71,7 @@ describe(`column update title (api)`, () => { await loggedInClient.patch(`${columnNode.id}/title`, { title: newTitle }); - const result = await em.findOneOrFail(ColumnNode, columnNode.id); + const result = await em.findOneOrFail(BoardNodeEntity, columnNode.id); expect(result.title).toEqual(newTitle); }); @@ -89,7 +83,7 @@ describe(`column update title (api)`, () => { const sanitizedTitle = 'foo bar'; await loggedInClient.patch(`${columnNode.id}/title`, { title: unsanitizedTitle }); - const result = await em.findOneOrFail(ColumnNode, columnNode.id); + const result = await em.findOneOrFail(BoardNodeEntity, columnNode.id); expect(result.title).toEqual(sanitizedTitle); }); @@ -104,11 +98,11 @@ describe(`column update title (api)`, () => { const course = courseFactory.build({ students: [studentUser] }); await em.persistAndFlush([studentUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); const title = 'old title'; - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode, title }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build({ title }); await em.persistAndFlush([studentAccount, studentUser, columnNode, columnBoardNode]); em.clear(); @@ -127,7 +121,7 @@ describe(`column update title (api)`, () => { expect(response.statusCode).toEqual(403); - const result = await em.findOneOrFail(ColumnNode, columnNode.id); + const result = await em.findOneOrFail(BoardNodeEntity, columnNode.id); expect(result.title).toEqual(title); }); }); diff --git a/apps/server/src/modules/board/controller/api-test/content-element-create.api.spec.ts b/apps/server/src/modules/board/controller/api-test/content-element-create.api.spec.ts index 7fdbaf734e0..b7a2f655d25 100644 --- a/apps/server/src/modules/board/controller/api-test/content-element-create.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/content-element-create.api.spec.ts @@ -2,19 +2,16 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { ServerTestModule } from '@modules/server/server.module'; import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardExternalReferenceType, ContentElementType } from '@shared/domain/domainobject'; -import { DrawingElementNode, RichTextElementNode } from '@shared/domain/entity'; +import { TestApiClient, UserAndAccountTestFactory, cleanupCollections, courseFactory } from '@shared/testing'; +import { BoardNodeEntity } from '../../repo'; +import { BoardExternalReferenceType, ContentElementType } from '../../domain'; import { - TestApiClient, - UserAndAccountTestFactory, - cardNodeFactory, - cleanupCollections, - columnBoardNodeFactory, - columnNodeFactory, - courseFactory, - submissionContainerElementNodeFactory, - submissionItemNodeFactory, -} from '@shared/testing'; + cardEntityFactory, + columnBoardEntityFactory, + columnEntityFactory, + submissionContainerElementEntityFactory, + submissionItemEntityFactory, +} from '../../testing'; import { AnyContentElementResponse, SubmissionContainerElementResponse } from '../dto'; const baseRouteName = '/cards'; @@ -52,11 +49,11 @@ describe(`content element create (api)`, () => { const course = courseFactory.build({ teachers: [teacherUser] }); await em.persistAndFlush([teacherAccount, teacherUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); + const cardNode = cardEntityFactory.withParent(columnNode).build(); await em.persistAndFlush([columnBoardNode, columnNode, cardNode]); em.clear(); @@ -127,7 +124,7 @@ describe(`content element create (api)`, () => { const elementId = (response.body as AnyContentElementResponse).id; - const result = await em.findOneOrFail(RichTextElementNode, elementId); + const result = await em.findOneOrFail(BoardNodeEntity, elementId); expect(result.id).toEqual(elementId); }); @@ -167,7 +164,7 @@ describe(`content element create (api)`, () => { const elementId = (response.body as AnyContentElementResponse).id; - const result = await em.findOneOrFail(DrawingElementNode, elementId); + const result = await em.findOneOrFail(BoardNodeEntity, elementId); expect(result.id).toEqual(elementId); }); }); @@ -180,11 +177,11 @@ describe(`content element create (api)`, () => { const course = courseFactory.build({}); await em.persistAndFlush([teacherAccount, teacherUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); + const cardNode = cardEntityFactory.withParent(columnNode).build(); await em.persistAndFlush([columnBoardNode, columnNode, cardNode]); em.clear(); @@ -212,11 +209,11 @@ describe(`content element create (api)`, () => { const course = courseFactory.build({ students: [studentUser] }); await em.persistAndFlush([studentAccount, studentUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); + const cardNode = cardEntityFactory.withParent(columnNode).build(); await em.persistAndFlush([columnBoardNode, columnNode, cardNode]); em.clear(); @@ -263,16 +260,13 @@ describe(`content element create (api)`, () => { await em.persistAndFlush([teacherAccount, teacherUser, studentAccount, studentUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); - const submissionElementContainerNode = submissionContainerElementNodeFactory.buildWithId({ - parent: cardNode, - }); - const submissionItemNode = submissionItemNodeFactory.buildWithId({ - parent: submissionElementContainerNode, + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); + const cardNode = cardEntityFactory.withParent(columnNode).build(); + const submissionElementContainerNode = submissionContainerElementEntityFactory.withParent(cardNode).build(); + const submissionItemNode = submissionItemEntityFactory.withParent(submissionElementContainerNode).build({ userId: studentUser.id, }); @@ -338,7 +332,7 @@ describe(`content element create (api)`, () => { const elementId = (response.body as AnyContentElementResponse).id; - const result = await em.findOneOrFail(RichTextElementNode, elementId); + const result = await em.findOneOrFail(BoardNodeEntity, elementId); expect(result.id).toEqual(elementId); }); }); @@ -353,16 +347,13 @@ describe(`content element create (api)`, () => { await em.persistAndFlush([teacherAccount, teacherUser, studentAccount, studentUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); - const submissionElementContainerNode = submissionContainerElementNodeFactory.buildWithId({ - parent: cardNode, - }); - const submissionItemNode = submissionItemNodeFactory.buildWithId({ - parent: submissionElementContainerNode, + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); + const cardNode = cardEntityFactory.withParent(columnNode).build(); + const submissionElementContainerNode = submissionContainerElementEntityFactory.withParent(cardNode).build(); + const submissionItemNode = submissionItemEntityFactory.withParent(submissionElementContainerNode).build({ userId: teacherUser.id, }); @@ -399,18 +390,15 @@ describe(`content element create (api)`, () => { await em.persistAndFlush([teacherAccount, teacherUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); - const submissionElementContainerNode = submissionContainerElementNodeFactory.buildWithId({ - parent: cardNode, - }); - const submissionItemNode = submissionItemNodeFactory.buildWithId({ - parent: submissionElementContainerNode, - userId: teacherUser.id, - }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); + const cardNode = cardEntityFactory.withParent(columnNode).build(); + const submissionElementContainerNode = submissionContainerElementEntityFactory.withParent(cardNode).build(); + const submissionItemNode = submissionItemEntityFactory + .withParent(submissionElementContainerNode) + .build({ userId: teacherUser.id }); await em.persistAndFlush([ columnBoardNode, @@ -446,16 +434,13 @@ describe(`content element create (api)`, () => { await em.persistAndFlush([teacherAccount, teacherUser, studentAccount, studentUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); - const submissionElementContainerNode = submissionContainerElementNodeFactory.buildWithId({ - parent: cardNode, - }); - const submissionItemNode = submissionItemNodeFactory.buildWithId({ - parent: submissionElementContainerNode, + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); + const cardNode = cardEntityFactory.withParent(columnNode).build(); + const submissionElementContainerNode = submissionContainerElementEntityFactory.withParent(cardNode).build(); + const submissionItemNode = submissionItemEntityFactory.withParent(submissionElementContainerNode).build({ userId: studentUser.id, }); diff --git a/apps/server/src/modules/board/controller/api-test/content-element-delete.api.spec.ts b/apps/server/src/modules/board/controller/api-test/content-element-delete.api.spec.ts index e3264a45a15..12921eff7d2 100644 --- a/apps/server/src/modules/board/controller/api-test/content-element-delete.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/content-element-delete.api.spec.ts @@ -5,24 +5,21 @@ import { ServerTestModule } from '@modules/server/server.module'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; -import { DrawingElementNode, RichTextElementNode } from '@shared/domain/entity'; -import { - cardNodeFactory, - cleanupCollections, - columnBoardNodeFactory, - columnNodeFactory, - courseFactory, - mapUserToCurrentUser, - richTextElementNodeFactory, - userFactory, -} from '@shared/testing'; +import { cleanupCollections, courseFactory, mapUserToCurrentUser, userFactory } from '@shared/testing'; import { Request } from 'express'; import request from 'supertest'; -import { drawingElementNodeFactory } from '@shared/testing/factory/boardnode/drawing-element-node.factory'; import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; import { DrawingElementAdapterService } from '@modules/tldraw-client'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { BoardNodeEntity } from '../../repo'; +import { + cardEntityFactory, + columnBoardEntityFactory, + columnEntityFactory, + drawingElementEntityFactory, + richTextElementEntityFactory, +} from '../../testing'; +import { BoardExternalReferenceType } from '../../domain'; const baseRouteName = '/elements'; @@ -89,13 +86,13 @@ describe(`content element delete (api)`, () => { const course = courseFactory.build({ teachers: [user] }); await em.persistAndFlush([user, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); - const element = richTextElementNodeFactory.buildWithId({ parent: cardNode }); - const sibling = richTextElementNodeFactory.buildWithId({ parent: cardNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); + const cardNode = cardEntityFactory.withParent(columnNode).build(); + const element = richTextElementEntityFactory.withParent(cardNode).build(); + const sibling = richTextElementEntityFactory.withParent(cardNode).build(); await em.persistAndFlush([user, columnBoardNode, columnNode, cardNode, element, sibling]); em.clear(); @@ -119,7 +116,7 @@ describe(`content element delete (api)`, () => { await api.delete(element.id); - await expect(em.findOneOrFail(RichTextElementNode, element.id)).rejects.toThrow(); + await expect(em.findOneOrFail(BoardNodeEntity, element.id)).rejects.toThrow(); }); it('should not delete siblings', async () => { @@ -128,7 +125,7 @@ describe(`content element delete (api)`, () => { await api.delete(element.id); - const siblingFromDb = await em.findOneOrFail(RichTextElementNode, sibling.id); + const siblingFromDb = await em.findOneOrFail(BoardNodeEntity, sibling.id); expect(siblingFromDb).toBeDefined(); }); }); @@ -155,12 +152,12 @@ describe(`content element delete (api)`, () => { const course = courseFactory.build({ teachers: [teacher], students: [student] }); await em.persistAndFlush([teacher, student, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); - const element = drawingElementNodeFactory.buildWithId({ parent: cardNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); + const cardNode = cardEntityFactory.withParent(columnNode).build(); + const element = drawingElementEntityFactory.withParent(cardNode).build(); filesStorageClientAdapterService.deleteFilesOfParent.mockResolvedValueOnce([]); drawingElementAdapterService.deleteDrawingBinData.mockResolvedValueOnce(); @@ -187,7 +184,7 @@ describe(`content element delete (api)`, () => { await api.delete(element.id); - await expect(em.findOneOrFail(DrawingElementNode, element.id)).rejects.toThrow(); + await expect(em.findOneOrFail(BoardNodeEntity, element.id)).rejects.toThrow(); }); }); diff --git a/apps/server/src/modules/board/controller/api-test/content-element-move.api.spec.ts b/apps/server/src/modules/board/controller/api-test/content-element-move.api.spec.ts index def2d498698..e11e0d309ab 100644 --- a/apps/server/src/modules/board/controller/api-test/content-element-move.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/content-element-move.api.spec.ts @@ -1,71 +1,38 @@ import { EntityManager } from '@mikro-orm/mongodb'; -import { ICurrentUser } from '@modules/authentication'; -import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; import { ServerTestModule } from '@modules/server/server.module'; -import { ExecutionContext, INestApplication } from '@nestjs/common'; +import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { ApiValidationError } from '@shared/common'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; -import { RichTextElementNode } from '@shared/domain/entity'; +import { cleanupCollections, courseFactory, TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; +import { BoardExternalReferenceType, pathOfChildren } from '../../domain'; +import { BoardNodeEntity } from '../../repo'; import { - cardNodeFactory, - cleanupCollections, - columnBoardNodeFactory, - columnNodeFactory, - courseFactory, - mapUserToCurrentUser, - richTextElementNodeFactory, - userFactory, -} from '@shared/testing'; -import { Request } from 'express'; -import request from 'supertest'; + cardEntityFactory, + columnBoardEntityFactory, + columnEntityFactory, + richTextElementEntityFactory, +} from '../../testing'; +import { MoveContentElementBody } from '../dto'; const baseRouteName = '/elements'; -class API { - app: INestApplication; - - constructor(app: INestApplication) { - this.app = app; - } - - async move(contentElementId: string, toCardId: string, toPosition: number) { - const response = await request(this.app.getHttpServer()) - .put(`${baseRouteName}/${contentElementId}/position`) - .set('Accept', 'application/json') - .send({ toCardId, toPosition }); - - return { - error: response.body as ApiValidationError, - status: response.status, - }; - } -} - describe(`content element move (api)`, () => { let app: INestApplication; let em: EntityManager; - let currentUser: ICurrentUser; - let api: API; + let testApiClient: TestApiClient; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [ServerTestModule], - }) - .overrideGuard(JwtAuthGuard) - .useValue({ - canActivate(context: ExecutionContext) { - const req: Request = context.switchToHttp().getRequest(); - req.user = currentUser; - return true; - }, - }) - .compile(); + }).compile(); app = module.createNestApplication(); await app.init(); em = module.get(EntityManager); - api = new API(app); + testApiClient = new TestApiClient(app, baseRouteName); + }); + + beforeEach(async () => { + await cleanupCollections(em); }); afterAll(async () => { @@ -73,57 +40,90 @@ describe(`content element move (api)`, () => { }); const setup = async () => { - await cleanupCollections(em); - const user = userFactory.build(); - const course = courseFactory.build({ teachers: [user] }); - await em.persistAndFlush([user, course]); + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); + + const course = courseFactory.build({ teachers: [teacherUser] }); + await em.persistAndFlush([teacherUser, teacherAccount, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const column = columnNodeFactory.buildWithId({ parent: columnBoardNode }); - const parentCard = cardNodeFactory.buildWithId({ parent: column }); - const targetCard = cardNodeFactory.buildWithId({ parent: column }); - const targetCardElements = richTextElementNodeFactory.buildListWithId(4, { parent: targetCard }); - const element = richTextElementNodeFactory.buildWithId({ parent: parentCard }); + const column = columnEntityFactory.withParent(columnBoardNode).build(); + const parentCard = cardEntityFactory.withParent(column).build(); + const targetCard = cardEntityFactory.withParent(column).build(); + const targetCardElements = richTextElementEntityFactory.withParent(targetCard).buildList(4); + const element = richTextElementEntityFactory.withParent(parentCard).build(); - await em.persistAndFlush([user, parentCard, column, targetCard, columnBoardNode, ...targetCardElements, element]); + await em.persistAndFlush([parentCard, column, targetCard, columnBoardNode, ...targetCardElements, element]); em.clear(); - return { user, parentCard, column, targetCard, columnBoardNode, targetCardElements, element }; + const loggedInClient = await testApiClient.login(teacherAccount); + + return { + loggedInClient, + parentCard, + column, + targetCard, + columnBoardNode, + targetCardElements, + element, + }; }; describe('with valid user', () => { it('should return status 204', async () => { - const { user, element, targetCard } = await setup(); - currentUser = mapUserToCurrentUser(user); + const { loggedInClient, element, targetCard } = await setup(); - const response = await api.move(element.id, targetCard.id, 4); + const params: MoveContentElementBody = { + toCardId: targetCard.id, + toPosition: 4, + }; + + const response = await loggedInClient.put(`${element.id}/position`, params); expect(response.status).toEqual(204); }); it('should actually move the element', async () => { - const { user, element, targetCard } = await setup(); - currentUser = mapUserToCurrentUser(user); + const { loggedInClient, element, targetCard } = await setup(); + + const params: MoveContentElementBody = { + toCardId: targetCard.id, + toPosition: 2, + }; + + await loggedInClient.put(`${element.id}/position`, params); - await api.move(element.id, targetCard.id, 2); - const result = await em.findOneOrFail(RichTextElementNode, element.id); + const result = await em.findOneOrFail(BoardNodeEntity, element.id); - expect(result.parentId).toEqual(targetCard.id); + expect(result.path).toEqual(pathOfChildren(targetCard)); expect(result.position).toEqual(2); }); }); - describe('with valid user', () => { + describe('when the user has no access to the board', () => { + const setupNoAccess = async () => { + const vars = await setup(); + + const { studentAccount: noAccessAccount, studentUser: noAccessUser } = UserAndAccountTestFactory.buildStudent(); + await em.persistAndFlush([noAccessAccount, noAccessUser]); + const loggedInClient = await testApiClient.login(noAccessAccount); + + return { + ...vars, + loggedInClient, + }; + }; + it('should return status 403', async () => { - const { element, targetCard } = await setup(); + const { loggedInClient, element, targetCard } = await setupNoAccess(); - const invalidUser = userFactory.build(); - await em.persistAndFlush([invalidUser]); - currentUser = mapUserToCurrentUser(invalidUser); + const params: MoveContentElementBody = { + toCardId: targetCard.id, + toPosition: 3, + }; - const response = await api.move(element.id, targetCard.id, 4); + const response = await loggedInClient.put(`${element.id}/position`, params); expect(response.status).toEqual(403); }); diff --git a/apps/server/src/modules/board/controller/api-test/content-element-update-content.spec.ts b/apps/server/src/modules/board/controller/api-test/content-element-update-content.spec.ts index b05b453cdf4..d16fb5f05c3 100644 --- a/apps/server/src/modules/board/controller/api-test/content-element-update-content.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/content-element-update-content.spec.ts @@ -3,21 +3,18 @@ import { ServerTestModule } from '@modules/server/server.module'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { sanitizeRichText } from '@shared/controller'; -import { BoardExternalReferenceType, ContentElementType } from '@shared/domain/domainobject'; -import { FileElementNode, RichTextElementNode, SubmissionContainerElementNode } from '@shared/domain/entity'; import { InputFormat } from '@shared/domain/types'; +import { cleanupCollections, courseFactory, TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; +import { BoardNodeEntity } from '../../repo'; +import { BoardExternalReferenceType, ContentElementType } from '../../domain'; import { - cardNodeFactory, - cleanupCollections, - columnBoardNodeFactory, - columnNodeFactory, - courseFactory, - fileElementNodeFactory, - richTextElementNodeFactory, - submissionContainerElementNodeFactory, - TestApiClient, - UserAndAccountTestFactory, -} from '@shared/testing'; + cardEntityFactory, + columnBoardEntityFactory, + columnEntityFactory, + fileElementEntityFactory, + richTextElementEntityFactory, + submissionContainerElementEntityFactory, +} from '../../testing'; describe(`content element update content (api)`, () => { let app: INestApplication; @@ -50,24 +47,24 @@ describe(`content element update content (api)`, () => { const course = courseFactory.build({ teachers: [teacherUser] }); await em.persistAndFlush([teacherUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const column = columnNodeFactory.buildWithId({ parent: columnBoardNode }); - const parentCard = cardNodeFactory.buildWithId({ parent: column }); - const richTextElement = richTextElementNodeFactory.buildWithId({ parent: parentCard }); - const fileElement = fileElementNodeFactory.buildWithId({ parent: parentCard }); - const submissionContainerElement = submissionContainerElementNodeFactory.buildWithId({ - parent: parentCard, - dueDate: null, + const column = columnEntityFactory.withParent(columnBoardNode).build(); + const parentCard = cardEntityFactory.withParent(column).build(); + const richTextElement = richTextElementEntityFactory.withParent(parentCard).build(); + const fileElement = fileElementEntityFactory.withParent(parentCard).build(); + const submissionContainerElement = submissionContainerElementEntityFactory.withParent(parentCard).build({ + dueDate: undefined, }); const tomorrow = new Date(Date.now() + 86400000); - const submissionContainerElementWithDueDate = submissionContainerElementNodeFactory.buildWithId({ - parent: parentCard, - dueDate: tomorrow, - }); + const submissionContainerElementWithDueDate = submissionContainerElementEntityFactory + .withParent(parentCard) + .build({ + dueDate: tomorrow, + }); await em.persistAndFlush([ teacherAccount, @@ -115,7 +112,7 @@ describe(`content element update content (api)`, () => { type: ContentElementType.RICH_TEXT, }, }); - const result = await em.findOneOrFail(RichTextElementNode, richTextElement.id); + const result = await em.findOneOrFail(BoardNodeEntity, richTextElement.id); expect(result.text).toEqual('hello world'); }); @@ -130,7 +127,7 @@ describe(`content element update content (api)`, () => { await loggedInClient.patch(`${richTextElement.id}/content`, { data: { content: { text, inputFormat: InputFormat.RICH_TEXT_CK5 }, type: ContentElementType.RICH_TEXT }, }); - const result = await em.findOneOrFail(RichTextElementNode, richTextElement.id); + const result = await em.findOneOrFail(BoardNodeEntity, richTextElement.id); expect(result.text).toEqual(sanitizedText); }); @@ -144,7 +141,7 @@ describe(`content element update content (api)`, () => { data: { content: { caption, alternativeText: '' }, type: ContentElementType.FILE }, }); - const result = await em.findOneOrFail(FileElementNode, fileElement.id); + const result = await em.findOneOrFail(BoardNodeEntity, fileElement.id); expect(result.caption).toEqual('rich text 1 some more text'); }); @@ -157,7 +154,7 @@ describe(`content element update content (api)`, () => { await loggedInClient.patch(`${fileElement.id}/content`, { data: { content: { caption: '', alternativeText }, type: ContentElementType.FILE }, }); - const result = await em.findOneOrFail(FileElementNode, fileElement.id); + const result = await em.findOneOrFail(BoardNodeEntity, fileElement.id); expect(result.alternativeText).toEqual('rich text 1 some more text'); }); @@ -183,8 +180,9 @@ describe(`content element update content (api)`, () => { type: 'submissionContainer', }, }); - const result = await em.findOneOrFail(SubmissionContainerElementNode, submissionContainerElement.id); - expect(result.dueDate).toBeNull(); + const result = await em.findOneOrFail(BoardNodeEntity, submissionContainerElement.id); + expect(result.id).toEqual(submissionContainerElement.id); + expect(result.dueDate).toBeUndefined(); }); it('should set dueDate value when provided for submission container element', async () => { @@ -198,7 +196,7 @@ describe(`content element update content (api)`, () => { type: 'submissionContainer', }, }); - const result = await em.findOneOrFail(SubmissionContainerElementNode, submissionContainerElement.id); + const result = await em.findOneOrFail(BoardNodeEntity, submissionContainerElement.id); expect(result.dueDate).toEqual(inThreeDays); }); @@ -214,9 +212,9 @@ describe(`content element update content (api)`, () => { type: 'submissionContainer', }, }); - const result = await em.findOneOrFail(SubmissionContainerElementNode, submissionContainerElementWithDueDate.id); - - expect(result.dueDate).toBeNull(); + const result = await em.findOneOrFail(BoardNodeEntity, submissionContainerElementWithDueDate.id); + expect(result.id).toEqual(submissionContainerElementWithDueDate.id); + expect(result.dueDate).toBeUndefined(); }); it('should return status 400 for wrong date format for submission container element', async () => { @@ -242,14 +240,14 @@ describe(`content element update content (api)`, () => { const course = courseFactory.build({ teachers: [] }); await em.persistAndFlush([invalidTeacherUser, invalidTeacherAccount, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const column = columnNodeFactory.buildWithId({ parent: columnBoardNode }); - const parentCard = cardNodeFactory.buildWithId({ parent: column }); - const richTextElement = richTextElementNodeFactory.buildWithId({ parent: parentCard }); - const submissionContainerElement = submissionContainerElementNodeFactory.buildWithId({ parent: parentCard }); + const column = columnEntityFactory.withParent(columnBoardNode).build(); + const parentCard = cardEntityFactory.withParent(column).build(); + const richTextElement = richTextElementEntityFactory.withParent(parentCard).build(); + const submissionContainerElement = submissionContainerElementEntityFactory.withParent(parentCard).build(); await em.persistAndFlush([parentCard, column, columnBoardNode, richTextElement, submissionContainerElement]); em.clear(); diff --git a/apps/server/src/modules/board/controller/api-test/drawing-item-check-permission.api.spec.ts b/apps/server/src/modules/board/controller/api-test/drawing-item-check-permission.api.spec.ts index 25d05911069..d3b8ec147f2 100644 --- a/apps/server/src/modules/board/controller/api-test/drawing-item-check-permission.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/drawing-item-check-permission.api.spec.ts @@ -2,17 +2,14 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { ServerTestModule } from '@modules/server'; import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; +import { TestApiClient, UserAndAccountTestFactory, cleanupCollections, courseFactory } from '@shared/testing'; import { - TestApiClient, - UserAndAccountTestFactory, - cardNodeFactory, - cleanupCollections, - columnBoardNodeFactory, - columnNodeFactory, - courseFactory, -} from '@shared/testing'; -import { drawingElementNodeFactory } from '@shared/testing/factory/boardnode/drawing-element-node.factory'; + cardEntityFactory, + columnEntityFactory, + columnBoardEntityFactory, + drawingElementEntityFactory, +} from '../../testing'; +import { BoardExternalReferenceType } from '../../domain'; const baseRouteName = '/elements'; describe('drawing permission check (api)', () => { @@ -43,15 +40,15 @@ describe('drawing permission check (api)', () => { const course = courseFactory.build({ teachers: [teacherUser] }); await em.persistAndFlush([teacherAccount, teacherUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); + const cardNode = cardEntityFactory.withParent(columnNode).build(); - const drawingItemNode = drawingElementNodeFactory.buildWithId({ parent: cardNode }); + const drawingItemNode = drawingElementEntityFactory.withParent(cardNode).build(); await em.persistAndFlush([columnBoardNode, columnNode, cardNode, drawingItemNode]); em.clear(); @@ -80,15 +77,15 @@ describe('drawing permission check (api)', () => { const course = courseFactory.build({ students: [teacherUser] }); await em.persistAndFlush([teacherAccount, teacherUser, course, studentAccount, studentUser]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); + const cardNode = cardEntityFactory.withParent(columnNode).build(); - const drawingItemNode = drawingElementNodeFactory.buildWithId({ parent: cardNode }); + const drawingItemNode = drawingElementEntityFactory.withParent(cardNode).build(); await em.persistAndFlush([columnBoardNode, columnNode, cardNode, drawingItemNode]); em.clear(); diff --git a/apps/server/src/modules/board/controller/api-test/submission-item-create.api.spec.ts b/apps/server/src/modules/board/controller/api-test/submission-item-create.api.spec.ts index 33b764e67a0..984b2ac42cf 100644 --- a/apps/server/src/modules/board/controller/api-test/submission-item-create.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/submission-item-create.api.spec.ts @@ -2,19 +2,21 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { ServerTestModule } from '@modules/server'; import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; -import { SubmissionItemNode } from '@shared/domain/entity'; import { TestApiClient, UserAndAccountTestFactory, - cardNodeFactory, cleanupCollections, - columnBoardNodeFactory, - columnNodeFactory, courseFactory, - submissionContainerElementNodeFactory, userFactory, } from '@shared/testing'; +import { BoardNodeEntity } from '../../repo'; +import { + cardEntityFactory, + columnBoardEntityFactory, + columnEntityFactory, + submissionContainerElementEntityFactory, +} from '../../testing'; +import { BoardExternalReferenceType } from '../../domain'; import { SubmissionItemResponse } from '../dto'; const baseRouteName = '/elements'; @@ -46,15 +48,15 @@ describe('submission create (api)', () => { const course = courseFactory.build({ teachers: [teacherUser] }); await em.persistAndFlush([teacherAccount, teacherUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); + const cardNode = cardEntityFactory.withParent(columnNode).build(); - const submissionContainerNode = submissionContainerElementNodeFactory.buildWithId({ parent: cardNode }); + const submissionContainerNode = submissionContainerElementEntityFactory.withParent(cardNode).build(); await em.persistAndFlush([columnBoardNode, columnNode, cardNode, submissionContainerNode]); em.clear(); @@ -80,15 +82,15 @@ describe('submission create (api)', () => { const course = courseFactory.build({ students: [studentUser] }); await em.persistAndFlush([studentAccount, studentUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); + const cardNode = cardEntityFactory.withParent(columnNode).build(); - const submissionContainerNode = submissionContainerElementNodeFactory.buildWithId({ parent: cardNode }); + const submissionContainerNode = submissionContainerElementEntityFactory.withParent(cardNode).build(); await em.persistAndFlush([columnBoardNode, columnNode, cardNode, submissionContainerNode]); em.clear(); @@ -124,7 +126,7 @@ describe('submission create (api)', () => { const submissionItemResponse = response.body as SubmissionItemResponse; - const result = await em.findOneOrFail(SubmissionItemNode, submissionItemResponse.id); + const result = await em.findOneOrFail(BoardNodeEntity, submissionItemResponse.id); expect(result.id).toEqual(submissionItemResponse.id); expect(result.completed).toEqual(true); }); @@ -155,15 +157,15 @@ describe('submission create (api)', () => { const course = courseFactory.build({ students: [] }); await em.persistAndFlush([studentAccount, studentUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); + const cardNode = cardEntityFactory.withParent(columnNode).build(); - const submissionContainerNode = submissionContainerElementNodeFactory.buildWithId({ parent: cardNode }); + const submissionContainerNode = submissionContainerElementEntityFactory.withParent(cardNode).build(); await em.persistAndFlush([columnBoardNode, columnNode, cardNode, submissionContainerNode]); em.clear(); @@ -193,15 +195,15 @@ describe('submission create (api)', () => { await em.persistAndFlush([user, teacherAccount, teacherUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); + const cardNode = cardEntityFactory.withParent(columnNode).build(); - const submissionContainerNode = submissionContainerElementNodeFactory.buildWithId({ parent: cardNode }); + const submissionContainerNode = submissionContainerElementEntityFactory.withParent(cardNode).build(); await em.persistAndFlush([columnBoardNode, columnNode, cardNode, submissionContainerNode]); em.clear(); diff --git a/apps/server/src/modules/board/controller/api-test/submission-item-delete.api.spec.ts b/apps/server/src/modules/board/controller/api-test/submission-item-delete.api.spec.ts index ed65c70cc06..39e8c019229 100644 --- a/apps/server/src/modules/board/controller/api-test/submission-item-delete.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/submission-item-delete.api.spec.ts @@ -1,20 +1,17 @@ -import { EntityManager } from '@mikro-orm/mongodb'; +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { ServerTestModule } from '@modules/server'; import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; -import { SubmissionItemNode } from '@shared/domain/entity'; +import { TestApiClient, UserAndAccountTestFactory, cleanupCollections, courseFactory } from '@shared/testing'; +import { BoardNodeEntity } from '../../repo'; import { - TestApiClient, - UserAndAccountTestFactory, - cardNodeFactory, - cleanupCollections, - columnBoardNodeFactory, - columnNodeFactory, - courseFactory, - submissionContainerElementNodeFactory, - submissionItemNodeFactory, -} from '@shared/testing'; + cardEntityFactory, + columnBoardEntityFactory, + columnEntityFactory, + submissionContainerElementEntityFactory, + submissionItemEntityFactory, +} from '../../testing'; +import { BoardExternalReferenceType } from '../../domain'; import { SubmissionItemResponse } from '../dto'; const baseRouteName = '/board-submissions'; @@ -46,18 +43,17 @@ describe('submission item delete (api)', () => { const course = courseFactory.build({ teachers: [teacherUser] }); await em.persistAndFlush([teacherAccount, teacherUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); + const cardNode = cardEntityFactory.withParent(columnNode).build(); - const submissionContainerNode = submissionContainerElementNodeFactory.buildWithId({ parent: cardNode }); - const submissionItemNode = submissionItemNodeFactory.buildWithId({ - userId: 'foo', - parent: submissionContainerNode, + const submissionContainerNode = submissionContainerElementEntityFactory.withParent(cardNode).build(); + const submissionItemNode = submissionItemEntityFactory.withParent(submissionContainerNode).build({ + userId: new ObjectId().toHexString(), completed: true, }); @@ -80,7 +76,7 @@ describe('submission item delete (api)', () => { await loggedInClient.delete(`${submissionItemNode.id}`); - const result = await em.findOneOrFail(SubmissionItemNode, submissionItemNode.id); + const result = await em.findOneOrFail(BoardNodeEntity, submissionItemNode.id); expect(result.completed).toEqual(submissionItemNode.completed); }); }); @@ -93,19 +89,18 @@ describe('submission item delete (api)', () => { const course = courseFactory.build({ students: [studentUser] }); await em.persistAndFlush([studentAccount, studentUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); + const cardNode = cardEntityFactory.withParent(columnNode).build(); - const submissionContainerNode = submissionContainerElementNodeFactory.buildWithId({ parent: cardNode }); + const submissionContainerNode = submissionContainerElementEntityFactory.withParent(cardNode).build(); - const submissionItemNode = submissionItemNodeFactory.buildWithId({ + const submissionItemNode = submissionItemEntityFactory.withParent(submissionContainerNode).build({ userId: studentUser.id, - parent: submissionContainerNode, completed: true, }); @@ -130,7 +125,7 @@ describe('submission item delete (api)', () => { const submissionItemResponse = response.body as SubmissionItemResponse; - await expect(em.findOneOrFail(SubmissionItemNode, submissionItemResponse.id)).rejects.toThrow(); + await expect(em.findOneOrFail(BoardNodeEntity, submissionItemResponse.id)).rejects.toThrow(); }); }); @@ -143,19 +138,18 @@ describe('submission item delete (api)', () => { const course = courseFactory.build({ students: [studentUser, studentUser2] }); await em.persistAndFlush([studentAccount, studentUser, studentAccount2, studentUser2, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); + const cardNode = cardEntityFactory.withParent(columnNode).build(); - const submissionContainerNode = submissionContainerElementNodeFactory.buildWithId({ parent: cardNode }); + const submissionContainerNode = submissionContainerElementEntityFactory.withParent(cardNode).build(); - const submissionItemNode = submissionItemNodeFactory.buildWithId({ + const submissionItemNode = submissionItemEntityFactory.withParent(submissionContainerNode).build({ userId: studentUser.id, - parent: submissionContainerNode, completed: true, }); await em.persistAndFlush([columnBoardNode, columnNode, cardNode, submissionContainerNode, submissionItemNode]); @@ -178,7 +172,7 @@ describe('submission item delete (api)', () => { await loggedInClient.delete(`${submissionItemNode.id}`); - const result = await em.findOneOrFail(SubmissionItemNode, submissionItemNode.id); + const result = await em.findOneOrFail(BoardNodeEntity, submissionItemNode.id); expect(result.completed).toEqual(submissionItemNode.completed); }); }); @@ -192,19 +186,18 @@ describe('submission item delete (api)', () => { const course = courseFactory.build({ students: [studentUser] }); await em.persistAndFlush([studentAccount, studentUser, studentAccount2, studentUser2, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); + const cardNode = cardEntityFactory.withParent(columnNode).build(); - const submissionContainerNode = submissionContainerElementNodeFactory.buildWithId({ parent: cardNode }); + const submissionContainerNode = submissionContainerElementEntityFactory.withParent(cardNode).build(); - const submissionItemNode = submissionItemNodeFactory.buildWithId({ + const submissionItemNode = submissionItemEntityFactory.withParent(submissionContainerNode).build({ userId: studentUser.id, - parent: submissionContainerNode, completed: true, }); await em.persistAndFlush([columnBoardNode, columnNode, cardNode, submissionContainerNode, submissionItemNode]); @@ -228,7 +221,7 @@ describe('submission item delete (api)', () => { await loggedInClient.delete(`${submissionItemNode.id}`); - const result = await em.findOneOrFail(SubmissionItemNode, submissionItemNode.id); + const result = await em.findOneOrFail(BoardNodeEntity, submissionItemNode.id); expect(result.completed).toEqual(submissionItemNode.completed); }); }); diff --git a/apps/server/src/modules/board/controller/api-test/submission-item-lookup.api.spec.ts b/apps/server/src/modules/board/controller/api-test/submission-item-lookup.api.spec.ts index 727fbdf4cba..796048d8aef 100644 --- a/apps/server/src/modules/board/controller/api-test/submission-item-lookup.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/submission-item-lookup.api.spec.ts @@ -2,21 +2,23 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { ServerTestModule } from '@modules/server'; import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardExternalReferenceType, ContentElementType } from '@shared/domain/domainobject'; import { TestApiClient, UserAndAccountTestFactory, - cardNodeFactory, cleanupCollections, - columnBoardNodeFactory, - columnNodeFactory, courseFactory, - fileElementNodeFactory, - richTextElementNodeFactory, - submissionContainerElementNodeFactory, - submissionItemNodeFactory, userFactory, } from '@shared/testing'; +import { BoardExternalReferenceType, ContentElementType } from '../../domain'; +import { + cardEntityFactory, + columnBoardEntityFactory, + columnEntityFactory, + fileElementEntityFactory, + richTextElementEntityFactory, + submissionContainerElementEntityFactory, + submissionItemEntityFactory, +} from '../../testing'; import { SubmissionsResponse } from '../dto'; const baseRouteName = '/board-submissions'; @@ -58,32 +60,28 @@ describe('submission item lookup (api)', () => { course, ]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); + const cardNode = cardEntityFactory.withParent(columnNode).build(); - const submissionContainerNode1 = submissionContainerElementNodeFactory.buildWithId({ parent: cardNode }); - const submissionContainerNode2 = submissionContainerElementNodeFactory.buildWithId({ parent: cardNode }); - const item11 = submissionItemNodeFactory.buildWithId({ - parent: submissionContainerNode1, + const submissionContainerNode1 = submissionContainerElementEntityFactory.withParent(cardNode).build(); + const submissionContainerNode2 = submissionContainerElementEntityFactory.withParent(cardNode).build(); + const item11 = submissionItemEntityFactory.withParent(submissionContainerNode1).build({ userId: studentUser1.id, }); - const item12 = submissionItemNodeFactory.buildWithId({ - parent: submissionContainerNode1, + const item12 = submissionItemEntityFactory.withParent(submissionContainerNode1).build({ userId: studentUser2.id, }); - const item21 = submissionItemNodeFactory.buildWithId({ - parent: submissionContainerNode2, + const item21 = submissionItemEntityFactory.withParent(submissionContainerNode2).build({ userId: studentUser1.id, }); - const item22 = submissionItemNodeFactory.buildWithId({ - parent: submissionContainerNode2, - userId: studentUser2.id, - }); + const item22 = submissionItemEntityFactory + .withParent(submissionContainerNode2) + .build({ userId: studentUser2.id }); await em.persistAndFlush([ columnBoardNode, @@ -168,23 +166,17 @@ describe('submission item lookup (api)', () => { const course = courseFactory.build({ teachers: [], students: [studentUser1, studentUser2] }); await em.persistAndFlush([studentAccount1, studentUser1, studentAccount2, studentUser2, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); + const cardNode = cardEntityFactory.withParent(columnNode).build(); - const submissionContainerNode = submissionContainerElementNodeFactory.buildWithId({ parent: cardNode }); - const item1 = submissionItemNodeFactory.buildWithId({ - parent: submissionContainerNode, - userId: studentUser1.id, - }); - const item2 = submissionItemNodeFactory.buildWithId({ - parent: submissionContainerNode, - userId: studentUser2.id, - }); + const submissionContainerNode = submissionContainerElementEntityFactory.withParent(cardNode).build(); + const item1 = submissionItemEntityFactory.withParent(submissionContainerNode).build({ userId: studentUser1.id }); + const item2 = submissionItemEntityFactory.withParent(submissionContainerNode).build({ userId: studentUser2.id }); await em.persistAndFlush([columnBoardNode, columnNode, cardNode, submissionContainerNode, item1, item2]); em.clear(); @@ -230,15 +222,15 @@ describe('submission item lookup (api)', () => { await em.persistAndFlush([user, teacherAccount, teacherUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); + const cardNode = cardEntityFactory.withParent(columnNode).build(); - const submissionContainerNode = submissionContainerElementNodeFactory.buildWithId({ parent: cardNode }); + const submissionContainerNode = submissionContainerElementEntityFactory.withParent(cardNode).build(); await em.persistAndFlush([columnBoardNode, columnNode, cardNode, submissionContainerNode]); em.clear(); @@ -270,20 +262,19 @@ describe('submission item lookup (api)', () => { const course = courseFactory.build({ teachers: [teacherUser], students: [studentUser] }); await em.persistAndFlush([studentAccount, studentUser, teacherAccount, teacherUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); + const cardNode = cardEntityFactory.withParent(columnNode).build(); - const submissionContainer = submissionContainerElementNodeFactory.buildWithId({ parent: cardNode }); - const submissionItem = submissionItemNodeFactory.buildWithId({ - parent: submissionContainer, - userId: studentUser.id, - }); - const richTextElement = richTextElementNodeFactory.buildWithId({ parent: submissionItem }); + const submissionContainer = submissionContainerElementEntityFactory.withParent(cardNode).build(); + const submissionItem = submissionItemEntityFactory + .withParent(submissionContainer) + .build({ userId: studentUser.id }); + const richTextElement = richTextElementEntityFactory.withParent(submissionItem).build(); await em.persistAndFlush([ columnBoardNode, @@ -326,20 +317,19 @@ describe('submission item lookup (api)', () => { const course = courseFactory.build({ teachers: [teacherUser], students: [studentUser] }); await em.persistAndFlush([studentAccount, studentUser, teacherAccount, teacherUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); + const cardNode = cardEntityFactory.withParent(columnNode).build(); - const submissionContainer = submissionContainerElementNodeFactory.buildWithId({ parent: cardNode }); - const submissionItem = submissionItemNodeFactory.buildWithId({ - parent: submissionContainer, - userId: studentUser.id, - }); - const fileElement = fileElementNodeFactory.buildWithId({ parent: submissionItem }); + const submissionContainer = submissionContainerElementEntityFactory.withParent(cardNode).build(); + const submissionItem = submissionItemEntityFactory + .withParent(submissionContainer) + .build({ userId: studentUser.id }); + const fileElement = fileElementEntityFactory.withParent(submissionItem).build(); await em.persistAndFlush([ columnBoardNode, diff --git a/apps/server/src/modules/board/controller/api-test/submission-item-update.api.spec.ts b/apps/server/src/modules/board/controller/api-test/submission-item-update.api.spec.ts index 698804a9925..c134feefe0b 100644 --- a/apps/server/src/modules/board/controller/api-test/submission-item-update.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/submission-item-update.api.spec.ts @@ -1,21 +1,17 @@ -import { EntityManager } from '@mikro-orm/mongodb'; +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { ServerTestModule } from '@modules/server'; import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; -import { SubmissionItemNode } from '@shared/domain/entity'; +import { TestApiClient, UserAndAccountTestFactory, cleanupCollections, courseFactory } from '@shared/testing'; +import { BoardNodeEntity } from '../../repo'; import { - TestApiClient, - UserAndAccountTestFactory, - cardNodeFactory, - cleanupCollections, - columnBoardNodeFactory, - columnNodeFactory, - courseFactory, - submissionContainerElementNodeFactory, - submissionItemNodeFactory, -} from '@shared/testing'; -import { SubmissionItemResponse } from '../dto'; + cardEntityFactory, + columnBoardEntityFactory, + columnEntityFactory, + submissionContainerElementEntityFactory, + submissionItemEntityFactory, +} from '../../testing'; +import { BoardExternalReferenceType } from '../../domain'; const baseRouteName = '/board-submissions'; describe('submission item update (api)', () => { @@ -46,18 +42,17 @@ describe('submission item update (api)', () => { const course = courseFactory.build({ teachers: [teacherUser] }); await em.persistAndFlush([teacherAccount, teacherUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); + const cardNode = cardEntityFactory.withParent(columnNode).build(); - const submissionContainerNode = submissionContainerElementNodeFactory.buildWithId({ parent: cardNode }); - const submissionItemNode = submissionItemNodeFactory.buildWithId({ - userId: 'foo', - parent: submissionContainerNode, + const submissionContainerNode = submissionContainerElementEntityFactory.withParent(cardNode).build(); + const submissionItemNode = submissionItemEntityFactory.withParent(submissionContainerNode).build({ + userId: new ObjectId().toHexString(), completed: true, }); @@ -80,7 +75,7 @@ describe('submission item update (api)', () => { await loggedInClient.patch(`${submissionItemNode.id}`, { completed: false }); - const result = await em.findOneOrFail(SubmissionItemNode, submissionItemNode.id); + const result = await em.findOneOrFail(BoardNodeEntity, submissionItemNode.id); expect(result.completed).toEqual(submissionItemNode.completed); }); }); @@ -93,19 +88,18 @@ describe('submission item update (api)', () => { const course = courseFactory.build({ students: [studentUser] }); await em.persistAndFlush([studentAccount, studentUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); + const cardNode = cardEntityFactory.withParent(columnNode).build(); - const submissionContainerNode = submissionContainerElementNodeFactory.buildWithId({ parent: cardNode }); + const submissionContainerNode = submissionContainerElementEntityFactory.withParent(cardNode).build(); - const submissionItemNode = submissionItemNodeFactory.buildWithId({ + const submissionItemNode = submissionItemEntityFactory.withParent(submissionContainerNode).build({ userId: studentUser.id, - parent: submissionContainerNode, completed: true, }); @@ -126,11 +120,9 @@ describe('submission item update (api)', () => { it('should actually update the submission item', async () => { const { loggedInClient, submissionItemNode } = await setup(); - const response = await loggedInClient.patch(`${submissionItemNode.id}`, { completed: false }); - - const submissionItemResponse = response.body as SubmissionItemResponse; + await loggedInClient.patch(`${submissionItemNode.id}`, { completed: false }); - const result = await em.findOneOrFail(SubmissionItemNode, submissionItemResponse.id); + const result = await em.findOneOrFail(BoardNodeEntity, submissionItemNode.id); expect(result.id).toEqual(submissionItemNode.id); expect(result.completed).toEqual(false); }); @@ -152,19 +144,18 @@ describe('submission item update (api)', () => { const course = courseFactory.build({ students: [studentUser, studentUser2] }); await em.persistAndFlush([studentAccount, studentUser, studentAccount2, studentUser2, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); + const cardNode = cardEntityFactory.withParent(columnNode).build(); - const submissionContainerNode = submissionContainerElementNodeFactory.buildWithId({ parent: cardNode }); + const submissionContainerNode = submissionContainerElementEntityFactory.withParent(cardNode).build(); - const submissionItemNode = submissionItemNodeFactory.buildWithId({ + const submissionItemNode = submissionItemEntityFactory.withParent(submissionContainerNode).build({ userId: studentUser.id, - parent: submissionContainerNode, completed: true, }); await em.persistAndFlush([columnBoardNode, columnNode, cardNode, submissionContainerNode, submissionItemNode]); @@ -187,7 +178,7 @@ describe('submission item update (api)', () => { await loggedInClient.patch(`${submissionItemNode.id}`, { completed: false }); - const result = await em.findOneOrFail(SubmissionItemNode, submissionItemNode.id); + const result = await em.findOneOrFail(BoardNodeEntity, submissionItemNode.id); expect(result.completed).toEqual(submissionItemNode.completed); }); }); @@ -201,19 +192,18 @@ describe('submission item update (api)', () => { const course = courseFactory.build({ students: [studentUser] }); await em.persistAndFlush([studentAccount, studentUser, studentAccount2, studentUser2, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); + const cardNode = cardEntityFactory.withParent(columnNode).build(); - const submissionContainerNode = submissionContainerElementNodeFactory.buildWithId({ parent: cardNode }); + const submissionContainerNode = submissionContainerElementEntityFactory.withParent(cardNode).build(); - const submissionItemNode = submissionItemNodeFactory.buildWithId({ + const submissionItemNode = submissionItemEntityFactory.withParent(submissionContainerNode).build({ userId: studentUser.id, - parent: submissionContainerNode, completed: true, }); await em.persistAndFlush([columnBoardNode, columnNode, cardNode, submissionContainerNode, submissionItemNode]); @@ -237,7 +227,7 @@ describe('submission item update (api)', () => { await loggedInClient.patch(`${submissionItemNode.id}`, { completed: false }); - const result = await em.findOneOrFail(SubmissionItemNode, submissionItemNode.id); + const result = await em.findOneOrFail(BoardNodeEntity, submissionItemNode.id); expect(result.completed).toEqual(submissionItemNode.completed); }); }); diff --git a/apps/server/src/modules/board/controller/board.controller.ts b/apps/server/src/modules/board/controller/board.controller.ts index c4cfd078a4b..a341e387e50 100644 --- a/apps/server/src/modules/board/controller/board.controller.ts +++ b/apps/server/src/modules/board/controller/board.controller.ts @@ -13,7 +13,7 @@ import { } from '@nestjs/common'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ApiValidationError, RequestTimeout } from '@shared/common'; -import { CopyApiResponse, CopyMapper } from '@src/modules/copy-helper'; +import { CopyApiResponse, CopyMapper } from '@modules/copy-helper'; import { BoardUc } from '../uc'; import { BoardResponse, diff --git a/apps/server/src/modules/board/controller/dto/board/board-context.reponse.ts b/apps/server/src/modules/board/controller/dto/board/board-context.reponse.ts index c78a7e4c1fa..98de17b6e1e 100644 --- a/apps/server/src/modules/board/controller/dto/board/board-context.reponse.ts +++ b/apps/server/src/modules/board/controller/dto/board/board-context.reponse.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; +import { BoardExternalReferenceType } from '../../../domain'; export class BoardContextResponse { constructor({ id, type }: BoardContextResponse) { diff --git a/apps/server/src/modules/board/controller/dto/board/board.response.ts b/apps/server/src/modules/board/controller/dto/board/board.response.ts index c8cb5128ed0..081dd5dea59 100644 --- a/apps/server/src/modules/board/controller/dto/board/board.response.ts +++ b/apps/server/src/modules/board/controller/dto/board/board.response.ts @@ -1,8 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; import { DecodeHtmlEntities } from '@shared/controller'; -import { BoardLayout } from '@shared/domain/domainobject'; -import { ColumnResponse } from './column.response'; +import { BoardLayout } from '../../../domain'; import { TimestampsResponse } from '../timestamps.response'; +import { ColumnResponse } from './column.response'; export class BoardResponse { constructor({ id, title, columns, timestamps, isVisible, layout }: BoardResponse) { diff --git a/apps/server/src/modules/board/controller/dto/board/column.response.ts b/apps/server/src/modules/board/controller/dto/board/column.response.ts index 9ef7bef7db0..56938cc62aa 100644 --- a/apps/server/src/modules/board/controller/dto/board/column.response.ts +++ b/apps/server/src/modules/board/controller/dto/board/column.response.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { DecodeHtmlEntities } from '@shared/controller'; -import { CardSkeletonResponse } from './card-skeleton.response'; import { TimestampsResponse } from '../timestamps.response'; +import { CardSkeletonResponse } from './card-skeleton.response'; export class ColumnResponse { constructor({ id, title, cards, timestamps }: ColumnResponse) { @@ -18,7 +18,7 @@ export class ColumnResponse { @ApiProperty() @DecodeHtmlEntities() - title?: string; + title: string; @ApiProperty({ type: [CardSkeletonResponse], diff --git a/apps/server/src/modules/board/controller/dto/board/create-board.body.params.ts b/apps/server/src/modules/board/controller/dto/board/create-board.body.params.ts index aa9f01924d5..1c48e19cb17 100644 --- a/apps/server/src/modules/board/controller/dto/board/create-board.body.params.ts +++ b/apps/server/src/modules/board/controller/dto/board/create-board.body.params.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { SanitizeHtml } from '@shared/controller'; -import { BoardExternalReferenceType, BoardLayout } from '@shared/domain/domainobject'; -import { IsEnum, IsMongoId, MaxLength, MinLength } from 'class-validator'; +import { IsEnum, IsMongoId, NotEquals, MaxLength, MinLength } from 'class-validator'; +import { BoardExternalReferenceType, BoardLayout } from '../../../domain'; export class CreateBoardBodyParams { @ApiProperty({ @@ -36,5 +36,6 @@ export class CreateBoardBodyParams { enumName: 'BoardLayout', }) @IsEnum(BoardLayout, {}) + @NotEquals(BoardLayout[BoardLayout.GRID]) layout!: BoardLayout; } diff --git a/apps/server/src/modules/board/controller/dto/card/create-card.body.params.ts b/apps/server/src/modules/board/controller/dto/card/create-card.body.params.ts index c00e8dd9704..4844930aac5 100644 --- a/apps/server/src/modules/board/controller/dto/card/create-card.body.params.ts +++ b/apps/server/src/modules/board/controller/dto/card/create-card.body.params.ts @@ -1,6 +1,6 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; -import { ContentElementType } from '@shared/domain/domainobject'; import { IsEnum, IsOptional } from 'class-validator'; +import { ContentElementType } from '../../../domain'; export class CreateCardBodyParams { @IsEnum(ContentElementType, { each: true }) diff --git a/apps/server/src/modules/board/controller/dto/element/collaborative-text-editor-element.response.ts b/apps/server/src/modules/board/controller/dto/element/collaborative-text-editor-element.response.ts index 085ab02c082..fe0c6ece1c6 100644 --- a/apps/server/src/modules/board/controller/dto/element/collaborative-text-editor-element.response.ts +++ b/apps/server/src/modules/board/controller/dto/element/collaborative-text-editor-element.response.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { ContentElementType } from '@shared/domain/domainobject'; +import { ContentElementType } from '../../../domain'; import { TimestampsResponse } from '../timestamps.response'; export class CollaborativeTextEditorElementResponse { diff --git a/apps/server/src/modules/board/controller/dto/element/create-content-element.body.params.ts b/apps/server/src/modules/board/controller/dto/element/create-content-element.body.params.ts index a20de3bf6f4..61ae3083a9c 100644 --- a/apps/server/src/modules/board/controller/dto/element/create-content-element.body.params.ts +++ b/apps/server/src/modules/board/controller/dto/element/create-content-element.body.params.ts @@ -1,6 +1,6 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { ContentElementType } from '@shared/domain/domainobject'; import { IsEnum, IsInt, IsOptional, Min } from 'class-validator'; +import { ContentElementType } from '../../../domain'; export class CreateContentElementBodyParams { @IsEnum(ContentElementType) diff --git a/apps/server/src/modules/board/controller/dto/element/drawing-element.response.ts b/apps/server/src/modules/board/controller/dto/element/drawing-element.response.ts index cdf54c18a02..4b220349958 100644 --- a/apps/server/src/modules/board/controller/dto/element/drawing-element.response.ts +++ b/apps/server/src/modules/board/controller/dto/element/drawing-element.response.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { ContentElementType } from '@shared/domain/domainobject'; +import { ContentElementType } from '../../../domain'; import { TimestampsResponse } from '../timestamps.response'; export class DrawingElementContent { diff --git a/apps/server/src/modules/board/controller/dto/element/external-tool-element.response.ts b/apps/server/src/modules/board/controller/dto/element/external-tool-element.response.ts index 68afa9d815b..0cf7974a9a9 100644 --- a/apps/server/src/modules/board/controller/dto/element/external-tool-element.response.ts +++ b/apps/server/src/modules/board/controller/dto/element/external-tool-element.response.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { ContentElementType } from '@shared/domain/domainobject'; +import { ContentElementType } from '../../../domain'; import { TimestampsResponse } from '../timestamps.response'; export class ExternalToolElementContent { diff --git a/apps/server/src/modules/board/controller/dto/element/file-element.response.ts b/apps/server/src/modules/board/controller/dto/element/file-element.response.ts index 9713891f7be..aad15af3237 100644 --- a/apps/server/src/modules/board/controller/dto/element/file-element.response.ts +++ b/apps/server/src/modules/board/controller/dto/element/file-element.response.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { DecodeHtmlEntities } from '@shared/controller'; -import { ContentElementType } from '@shared/domain/domainobject'; +import { ContentElementType } from '../../../domain'; import { TimestampsResponse } from '../timestamps.response'; export class FileElementContent { diff --git a/apps/server/src/modules/board/controller/dto/element/link-element.response.ts b/apps/server/src/modules/board/controller/dto/element/link-element.response.ts index 5ec02920642..2c647477f22 100644 --- a/apps/server/src/modules/board/controller/dto/element/link-element.response.ts +++ b/apps/server/src/modules/board/controller/dto/element/link-element.response.ts @@ -1,5 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { ContentElementType } from '@shared/domain/domainobject'; +import { ContentElementType } from '../../../domain'; import { TimestampsResponse } from '../timestamps.response'; export class LinkElementContent { diff --git a/apps/server/src/modules/board/controller/dto/element/rich-text-element.response.ts b/apps/server/src/modules/board/controller/dto/element/rich-text-element.response.ts index 1b6dcbc7313..12b394d15d0 100644 --- a/apps/server/src/modules/board/controller/dto/element/rich-text-element.response.ts +++ b/apps/server/src/modules/board/controller/dto/element/rich-text-element.response.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -import { ContentElementType } from '@shared/domain/domainobject'; import { InputFormat } from '@shared/domain/types'; +import { ContentElementType } from '../../../domain'; import { TimestampsResponse } from '../timestamps.response'; export class RichTextElementContent { diff --git a/apps/server/src/modules/board/controller/dto/element/submission-container-element.response.ts b/apps/server/src/modules/board/controller/dto/element/submission-container-element.response.ts index 2b0ced78a79..0490df9e429 100644 --- a/apps/server/src/modules/board/controller/dto/element/submission-container-element.response.ts +++ b/apps/server/src/modules/board/controller/dto/element/submission-container-element.response.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { ContentElementType } from '@shared/domain/domainobject'; +import { ContentElementType } from '../../../domain'; import { TimestampsResponse } from '../timestamps.response'; export class SubmissionContainerElementContent { diff --git a/apps/server/src/modules/board/controller/dto/element/update-element-content.body.params.ts b/apps/server/src/modules/board/controller/dto/element/update-element-content.body.params.ts index 700b75ac950..bcf677cbc46 100644 --- a/apps/server/src/modules/board/controller/dto/element/update-element-content.body.params.ts +++ b/apps/server/src/modules/board/controller/dto/element/update-element-content.body.params.ts @@ -1,8 +1,8 @@ import { ApiProperty, ApiPropertyOptional, getSchemaPath } from '@nestjs/swagger'; -import { ContentElementType } from '@shared/domain/domainobject'; import { InputFormat } from '@shared/domain/types'; import { Type } from 'class-transformer'; import { IsDate, IsEnum, IsMongoId, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { ContentElementType } from '../../../domain/types'; export abstract class ElementContentBody { @IsEnum(ContentElementType) diff --git a/apps/server/src/modules/board/controller/mapper/base-mapper.interface.ts b/apps/server/src/modules/board/controller/mapper/base-mapper.interface.ts index 99f5186b286..bb67249e04f 100644 --- a/apps/server/src/modules/board/controller/mapper/base-mapper.interface.ts +++ b/apps/server/src/modules/board/controller/mapper/base-mapper.interface.ts @@ -1,7 +1,7 @@ -import type { AnyBoardDo } from '@shared/domain/domainobject'; +import type { AnyBoardNode } from '../../domain'; import type { AnyContentElementResponse } from '../dto'; -export interface BaseResponseMapper { +export interface BaseResponseMapper { mapToResponse(element: T): AnyContentElementResponse; canMap(element: T): boolean; diff --git a/apps/server/src/modules/board/controller/mapper/board-response.mapper.ts b/apps/server/src/modules/board/controller/mapper/board-response.mapper.ts index 74682cf1e25..061f9576c9c 100644 --- a/apps/server/src/modules/board/controller/mapper/board-response.mapper.ts +++ b/apps/server/src/modules/board/controller/mapper/board-response.mapper.ts @@ -1,5 +1,5 @@ import { HttpException, HttpStatus } from '@nestjs/common'; -import { Column, ColumnBoard } from '@shared/domain/domainobject'; +import { Column, ColumnBoard } from '../../domain'; import { BoardResponse, TimestampsResponse } from '../dto'; import { ColumnResponseMapper } from './column-response.mapper'; diff --git a/apps/server/src/modules/board/controller/mapper/card-response.mapper.ts b/apps/server/src/modules/board/controller/mapper/card-response.mapper.ts index 935fe120ba4..17e426997ea 100644 --- a/apps/server/src/modules/board/controller/mapper/card-response.mapper.ts +++ b/apps/server/src/modules/board/controller/mapper/card-response.mapper.ts @@ -1,4 +1,4 @@ -import { Card } from '@shared/domain/domainobject'; +import { Card } from '../../domain'; import { CardResponse, TimestampsResponse, VisibilitySettingsResponse } from '../dto'; import { ContentElementResponseFactory } from './content-element-response.factory'; diff --git a/apps/server/src/modules/board/controller/mapper/collaborative-text-editor-element-response.mapper.ts b/apps/server/src/modules/board/controller/mapper/collaborative-text-editor-element-response.mapper.ts index fcd768be342..d6dca93b18f 100644 --- a/apps/server/src/modules/board/controller/mapper/collaborative-text-editor-element-response.mapper.ts +++ b/apps/server/src/modules/board/controller/mapper/collaborative-text-editor-element-response.mapper.ts @@ -1,5 +1,4 @@ -import { ContentElementType } from '@shared/domain/domainobject'; -import { CollaborativeTextEditorElement } from '@shared/domain/domainobject/board/collaborative-text-editor-element.do'; +import { ContentElementType, CollaborativeTextEditorElement } from '../../domain'; import { TimestampsResponse } from '../dto'; import { CollaborativeTextEditorElementResponse } from '../dto/element/collaborative-text-editor-element.response'; import { BaseResponseMapper } from './base-mapper.interface'; diff --git a/apps/server/src/modules/board/controller/mapper/column-response.mapper.ts b/apps/server/src/modules/board/controller/mapper/column-response.mapper.ts index 8938f029316..adf831e785d 100644 --- a/apps/server/src/modules/board/controller/mapper/column-response.mapper.ts +++ b/apps/server/src/modules/board/controller/mapper/column-response.mapper.ts @@ -1,12 +1,12 @@ import { HttpException, HttpStatus } from '@nestjs/common'; -import { Card, Column } from '@shared/domain/domainobject'; +import { Card, Column } from '../../domain'; import { CardSkeletonResponse, ColumnResponse, TimestampsResponse } from '../dto'; export class ColumnResponseMapper { static mapToResponse(column: Column): ColumnResponse { const result = new ColumnResponse({ id: column.id, - title: column.title, + title: column.title ?? '', cards: column.children.map((card) => { /* istanbul ignore next */ if (!(card instanceof Card)) { diff --git a/apps/server/src/modules/board/controller/mapper/content-element-response.factory.spec.ts b/apps/server/src/modules/board/controller/mapper/content-element-response.factory.spec.ts index c4fb577f5c2..a4bad1ef915 100644 --- a/apps/server/src/modules/board/controller/mapper/content-element-response.factory.spec.ts +++ b/apps/server/src/modules/board/controller/mapper/content-element-response.factory.spec.ts @@ -5,7 +5,7 @@ import { linkElementFactory, richTextElementFactory, submissionContainerElementFactory, -} from '@shared/testing'; +} from '../../testing'; import { FileElementResponse, LinkElementResponse, diff --git a/apps/server/src/modules/board/controller/mapper/content-element-response.factory.ts b/apps/server/src/modules/board/controller/mapper/content-element-response.factory.ts index b7cbd4726ed..4047a921c98 100644 --- a/apps/server/src/modules/board/controller/mapper/content-element-response.factory.ts +++ b/apps/server/src/modules/board/controller/mapper/content-element-response.factory.ts @@ -1,5 +1,5 @@ import { NotImplementedException, UnprocessableEntityException } from '@nestjs/common'; -import { AnyBoardDo, FileElement, RichTextElement } from '@shared/domain/domainobject'; +import { AnyBoardNode, FileElement, RichTextElement } from '../../domain'; import { AnyContentElementResponse, FileElementResponse, @@ -27,7 +27,7 @@ export class ContentElementResponseFactory { CollaborativeTextEditorElementResponseMapper.getInstance(), ]; - static mapToResponse(element: AnyBoardDo): AnyContentElementResponse { + static mapToResponse(element: AnyBoardNode): AnyContentElementResponse { const elementMapper = this.mappers.find((mapper) => mapper.canMap(element)); if (!elementMapper) { diff --git a/apps/server/src/modules/board/controller/mapper/create-board-response.mapper.ts b/apps/server/src/modules/board/controller/mapper/create-board-response.mapper.ts index bb4ee33f014..bbdd6a48547 100644 --- a/apps/server/src/modules/board/controller/mapper/create-board-response.mapper.ts +++ b/apps/server/src/modules/board/controller/mapper/create-board-response.mapper.ts @@ -1,4 +1,4 @@ -import { ColumnBoard } from '@shared/domain/domainobject'; +import { ColumnBoard } from '../../domain'; import { CreateBoardResponse } from '../dto'; export class CreateBoardResponseMapper { diff --git a/apps/server/src/modules/board/controller/mapper/drawing-element-response.mapper.ts b/apps/server/src/modules/board/controller/mapper/drawing-element-response.mapper.ts index 90b4656617f..089740fe731 100644 --- a/apps/server/src/modules/board/controller/mapper/drawing-element-response.mapper.ts +++ b/apps/server/src/modules/board/controller/mapper/drawing-element-response.mapper.ts @@ -1,5 +1,4 @@ -import { DrawingElement } from '@shared/domain/domainobject/board/drawing-element.do'; -import { ContentElementType } from '@shared/domain/domainobject'; +import { ContentElementType, DrawingElement } from '../../domain'; import { DrawingElementContent, DrawingElementResponse } from '../dto/element/drawing-element.response'; import { TimestampsResponse } from '../dto'; import { BaseResponseMapper } from './base-mapper.interface'; diff --git a/apps/server/src/modules/board/controller/mapper/external-tool-element-response.mapper.ts b/apps/server/src/modules/board/controller/mapper/external-tool-element-response.mapper.ts index 30a46e0d326..226eb65f907 100644 --- a/apps/server/src/modules/board/controller/mapper/external-tool-element-response.mapper.ts +++ b/apps/server/src/modules/board/controller/mapper/external-tool-element-response.mapper.ts @@ -1,4 +1,4 @@ -import { ContentElementType, ExternalToolElement } from '@shared/domain/domainobject'; +import { ContentElementType, ExternalToolElement } from '../../domain'; import { ExternalToolElementContent, ExternalToolElementResponse, TimestampsResponse } from '../dto'; import { BaseResponseMapper } from './base-mapper.interface'; diff --git a/apps/server/src/modules/board/controller/mapper/file-element-response.mapper.ts b/apps/server/src/modules/board/controller/mapper/file-element-response.mapper.ts index e0b78a069a2..6bf2eb5d8da 100644 --- a/apps/server/src/modules/board/controller/mapper/file-element-response.mapper.ts +++ b/apps/server/src/modules/board/controller/mapper/file-element-response.mapper.ts @@ -1,4 +1,4 @@ -import { ContentElementType, FileElement } from '@shared/domain/domainobject'; +import { ContentElementType, FileElement } from '../../domain'; import { FileElementContent, FileElementResponse, TimestampsResponse } from '../dto'; import { BaseResponseMapper } from './base-mapper.interface'; diff --git a/apps/server/src/modules/board/controller/mapper/link-element-response.mapper.ts b/apps/server/src/modules/board/controller/mapper/link-element-response.mapper.ts index 5abf9f3fe8b..dd1a28aa874 100644 --- a/apps/server/src/modules/board/controller/mapper/link-element-response.mapper.ts +++ b/apps/server/src/modules/board/controller/mapper/link-element-response.mapper.ts @@ -1,4 +1,4 @@ -import { ContentElementType, LinkElement } from '@shared/domain/domainobject'; +import { ContentElementType, LinkElement } from '../../domain'; import { LinkElementContent, LinkElementResponse, TimestampsResponse } from '../dto'; import { BaseResponseMapper } from './base-mapper.interface'; diff --git a/apps/server/src/modules/board/controller/mapper/rich-text-element-response.mapper.ts b/apps/server/src/modules/board/controller/mapper/rich-text-element-response.mapper.ts index 1e513adb0a6..c845bc63346 100644 --- a/apps/server/src/modules/board/controller/mapper/rich-text-element-response.mapper.ts +++ b/apps/server/src/modules/board/controller/mapper/rich-text-element-response.mapper.ts @@ -1,4 +1,4 @@ -import { ContentElementType, RichTextElement } from '@shared/domain/domainobject'; +import { ContentElementType, RichTextElement } from '../../domain'; import { TimestampsResponse } from '../dto'; import { RichTextElementContent, RichTextElementResponse } from '../dto/element/rich-text-element.response'; import { BaseResponseMapper } from './base-mapper.interface'; diff --git a/apps/server/src/modules/board/controller/mapper/submission-container-element-response.mapper.ts b/apps/server/src/modules/board/controller/mapper/submission-container-element-response.mapper.ts index df58f12f73c..b65a1b5654e 100644 --- a/apps/server/src/modules/board/controller/mapper/submission-container-element-response.mapper.ts +++ b/apps/server/src/modules/board/controller/mapper/submission-container-element-response.mapper.ts @@ -1,4 +1,4 @@ -import { ContentElementType, SubmissionContainerElement } from '@shared/domain/domainobject'; +import { ContentElementType, SubmissionContainerElement } from '../../domain'; import { SubmissionContainerElementContent, SubmissionContainerElementResponse, TimestampsResponse } from '../dto'; import { BaseResponseMapper } from './base-mapper.interface'; @@ -19,7 +19,7 @@ export class SubmissionContainerElementResponseMapper implements BaseResponseMap timestamps: new TimestampsResponse({ lastUpdatedAt: element.updatedAt, createdAt: element.createdAt }), type: ContentElementType.SUBMISSION_CONTAINER, content: new SubmissionContainerElementContent({ - dueDate: element.dueDate, + dueDate: element.dueDate ?? null, }), }); diff --git a/apps/server/src/modules/board/controller/mapper/submission-item-response.mapper.ts b/apps/server/src/modules/board/controller/mapper/submission-item-response.mapper.ts index 4145cc02306..ee2d82fe5db 100644 --- a/apps/server/src/modules/board/controller/mapper/submission-item-response.mapper.ts +++ b/apps/server/src/modules/board/controller/mapper/submission-item-response.mapper.ts @@ -4,7 +4,7 @@ import { RichTextElement, SubmissionItem, UserWithBoardRoles, -} from '@shared/domain/domainobject'; +} from '../../domain'; import { SubmissionItemResponse, SubmissionsResponse, TimestampsResponse, UserDataResponse } from '../dto'; import { ContentElementResponseFactory } from './content-element-response.factory'; diff --git a/apps/server/src/modules/board/controller/media-board/api-test/media-board.api.spec.ts b/apps/server/src/modules/board/controller/media-board/api-test/media-board.api.spec.ts index fe6791bcbd6..756fcddc685 100644 --- a/apps/server/src/modules/board/controller/media-board/api-test/media-board.api.spec.ts +++ b/apps/server/src/modules/board/controller/media-board/api-test/media-board.api.spec.ts @@ -1,5 +1,5 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; -import { type ServerConfig, serverConfig, ServerTestModule } from '@modules/server'; +import { serverConfig, ServerTestModule, type ServerConfig } from '@modules/server'; import { contextExternalToolEntityFactory } from '@modules/tool/context-external-tool/testing'; import { externalToolEntityFactory } from '@modules/tool/external-tool/testing'; import { schoolExternalToolEntityFactory } from '@modules/tool/school-external-tool/testing'; @@ -7,24 +7,21 @@ import { MediaUserLicenseEntity } from '@modules/user-license/entity'; import { mediaUserLicenseEntityFactory } from '@modules/user-license/testing'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; -import { MediaBoardNode } from '@shared/domain/entity'; +import { TestApiClient, UserAndAccountTestFactory, type DatesToStrings } from '@shared/testing'; +import { BoardExternalReferenceType, BoardLayout, MediaBoardColors } from '../../../domain'; +import { BoardNodeEntity } from '../../../repo'; import { - type DatesToStrings, - mediaBoardNodeFactory, - mediaExternalToolElementNodeFactory, - mediaLineNodeFactory, - TestApiClient, - UserAndAccountTestFactory, -} from '@shared/testing'; -import { MediaBoardColors, MediaBoardLayoutType } from '../../../domain'; + mediaBoardEntityFactory, + mediaExternalToolElementEntityFactory, + mediaLineEntityFactory, +} from '../../../testing'; import { CollapsableBodyParams, ColorBodyParams, LayoutBodyParams, MediaAvailableLineResponse, - type MediaBoardResponse, MediaLineResponse, + type MediaBoardResponse, } from '../dto'; const baseRouteName = '/media-boards'; @@ -57,14 +54,17 @@ describe('Media Board (API)', () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: studentUser.id, type: BoardExternalReferenceType.User, }, }); - const mediaLine = mediaLineNodeFactory.buildWithId({ parent: mediaBoard }); - const mediaElement = mediaExternalToolElementNodeFactory.buildWithId({ parent: mediaLine }); + const mediaLine = mediaLineEntityFactory.withParent(mediaBoard).build(); + const contextExternalToolId = new ObjectId().toHexString(); + const mediaElement = mediaExternalToolElementEntityFactory + .withParent(mediaLine) + .build({ contextExternalToolId }); await em.persistAndFlush([studentAccount, studentUser, mediaBoard, mediaLine, mediaElement]); em.clear(); @@ -76,11 +76,12 @@ describe('Media Board (API)', () => { mediaBoard, mediaLine, mediaElement, + contextExternalToolId, }; }; it('should return the media board of the user', async () => { - const { studentClient, mediaBoard, mediaLine, mediaElement } = await setup(); + const { studentClient, mediaBoard, mediaLine, mediaElement, contextExternalToolId } = await setup(); const response = await studentClient.get('me'); @@ -90,7 +91,7 @@ describe('Media Board (API)', () => { createdAt: mediaBoard.createdAt.toISOString(), lastUpdatedAt: mediaBoard.updatedAt.toISOString(), }, - layout: MediaBoardLayoutType.LIST, + layout: BoardLayout.LIST, lines: [ { id: mediaLine.id, @@ -100,7 +101,7 @@ describe('Media Board (API)', () => { }, collapsed: false, backgroundColor: MediaBoardColors.TRANSPARENT, - title: mediaLine.title, + title: mediaLine.title as string, elements: [ { id: mediaElement.id, @@ -109,7 +110,7 @@ describe('Media Board (API)', () => { lastUpdatedAt: mediaElement.updatedAt.toISOString(), }, content: { - contextExternalToolId: mediaElement.contextExternalTool.id, + contextExternalToolId, }, }, ], @@ -181,7 +182,7 @@ describe('Media Board (API)', () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: studentUser.id, type: BoardExternalReferenceType.User, @@ -224,7 +225,7 @@ describe('Media Board (API)', () => { config.FEATURE_MEDIA_SHELF_ENABLED = false; const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: studentUser.id, type: BoardExternalReferenceType.User, @@ -262,7 +263,7 @@ describe('Media Board (API)', () => { const config: ServerConfig = serverConfig(); config.FEATURE_MEDIA_SHELF_ENABLED = true; - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: new ObjectId().toHexString(), type: BoardExternalReferenceType.User, @@ -311,23 +312,22 @@ describe('Media Board (API)', () => { tool: unusedExternalTool, school: studentUser.school, }); - const contextExternalTool = contextExternalToolEntityFactory.build({ + const contextExternalTool = contextExternalToolEntityFactory.buildWithId({ schoolTool: schoolExternalTool, }); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: studentUser.id, type: BoardExternalReferenceType.User, }, - mediaAvailableLineBackgroundColor: MediaBoardColors.RED, - mediaAvailableLineCollapsed: true, - }); - const mediaLine = mediaLineNodeFactory.buildWithId({ parent: mediaBoard }); - const mediaElement = mediaExternalToolElementNodeFactory.buildWithId({ - parent: mediaLine, - contextExternalTool, + backgroundColor: MediaBoardColors.RED, + collapsed: true, }); + const mediaLine = mediaLineEntityFactory.withParent(mediaBoard).build(); + const mediaElement = mediaExternalToolElementEntityFactory + .withParent(mediaLine) + .build({ contextExternalToolId: contextExternalTool.id }); await em.persistAndFlush([ studentAccount, @@ -367,8 +367,8 @@ describe('Media Board (API)', () => { description: unusedExternalTool.description, }, ], - collapsed: mediaBoard.mediaAvailableLineCollapsed, - backgroundColor: mediaBoard.mediaAvailableLineBackgroundColor, + collapsed: mediaBoard.collapsed as boolean, + backgroundColor: mediaBoard.backgroundColor as MediaBoardColors, }); }); }); @@ -380,7 +380,7 @@ describe('Media Board (API)', () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: studentUser.id, type: BoardExternalReferenceType.User, @@ -417,7 +417,7 @@ describe('Media Board (API)', () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: new ObjectId().toHexString(), type: BoardExternalReferenceType.User, @@ -457,7 +457,7 @@ describe('Media Board (API)', () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: studentUser.id, type: BoardExternalReferenceType.User, @@ -513,20 +513,19 @@ describe('Media Board (API)', () => { tool: unusedExternalTool, school: studentUser.school, }); - const contextExternalTool = contextExternalToolEntityFactory.build({ + const contextExternalTool = contextExternalToolEntityFactory.buildWithId({ schoolTool: schoolExternalTool, }); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: studentUser.id, type: BoardExternalReferenceType.User, }, }); - const mediaLine = mediaLineNodeFactory.buildWithId({ parent: mediaBoard }); - const mediaElement = mediaExternalToolElementNodeFactory.buildWithId({ - parent: mediaLine, - contextExternalTool, + const mediaLine = mediaLineEntityFactory.withParent(mediaBoard).build(); + const mediaElement = mediaExternalToolElementEntityFactory.withParent(mediaLine).build({ + contextExternalToolId: contextExternalTool.id, }); const userLicense: MediaUserLicenseEntity = mediaUserLicenseEntityFactory.build({ @@ -602,22 +601,21 @@ describe('Media Board (API)', () => { tool: unusedExternalTool, school: studentUser.school, }); - const contextExternalTool = contextExternalToolEntityFactory.build({ + const contextExternalTool = contextExternalToolEntityFactory.buildWithId({ schoolTool: schoolExternalTool, }); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: studentUser.id, type: BoardExternalReferenceType.User, }, }); - const mediaLine = mediaLineNodeFactory.buildWithId({ parent: mediaBoard, collapsed: false }); - const mediaElement = mediaExternalToolElementNodeFactory.buildWithId({ - parent: mediaLine, - contextExternalTool, - }); + const mediaLine = mediaLineEntityFactory.withParent(mediaBoard).build({ collapsed: false }); + const mediaElement = mediaExternalToolElementEntityFactory + .withParent(mediaLine) + .build({ contextExternalToolId: contextExternalTool.id }); await em.persistAndFlush([ studentAccount, @@ -654,8 +652,8 @@ describe('Media Board (API)', () => { ); expect(response.status).toEqual(HttpStatus.NO_CONTENT); - const modifiedBoard = await em.findOneOrFail(MediaBoardNode, mediaBoard.id); - expect(modifiedBoard.mediaAvailableLineCollapsed).toBe(true); + const modifiedBoard = await em.findOneOrFail(BoardNodeEntity, mediaBoard.id); + expect(modifiedBoard.collapsed).toBe(true); }); }); @@ -666,7 +664,7 @@ describe('Media Board (API)', () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: studentUser.id, type: BoardExternalReferenceType.User, @@ -708,7 +706,7 @@ describe('Media Board (API)', () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: new ObjectId().toHexString(), type: BoardExternalReferenceType.User, @@ -765,22 +763,21 @@ describe('Media Board (API)', () => { tool: unusedExternalTool, school: studentUser.school, }); - const contextExternalTool = contextExternalToolEntityFactory.build({ + const contextExternalTool = contextExternalToolEntityFactory.buildWithId({ schoolTool: schoolExternalTool, }); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: studentUser.id, type: BoardExternalReferenceType.User, }, }); - const mediaLine = mediaLineNodeFactory.buildWithId({ parent: mediaBoard, collapsed: false }); - const mediaElement = mediaExternalToolElementNodeFactory.buildWithId({ - parent: mediaLine, - contextExternalTool, - }); + const mediaLine = mediaLineEntityFactory.withParent(mediaBoard).build({ collapsed: false }); + const mediaElement = mediaExternalToolElementEntityFactory + .withParent(mediaLine) + .build({ contextExternalToolId: contextExternalTool.id }); await em.persistAndFlush([ studentAccount, @@ -814,8 +811,8 @@ describe('Media Board (API)', () => { }); expect(response.status).toEqual(HttpStatus.NO_CONTENT); - const modifiedBoard = await em.findOneOrFail(MediaBoardNode, mediaBoard.id); - expect(modifiedBoard.mediaAvailableLineBackgroundColor).toBe(MediaBoardColors.BLUE); + const modifiedBoard = await em.findOneOrFail(BoardNodeEntity, mediaBoard.id); + expect(modifiedBoard.backgroundColor).toBe(MediaBoardColors.BLUE); }); }); @@ -826,7 +823,7 @@ describe('Media Board (API)', () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: studentUser.id, type: BoardExternalReferenceType.User, @@ -865,7 +862,7 @@ describe('Media Board (API)', () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: new ObjectId().toHexString(), type: BoardExternalReferenceType.User, @@ -907,7 +904,7 @@ describe('Media Board (API)', () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: studentUser.id, type: BoardExternalReferenceType.User, @@ -951,14 +948,14 @@ describe('Media Board (API)', () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: studentUser.id, type: BoardExternalReferenceType.User, }, }); - const mediaLine = mediaLineNodeFactory.buildWithId({ parent: mediaBoard }); - const mediaElement = mediaExternalToolElementNodeFactory.buildWithId({ parent: mediaLine }); + const mediaLine = mediaLineEntityFactory.withParent(mediaBoard).build(); + const mediaElement = mediaExternalToolElementEntityFactory.withParent(mediaLine).build(); await em.persistAndFlush([studentAccount, studentUser, mediaBoard, mediaLine, mediaElement]); em.clear(); @@ -977,12 +974,12 @@ describe('Media Board (API)', () => { const { studentClient, mediaBoard } = await setup(); const response = await studentClient.patch(`${mediaBoard.id}/layout`, { - layout: MediaBoardLayoutType.GRID, + layout: BoardLayout.GRID, }); expect(response.status).toEqual(HttpStatus.NO_CONTENT); - const modifiedBoard = await em.findOneOrFail(MediaBoardNode, mediaBoard.id); - expect(modifiedBoard.layout).toBe(MediaBoardLayoutType.GRID); + const modifiedBoard = await em.findOneOrFail(BoardNodeEntity, mediaBoard.id); + expect(modifiedBoard.layout).toBe(BoardLayout.GRID); }); }); @@ -993,7 +990,7 @@ describe('Media Board (API)', () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: studentUser.id, type: BoardExternalReferenceType.User, @@ -1030,7 +1027,7 @@ describe('Media Board (API)', () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: new ObjectId().toHexString(), type: BoardExternalReferenceType.User, @@ -1070,7 +1067,7 @@ describe('Media Board (API)', () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: studentUser.id, type: BoardExternalReferenceType.User, diff --git a/apps/server/src/modules/board/controller/media-board/api-test/media-element.api.spec.ts b/apps/server/src/modules/board/controller/media-board/api-test/media-element.api.spec.ts index f0f462d63cf..0ef23aa3fb2 100644 --- a/apps/server/src/modules/board/controller/media-board/api-test/media-element.api.spec.ts +++ b/apps/server/src/modules/board/controller/media-board/api-test/media-element.api.spec.ts @@ -1,20 +1,19 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; -import { type ServerConfig, serverConfig, ServerTestModule } from '@modules/server'; +import { serverConfig, ServerTestModule, type ServerConfig } from '@modules/server'; import { ContextExternalToolEntity, ContextExternalToolType } from '@modules/tool/context-external-tool/entity'; import { contextExternalToolEntityFactory } from '@modules/tool/context-external-tool/testing'; import { externalToolEntityFactory } from '@modules/tool/external-tool/testing'; import { schoolExternalToolEntityFactory } from '@modules/tool/school-external-tool/testing'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; -import { BoardNode } from '@shared/domain/entity'; +import { TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; +import { BoardExternalReferenceType } from '../../../domain'; +import { BoardNodeEntity } from '../../../repo'; import { - mediaBoardNodeFactory, - mediaExternalToolElementNodeFactory, - mediaLineNodeFactory, - TestApiClient, - UserAndAccountTestFactory, -} from '@shared/testing'; + mediaBoardEntityFactory, + mediaExternalToolElementEntityFactory, + mediaLineEntityFactory, +} from '../../../testing'; import { MoveElementBodyParams } from '../dto'; const baseRouteName = '/media-elements'; @@ -47,23 +46,15 @@ describe('Media Element (API)', () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: studentUser.id, type: BoardExternalReferenceType.User, }, }); - const mediaLine = mediaLineNodeFactory.buildWithId({ - parent: mediaBoard, - }); - const mediaElementA = mediaExternalToolElementNodeFactory.buildWithId({ - parent: mediaLine, - position: 0, - }); - const mediaElementB = mediaExternalToolElementNodeFactory.buildWithId({ - parent: mediaLine, - position: 1, - }); + const mediaLine = mediaLineEntityFactory.withParent(mediaBoard).build(); + const mediaElementA = mediaExternalToolElementEntityFactory.withParent(mediaLine).build({ position: 0 }); + const mediaElementB = mediaExternalToolElementEntityFactory.withParent(mediaLine).build({ position: 1 }); await em.persistAndFlush([studentAccount, studentUser, mediaBoard, mediaLine, mediaElementA, mediaElementB]); em.clear(); @@ -87,8 +78,8 @@ describe('Media Element (API)', () => { }); expect(response.status).toEqual(HttpStatus.NO_CONTENT); - const modifiedElementA = await em.findOneOrFail(BoardNode, mediaElementA.id); - const modifiedElementB = await em.findOneOrFail(BoardNode, mediaElementB.id); + const modifiedElementA = await em.findOneOrFail(BoardNodeEntity, mediaElementA.id); + const modifiedElementB = await em.findOneOrFail(BoardNodeEntity, mediaElementB.id); expect(modifiedElementA.position).toEqual(1); expect(modifiedElementB.position).toEqual(0); }); @@ -101,19 +92,14 @@ describe('Media Element (API)', () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: studentUser.id, type: BoardExternalReferenceType.User, }, }); - const mediaLine = mediaLineNodeFactory.buildWithId({ - parent: mediaBoard, - }); - const mediaElement = mediaExternalToolElementNodeFactory.buildWithId({ - parent: mediaLine, - position: 0, - }); + const mediaLine = mediaLineEntityFactory.withParent(mediaBoard).build(); + const mediaElement = mediaExternalToolElementEntityFactory.withParent(mediaLine).build({ position: 0 }); await em.persistAndFlush([studentAccount, studentUser, mediaBoard, mediaLine, mediaElement]); em.clear(); @@ -151,19 +137,14 @@ describe('Media Element (API)', () => { const config: ServerConfig = serverConfig(); config.FEATURE_MEDIA_SHELF_ENABLED = true; - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: new ObjectId().toHexString(), type: BoardExternalReferenceType.User, }, }); - const mediaLine = mediaLineNodeFactory.buildWithId({ - parent: mediaBoard, - }); - const mediaElement = mediaExternalToolElementNodeFactory.buildWithId({ - parent: mediaLine, - position: 0, - }); + const mediaLine = mediaLineEntityFactory.withParent(mediaBoard).build(); + const mediaElement = mediaExternalToolElementEntityFactory.withParent(mediaLine).build({ position: 0 }); await em.persistAndFlush([mediaBoard, mediaLine]); em.clear(); @@ -202,20 +183,18 @@ describe('Media Element (API)', () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - const externalTool = externalToolEntityFactory.buildWithId(); - const schoolExternalTool = schoolExternalToolEntityFactory.buildWithId({ + const externalTool = externalToolEntityFactory.build(); + const schoolExternalTool = schoolExternalToolEntityFactory.build({ tool: externalTool, school: studentUser.school, }); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: studentUser.id, type: BoardExternalReferenceType.User, }, }); - const mediaLine = mediaLineNodeFactory.buildWithId({ - parent: mediaBoard, - }); + const mediaLine = mediaLineEntityFactory.withParent(mediaBoard).build(); await em.persistAndFlush([ studentAccount, @@ -327,29 +306,26 @@ describe('Media Element (API)', () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - const externalTool = externalToolEntityFactory.buildWithId(); - const schoolExternalTool = schoolExternalToolEntityFactory.buildWithId({ + const externalTool = externalToolEntityFactory.build(); + const schoolExternalTool = schoolExternalToolEntityFactory.build({ tool: externalTool, school: studentUser.school, }); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: studentUser.id, type: BoardExternalReferenceType.User, }, }); - const mediaLine = mediaLineNodeFactory.buildWithId({ - parent: mediaBoard, - }); + const mediaLine = mediaLineEntityFactory.withParent(mediaBoard).build(); const contextExternalTool = contextExternalToolEntityFactory.buildWithId({ schoolTool: schoolExternalTool, contextType: ContextExternalToolType.MEDIA_BOARD, contextId: mediaBoard.id, }); - const mediaElement = mediaExternalToolElementNodeFactory.buildWithId({ - parent: mediaLine, - contextExternalTool, - }); + const mediaElement = mediaExternalToolElementEntityFactory + .withParent(mediaLine) + .build({ contextExternalToolId: contextExternalTool.id }); await em.persistAndFlush([ studentAccount, @@ -368,22 +344,20 @@ describe('Media Element (API)', () => { return { studentClient, mediaElement, + contextExternalToolId: contextExternalTool.id, }; }; it('should delete the element', async () => { - const { studentClient, mediaElement } = await setup(); + const { studentClient, mediaElement, contextExternalToolId } = await setup(); - const response = await studentClient.delete(`${mediaElement.id}`); + const response = await studentClient.delete(mediaElement.id); expect(response.status).toEqual(HttpStatus.NO_CONTENT); - const deletedElement = await em.findOne(BoardNode, mediaElement.id); + const deletedElement = await em.findOne(BoardNodeEntity, mediaElement.id); expect(deletedElement).toBeNull(); - const deletedContextExternalTool = await em.findOne( - ContextExternalToolEntity, - mediaElement.contextExternalTool.id - ); + const deletedContextExternalTool = await em.findOne(ContextExternalToolEntity, contextExternalToolId); expect(deletedContextExternalTool).toBeNull(); }); }); diff --git a/apps/server/src/modules/board/controller/media-board/api-test/media-line.api.spec.ts b/apps/server/src/modules/board/controller/media-board/api-test/media-line.api.spec.ts index 408dbadc6dc..aba9b5a7d8d 100644 --- a/apps/server/src/modules/board/controller/media-board/api-test/media-line.api.spec.ts +++ b/apps/server/src/modules/board/controller/media-board/api-test/media-line.api.spec.ts @@ -2,12 +2,12 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { type ServerConfig, serverConfig, ServerTestModule } from '@modules/server'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; -import { BoardNode, MediaLineNode } from '@shared/domain/entity'; -import { mediaBoardNodeFactory, mediaLineNodeFactory, TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; -import { MediaBoardColors } from '../../../domain'; +import { TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; +import { BoardNodeEntity } from '../../../repo'; +import { BoardExternalReferenceType, MediaBoardColors } from '../../../domain'; import { MoveColumnBodyParams, RenameBodyParams } from '../../dto'; import { CollapsableBodyParams, ColorBodyParams } from '../dto'; +import { mediaBoardEntityFactory, mediaLineEntityFactory } from '../../../testing'; const baseRouteName = '/media-lines'; @@ -39,18 +39,16 @@ describe('Media Line (API)', () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: studentUser.id, type: BoardExternalReferenceType.User, }, }); - const mediaLineA = mediaLineNodeFactory.buildWithId({ - parent: mediaBoard, + const mediaLineA = mediaLineEntityFactory.withParent(mediaBoard).build({ position: 0, }); - const mediaLineB = mediaLineNodeFactory.buildWithId({ - parent: mediaBoard, + const mediaLineB = mediaLineEntityFactory.withParent(mediaBoard).build({ position: 1, }); @@ -76,8 +74,8 @@ describe('Media Line (API)', () => { }); expect(response.status).toEqual(HttpStatus.NO_CONTENT); - const modifiedLineA = await em.findOneOrFail(BoardNode, mediaLineA.id); - const modifiedLineB = await em.findOneOrFail(BoardNode, mediaLineB.id); + const modifiedLineA = await em.findOneOrFail(BoardNodeEntity, mediaLineA.id); + const modifiedLineB = await em.findOneOrFail(BoardNodeEntity, mediaLineB.id); expect(modifiedLineA.position).toEqual(1); expect(modifiedLineB.position).toEqual(0); }); @@ -90,14 +88,13 @@ describe('Media Line (API)', () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: studentUser.id, type: BoardExternalReferenceType.User, }, }); - const mediaLine = mediaLineNodeFactory.buildWithId({ - parent: mediaBoard, + const mediaLine = mediaLineEntityFactory.withParent(mediaBoard).build({ position: 0, }); @@ -136,14 +133,13 @@ describe('Media Line (API)', () => { const config: ServerConfig = serverConfig(); config.FEATURE_MEDIA_SHELF_ENABLED = true; - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: new ObjectId().toHexString(), type: BoardExternalReferenceType.User, }, }); - const mediaLine = mediaLineNodeFactory.buildWithId({ - parent: mediaBoard, + const mediaLine = mediaLineEntityFactory.withParent(mediaBoard).build({ position: 0, }); @@ -183,14 +179,13 @@ describe('Media Line (API)', () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: studentUser.id, type: BoardExternalReferenceType.User, }, }); - const mediaLine = mediaLineNodeFactory.buildWithId({ - parent: mediaBoard, + const mediaLine = mediaLineEntityFactory.withParent(mediaBoard).build({ title: '', }); @@ -214,7 +209,7 @@ describe('Media Line (API)', () => { }); expect(response.status).toEqual(HttpStatus.NO_CONTENT); - const modifiedLine = await em.findOneOrFail(BoardNode, mediaLine.id); + const modifiedLine = await em.findOneOrFail(BoardNodeEntity, mediaLine.id); expect(modifiedLine.title).toEqual('newTitle'); }); }); @@ -226,14 +221,13 @@ describe('Media Line (API)', () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: studentUser.id, type: BoardExternalReferenceType.User, }, }); - const mediaLine = mediaLineNodeFactory.buildWithId({ - parent: mediaBoard, + const mediaLine = mediaLineEntityFactory.withParent(mediaBoard).build({ title: '', }); @@ -270,14 +264,13 @@ describe('Media Line (API)', () => { const config: ServerConfig = serverConfig(); config.FEATURE_MEDIA_SHELF_ENABLED = true; - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: new ObjectId().toHexString(), type: BoardExternalReferenceType.User, }, }); - const mediaLine = mediaLineNodeFactory.buildWithId({ - parent: mediaBoard, + const mediaLine = mediaLineEntityFactory.withParent(mediaBoard).build({ title: '', }); @@ -315,15 +308,13 @@ describe('Media Line (API)', () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: studentUser.id, type: BoardExternalReferenceType.User, }, }); - const mediaLine = mediaLineNodeFactory.buildWithId({ - parent: mediaBoard, - }); + const mediaLine = mediaLineEntityFactory.withParent(mediaBoard).build(); await em.persistAndFlush([studentAccount, studentUser, mediaBoard, mediaLine]); em.clear(); @@ -345,7 +336,7 @@ describe('Media Line (API)', () => { }); expect(response.status).toEqual(HttpStatus.NO_CONTENT); - const modifiedLine = await em.findOneOrFail(MediaLineNode, mediaLine.id); + const modifiedLine = await em.findOneOrFail(BoardNodeEntity, mediaLine.id); expect(modifiedLine.backgroundColor).toEqual(MediaBoardColors.BLUE); }); }); @@ -357,16 +348,15 @@ describe('Media Line (API)', () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: studentUser.id, type: BoardExternalReferenceType.User, }, }); - const mediaLine = mediaLineNodeFactory.buildWithId({ - parent: mediaBoard, - backgroundColor: MediaBoardColors.TRANSPARENT, - }); + const mediaLine = mediaLineEntityFactory + .withParent(mediaBoard) + .build({ backgroundColor: MediaBoardColors.TRANSPARENT }); await em.persistAndFlush([studentAccount, studentUser, mediaBoard, mediaLine]); em.clear(); @@ -401,14 +391,13 @@ describe('Media Line (API)', () => { const config: ServerConfig = serverConfig(); config.FEATURE_MEDIA_SHELF_ENABLED = true; - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: new ObjectId().toHexString(), type: BoardExternalReferenceType.User, }, }); - const mediaLine = mediaLineNodeFactory.buildWithId({ - parent: mediaBoard, + const mediaLine = mediaLineEntityFactory.withParent(mediaBoard).build({ backgroundColor: MediaBoardColors.TRANSPARENT, }); @@ -446,16 +435,13 @@ describe('Media Line (API)', () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: studentUser.id, type: BoardExternalReferenceType.User, }, }); - const mediaLine = mediaLineNodeFactory.buildWithId({ - parent: mediaBoard, - collapsed: false, - }); + const mediaLine = mediaLineEntityFactory.withParent(mediaBoard).build({ collapsed: false }); await em.persistAndFlush([studentAccount, studentUser, mediaBoard, mediaLine]); em.clear(); @@ -477,7 +463,7 @@ describe('Media Line (API)', () => { }); expect(response.status).toEqual(HttpStatus.NO_CONTENT); - const modifiedLine = await em.findOneOrFail(MediaLineNode, mediaLine.id); + const modifiedLine = await em.findOneOrFail(BoardNodeEntity, mediaLine.id); expect(modifiedLine.collapsed).toEqual(true); }); }); @@ -489,16 +475,13 @@ describe('Media Line (API)', () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: studentUser.id, type: BoardExternalReferenceType.User, }, }); - const mediaLine = mediaLineNodeFactory.buildWithId({ - parent: mediaBoard, - title: '', - }); + const mediaLine = mediaLineEntityFactory.withParent(mediaBoard).build({ title: '' }); await em.persistAndFlush([studentAccount, studentUser, mediaBoard, mediaLine]); em.clear(); @@ -533,14 +516,13 @@ describe('Media Line (API)', () => { const config: ServerConfig = serverConfig(); config.FEATURE_MEDIA_SHELF_ENABLED = true; - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: new ObjectId().toHexString(), type: BoardExternalReferenceType.User, }, }); - const mediaLine = mediaLineNodeFactory.buildWithId({ - parent: mediaBoard, + const mediaLine = mediaLineEntityFactory.withParent(mediaBoard).build({ title: '', }); @@ -578,15 +560,13 @@ describe('Media Line (API)', () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: studentUser.id, type: BoardExternalReferenceType.User, }, }); - const mediaLine = mediaLineNodeFactory.buildWithId({ - parent: mediaBoard, - }); + const mediaLine = mediaLineEntityFactory.withParent(mediaBoard).build(); await em.persistAndFlush([studentAccount, studentUser, mediaBoard, mediaLine]); em.clear(); @@ -606,7 +586,7 @@ describe('Media Line (API)', () => { const response = await studentClient.delete(`${mediaLine.id}`); expect(response.status).toEqual(HttpStatus.NO_CONTENT); - const modifiedLine = await em.findOne(BoardNode, mediaLine.id); + const modifiedLine = await em.findOne(BoardNodeEntity, mediaLine.id); expect(modifiedLine).toBeNull(); }); }); @@ -618,15 +598,13 @@ describe('Media Line (API)', () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: studentUser.id, type: BoardExternalReferenceType.User, }, }); - const mediaLine = mediaLineNodeFactory.buildWithId({ - parent: mediaBoard, - }); + const mediaLine = mediaLineEntityFactory.withParent(mediaBoard).build(); await em.persistAndFlush([studentAccount, studentUser, mediaBoard, mediaLine]); em.clear(); @@ -659,15 +637,13 @@ describe('Media Line (API)', () => { const config: ServerConfig = serverConfig(); config.FEATURE_MEDIA_SHELF_ENABLED = true; - const mediaBoard = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: new ObjectId().toHexString(), type: BoardExternalReferenceType.User, }, }); - const mediaLine = mediaLineNodeFactory.buildWithId({ - parent: mediaBoard, - }); + const mediaLine = mediaLineEntityFactory.withParent(mediaBoard).build(); await em.persistAndFlush([mediaBoard, mediaLine]); em.clear(); diff --git a/apps/server/src/modules/board/controller/media-board/dto/color.body.params.ts b/apps/server/src/modules/board/controller/media-board/dto/color.body.params.ts index 65dd99b0b5f..a3ff7da910b 100644 --- a/apps/server/src/modules/board/controller/media-board/dto/color.body.params.ts +++ b/apps/server/src/modules/board/controller/media-board/dto/color.body.params.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEnum } from 'class-validator'; -import { MediaBoardColors } from '../../../domain'; +import { MediaBoardColors } from '../../../domain/media-board/types'; export class ColorBodyParams { @IsEnum(MediaBoardColors) diff --git a/apps/server/src/modules/board/controller/media-board/dto/layout.body.params.ts b/apps/server/src/modules/board/controller/media-board/dto/layout.body.params.ts index 8b7300ab1f5..2ead06c804d 100644 --- a/apps/server/src/modules/board/controller/media-board/dto/layout.body.params.ts +++ b/apps/server/src/modules/board/controller/media-board/dto/layout.body.params.ts @@ -1,9 +1,10 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum } from 'class-validator'; -import { MediaBoardLayoutType } from '../../../domain'; +import { IsEnum, NotEquals } from 'class-validator'; +import { BoardLayout } from '../../../domain/types'; export class LayoutBodyParams { - @IsEnum(MediaBoardLayoutType) - @ApiProperty({ enum: MediaBoardLayoutType, enumName: 'MediaBoardLayoutType' }) - layout!: MediaBoardLayoutType; + @IsEnum(BoardLayout) + @NotEquals(BoardLayout[BoardLayout.COLUMNS]) + @ApiProperty({ enum: BoardLayout, enumName: 'MediaBoardLayoutType' }) + layout!: BoardLayout; } diff --git a/apps/server/src/modules/board/controller/media-board/dto/media-available-line.response.ts b/apps/server/src/modules/board/controller/media-board/dto/media-available-line.response.ts index 6daed37d4f2..d858c1c159f 100644 --- a/apps/server/src/modules/board/controller/media-board/dto/media-available-line.response.ts +++ b/apps/server/src/modules/board/controller/media-board/dto/media-available-line.response.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { MediaBoardColors } from '../../../domain'; +import { MediaBoardColors } from '../../../domain/media-board/types'; import { MediaAvailableLineElementResponse } from './media-available-line-element.response'; export class MediaAvailableLineResponse { diff --git a/apps/server/src/modules/board/controller/media-board/dto/media-board.response.ts b/apps/server/src/modules/board/controller/media-board/dto/media-board.response.ts index 60719d48fc2..4550bc45bf0 100644 --- a/apps/server/src/modules/board/controller/media-board/dto/media-board.response.ts +++ b/apps/server/src/modules/board/controller/media-board/dto/media-board.response.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { MediaBoardLayoutType } from '../../../domain'; +import { BoardLayout } from '../../../domain'; import { TimestampsResponse } from '../../dto'; import { MediaLineResponse } from './media-line.response'; @@ -17,11 +17,11 @@ export class MediaBoardResponse { timestamps: TimestampsResponse; @ApiProperty({ - enum: MediaBoardLayoutType, + enum: BoardLayout, enumName: 'MediaBoardLayoutType', description: 'Layout of media board', }) - layout: MediaBoardLayoutType; + layout: BoardLayout; constructor(props: MediaBoardResponse) { this.id = props.id; diff --git a/apps/server/src/modules/board/controller/media-board/dto/media-line.response.ts b/apps/server/src/modules/board/controller/media-board/dto/media-line.response.ts index 5de971c08d2..f47becb56c3 100644 --- a/apps/server/src/modules/board/controller/media-board/dto/media-line.response.ts +++ b/apps/server/src/modules/board/controller/media-board/dto/media-line.response.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { DecodeHtmlEntities } from '@shared/controller'; -import { MediaBoardColors } from '../../../domain'; +import { MediaBoardColors } from '../../../domain/media-board/types'; import { TimestampsResponse } from '../../dto'; import { MediaExternalToolElementResponse } from './media-external-tool-element.response'; diff --git a/apps/server/src/modules/board/controller/media-board/mapper/media-available-line-response.mapper.ts b/apps/server/src/modules/board/controller/media-board/mapper/media-available-line-response.mapper.ts index 0f4f09637cd..5c4498e3d77 100644 --- a/apps/server/src/modules/board/controller/media-board/mapper/media-available-line-response.mapper.ts +++ b/apps/server/src/modules/board/controller/media-board/mapper/media-available-line-response.mapper.ts @@ -1,4 +1,4 @@ -import { MediaAvailableLine, MediaAvailableLineElement } from '@shared/domain/domainobject'; +import { MediaAvailableLine, MediaAvailableLineElement } from '../../../domain'; import { MediaAvailableLineElementResponse, MediaAvailableLineResponse } from '../dto'; export class MediaAvailableLineResponseMapper { diff --git a/apps/server/src/modules/board/controller/media-board/mapper/media-board-response.mapper.ts b/apps/server/src/modules/board/controller/media-board/mapper/media-board-response.mapper.ts index 33bdf99bf4c..c85074f8d8b 100644 --- a/apps/server/src/modules/board/controller/media-board/mapper/media-board-response.mapper.ts +++ b/apps/server/src/modules/board/controller/media-board/mapper/media-board-response.mapper.ts @@ -1,4 +1,4 @@ -import { type AnyBoardDo, isMediaLine, type MediaBoard, type MediaLine } from '@shared/domain/domainobject'; +import { isMediaLine, type AnyBoardNode, type MediaBoard, type MediaLine } from '../../../domain'; import { TimestampsResponse } from '../../dto'; import { MediaBoardResponse, MediaLineResponse } from '../dto'; import { MediaLineResponseMapper } from './media-line-response.mapper'; @@ -6,7 +6,7 @@ import { MediaLineResponseMapper } from './media-line-response.mapper'; export class MediaBoardResponseMapper { static mapToResponse(board: MediaBoard): MediaBoardResponse { const lines: MediaLineResponse[] = board.children - .filter((line: AnyBoardDo): line is MediaLine => isMediaLine(line)) + .filter((line: AnyBoardNode): line is MediaLine => isMediaLine(line)) .map((line: MediaLine) => MediaLineResponseMapper.mapToResponse(line)); const boardResponse: MediaBoardResponse = new MediaBoardResponse({ diff --git a/apps/server/src/modules/board/controller/media-board/mapper/media-external-tool-element-response.mapper.ts b/apps/server/src/modules/board/controller/media-board/mapper/media-external-tool-element-response.mapper.ts index 90300a1280c..2a0d51034ee 100644 --- a/apps/server/src/modules/board/controller/media-board/mapper/media-external-tool-element-response.mapper.ts +++ b/apps/server/src/modules/board/controller/media-board/mapper/media-external-tool-element-response.mapper.ts @@ -1,4 +1,4 @@ -import type { MediaExternalToolElement } from '@shared/domain/domainobject'; +import type { MediaExternalToolElement } from '../../../domain'; import { TimestampsResponse } from '../../dto'; import { MediaExternalToolElementContent, MediaExternalToolElementResponse } from '../dto'; diff --git a/apps/server/src/modules/board/controller/media-board/mapper/media-line-response.mapper.ts b/apps/server/src/modules/board/controller/media-board/mapper/media-line-response.mapper.ts index e299b13a5a7..23d232ca141 100644 --- a/apps/server/src/modules/board/controller/media-board/mapper/media-line-response.mapper.ts +++ b/apps/server/src/modules/board/controller/media-board/mapper/media-line-response.mapper.ts @@ -1,9 +1,4 @@ -import { - type AnyBoardDo, - isMediaExternalToolElement, - MediaExternalToolElement, - MediaLine, -} from '@shared/domain/domainobject'; +import { type AnyBoardNode, isMediaExternalToolElement, MediaExternalToolElement, MediaLine } from '../../../domain'; import { TimestampsResponse } from '../../dto'; import { type MediaExternalToolElementResponse, MediaLineResponse } from '../dto'; import { MediaExternalToolElementResponseMapper } from './media-external-tool-element-response.mapper'; @@ -11,7 +6,7 @@ import { MediaExternalToolElementResponseMapper } from './media-external-tool-el export class MediaLineResponseMapper { static mapToResponse(line: MediaLine): MediaLineResponse { const elements: MediaExternalToolElementResponse[] = line.children - .filter((element: AnyBoardDo): element is MediaExternalToolElement => isMediaExternalToolElement(element)) + .filter((element: AnyBoardNode): element is MediaExternalToolElement => isMediaExternalToolElement(element)) .map((element: MediaExternalToolElement) => MediaExternalToolElementResponseMapper.mapToResponse(element)); const lineResponse: MediaLineResponse = new MediaLineResponse({ diff --git a/apps/server/src/modules/board/controller/media-board/media-board.controller.ts b/apps/server/src/modules/board/controller/media-board/media-board.controller.ts index 1bb60726b4a..2b331e05c16 100644 --- a/apps/server/src/modules/board/controller/media-board/media-board.controller.ts +++ b/apps/server/src/modules/board/controller/media-board/media-board.controller.ts @@ -22,7 +22,7 @@ import { ApiTags, } from '@nestjs/swagger'; import { ApiValidationError } from '@shared/common'; -import { MediaAvailableLine, MediaBoard, MediaLine } from '@shared/domain/domainobject'; +import { MediaAvailableLine, MediaBoard, MediaLine } from '../../domain'; import { MediaAvailableLineUc, MediaBoardUc } from '../../uc'; import { BoardUrlParams } from '../dto'; import { diff --git a/apps/server/src/modules/board/controller/media-board/media-element.controller.ts b/apps/server/src/modules/board/controller/media-board/media-element.controller.ts index d68e843d6ee..1f9df2d91ff 100644 --- a/apps/server/src/modules/board/controller/media-board/media-element.controller.ts +++ b/apps/server/src/modules/board/controller/media-board/media-element.controller.ts @@ -21,7 +21,7 @@ import { ApiTags, } from '@nestjs/swagger'; import { ApiValidationError } from '@shared/common'; -import { MediaExternalToolElement } from '@shared/domain/domainobject'; +import { MediaExternalToolElement } from '../../domain'; import { MediaElementUc } from '../../uc'; import { CreateMediaElementBodyParams, diff --git a/apps/server/src/modules/board/domain/board-node-authorizable.do.ts b/apps/server/src/modules/board/domain/board-node-authorizable.do.ts new file mode 100644 index 00000000000..84093004d69 --- /dev/null +++ b/apps/server/src/modules/board/domain/board-node-authorizable.do.ts @@ -0,0 +1,51 @@ +import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; +import { EntityId } from '@shared/domain/types'; +import { AnyBoardNode } from './types'; + +export enum BoardRoles { + EDITOR = 'editor', + READER = 'reader', +} + +export interface UserWithBoardRoles { + firstName?: string; + lastName?: string; + roles: BoardRoles[]; + userId: EntityId; +} + +export interface BoardNodeAuthorizableProps extends AuthorizableObject { + id: EntityId; + users: UserWithBoardRoles[]; + boardNode: AnyBoardNode; + rootNode: AnyBoardNode; + parentNode?: AnyBoardNode; +} + +export class BoardNodeAuthorizable extends DomainObject { + get users(): UserWithBoardRoles[] { + return this.props.users; + } + + get boardNode(): AnyBoardNode { + return this.props.boardNode; + } + + // TODO should we really be able to alter that? (check BoardNodeRule) + set boardNode(boardNode: AnyBoardNode) { + this.props.boardNode = boardNode; + } + + get parentNode(): AnyBoardNode | undefined { + return this.props.parentNode; + } + + // TODO should we really be able to alter that? (check BoardNodeRule) + set parentNode(boardNode: AnyBoardNode | undefined) { + this.props.parentNode = boardNode; + } + + get rootNode(): AnyBoardNode { + return this.props.rootNode; + } +} diff --git a/apps/server/src/modules/board/domain/board-node.do.spec.ts b/apps/server/src/modules/board/domain/board-node.do.spec.ts new file mode 100644 index 00000000000..6fff3c824d5 --- /dev/null +++ b/apps/server/src/modules/board/domain/board-node.do.spec.ts @@ -0,0 +1,334 @@ +import { cardFactory, columnBoardFactory, columnFactory } from '../testing'; +import { ROOT_PATH, joinPath } from './path-utils'; + +describe('BoardNode', () => { + describe('a new instance', () => { + const setup = () => { + const boardNode = columnFactory.build(); + + return { boardNode }; + }; + + it('should have no ancestors', () => { + const { boardNode } = setup(); + expect(boardNode.ancestorIds).toHaveLength(0); + }); + + it('should have no parent', () => { + const { boardNode } = setup(); + expect(boardNode.hasParent()).toBe(false); + expect(boardNode.parentId).not.toBeDefined(); + }); + + it('should have level = 0', () => { + const { boardNode } = setup(); + expect(boardNode.level).toBe(0); + }); + + it('should have root path', () => { + const { boardNode } = setup(); + expect(boardNode.path).toBe(ROOT_PATH); + }); + + describe('when children are provided', () => { + const setupWithChildren = () => { + const boardNode = columnFactory.build({ children: cardFactory.buildList(2) }); + + return { boardNode }; + }; + + it('should update the paths of children', () => { + const { boardNode } = setupWithChildren(); + + expect(boardNode.children[0].path).toBe(joinPath(boardNode.path, boardNode.id)); + expect(boardNode.children[1].path).toBe(joinPath(boardNode.path, boardNode.id)); + }); + + it('should update the positions of children', () => { + const { boardNode } = setupWithChildren(); + + expect(boardNode.children[0].position).toBe(0); + expect(boardNode.children[1].position).toBe(1); + }); + }); + }); + + describe('when adding a child', () => { + const setup = () => { + const parent = columnFactory.build(); + const child = cardFactory.build(); + const extraChild = cardFactory.build(); + + return { parent, child, extraChild }; + }; + + it('should update the ancestor list', () => { + const { parent, child } = setup(); + + parent.addChild(child); + + expect(child.ancestorIds).toEqual([parent.id]); + }); + + it('should update the children of the parent', () => { + const { parent, child } = setup(); + + parent.addChild(child); + + expect(parent.children).toEqual([child]); + }); + + it('should update the level of the child', () => { + const { parent, child } = setup(); + + parent.addChild(child); + + expect(child.level).toEqual(1); + }); + + it('should update the path of the child', () => { + const { parent, child } = setup(); + + parent.addChild(child); + + expect(child.path).toEqual(joinPath(parent.path, parent.id)); + }); + + it('should update the position of the child', () => { + const { parent, child, extraChild } = setup(); + + parent.addChild(child); + parent.addChild(extraChild); + + expect(child.position).toBe(0); + expect(extraChild.position).toBe(1); + }); + + describe('when child already exists', () => { + const setupWithChild = () => { + const child = cardFactory.build(); + const parent = columnFactory.build({ children: [child] }); + + return { parent, existingChild: child }; + }; + + it('should thow an error', () => { + const { parent, existingChild } = setupWithChild(); + + expect(() => parent.addChild(existingChild)).toThrowError(); + }); + }); + + describe('when position is given', () => { + describe('when position is valid', () => { + it('should insert at proper position', () => { + const { parent, child, extraChild } = setup(); + + parent.addChild(child, 0); + parent.addChild(extraChild, 0); + + expect(parent.children.map((n) => n.id)).toEqual([extraChild.id, child.id]); + }); + + it('should update child + siblings positions', () => { + const { parent, child, extraChild } = setup(); + + parent.addChild(child, 0); + parent.addChild(extraChild, 0); + + expect(extraChild.position).toBe(0); + expect(child.position).toBe(1); + }); + }); + + describe('when position < 0', () => { + it('should thow an error', () => { + const { parent, child } = setup(); + + expect(() => parent.addChild(child, -1)).toThrowError(); + }); + }); + + describe('when position > length', () => { + it('should thow an error', () => { + const { parent, child } = setup(); + + expect(() => parent.addChild(child, 1)).toThrowError(); + }); + }); + }); + + describe('when position omitted', () => { + it('should append the child', () => { + const { parent, child, extraChild } = setup(); + + parent.addChild(child); + parent.addChild(extraChild); + + expect(parent.children.map((n) => n.id)).toEqual([child.id, extraChild.id]); + }); + + it('should update child position', () => { + const { parent, child, extraChild } = setup(); + + parent.addChild(child); + parent.addChild(extraChild); + + expect(parent.children.map((n) => n.position)).toEqual([0, 1]); + }); + }); + + describe('when child is not allowed to add', () => { + it('should throw an error', () => { + const { parent, child } = setup(); + + expect(() => child.addChild(parent)).toThrowError(); + }); + }); + }); + + describe('constructor', () => { + const setup = () => { + const column = columnFactory.build(); + const board = columnBoardFactory.build({ children: [column] }); + return { board, column }; + }; + + it('should add children', () => { + const { board, column } = setup(); + + expect(board.children).toEqual([column]); + }); + + it('should update path of children', () => { + const { board, column } = setup(); + + expect(column.ancestorIds).toEqual([board.id]); + }); + + it('should update level of children', () => { + const { board, column } = setup(); + + expect(column.level).toEqual(board.level + 1); + }); + }); + + describe('when removing a child', () => { + const setup = () => { + const parent = columnFactory.build(); + const child = cardFactory.build(); + const extraChild = cardFactory.build(); + parent.addChild(child); + + return { parent, child, extraChild }; + }; + + it('should update the ancestor list', () => { + const { parent, child } = setup(); + + parent.removeChild(child); + + expect(child.ancestorIds).toEqual([]); + }); + + it('should update the children of the parent', () => { + const { parent, child } = setup(); + + parent.removeChild(child); + + expect(parent.children).toEqual([]); + }); + + it('should update the level of the child', () => { + const { parent, child } = setup(); + + parent.removeChild(child); + + expect(child.level).toEqual(0); + }); + + it('should update the path of the child', () => { + const { parent, child } = setup(); + + parent.removeChild(child); + + expect(child.path).toEqual(ROOT_PATH); + }); + + it('should update child position', () => { + const { parent, child, extraChild } = setup(); + parent.addChild(extraChild); + + parent.removeChild(child); + + expect(parent.children.map((n) => n.position)).toEqual([0]); + }); + }); + + describe('hasChild', () => { + const setup = () => { + const parent = columnFactory.build({ children: cardFactory.buildList(1) }); + const child = parent.children[0]; + const extraChild = cardFactory.build({ id: child.id }); + + return { parent, child, extraChild }; + }; + + it('should check by reference', () => { + const { parent, child, extraChild } = setup(); + + expect(parent.hasChild(child)).toBe(true); + expect(parent.hasChild(extraChild)).toBe(false); + }); + }); + + describe('isRoot', () => { + const setup = () => { + const parent = columnFactory.build({ children: cardFactory.buildList(1) }); + const child = parent.children[0]; + + return { parent, child }; + }; + + describe(`when there's no parent`, () => { + it('should return true', () => { + const { parent } = setup(); + + expect(parent.isRoot()).toBe(true); + }); + }); + + describe(`when having a parent`, () => { + it('should return false', () => { + const { child } = setup(); + + expect(child.isRoot()).toBe(false); + }); + }); + }); + + describe('rootId', () => { + const setup = () => { + const parent = columnFactory.build({ children: cardFactory.buildList(1) }); + const child = parent.children[0]; + + return { parent, child }; + }; + + describe(`when there's no parent`, () => { + it('should return the own id', () => { + const { parent } = setup(); + + expect(parent.rootId).toBe(parent.id); + }); + }); + + describe(`when having a parent`, () => { + it('should return the id of the parent', () => { + const { parent, child } = setup(); + + expect(child.rootId).toBe(parent.id); + }); + }); + }); +}); diff --git a/apps/server/src/modules/board/domain/board-node.do.ts b/apps/server/src/modules/board/domain/board-node.do.ts new file mode 100644 index 00000000000..7c1f50a0092 --- /dev/null +++ b/apps/server/src/modules/board/domain/board-node.do.ts @@ -0,0 +1,114 @@ +import { BadRequestException, ForbiddenException } from '@nestjs/common'; +import { DomainObject } from '@shared/domain/domain-object'; +import { EntityId } from '@shared/domain/types'; +import { joinPath, PATH_SEPARATOR, ROOT_PATH } from './path-utils'; +import type { AnyBoardNode, BoardNodeProps } from './types'; + +export abstract class BoardNode extends DomainObject { + constructor(props: T) { + super(props); + props.children.forEach((child, pos) => { + child.updatePath(this); + child.updatePosition(pos); + }); + } + + get level(): number { + return this.ancestorIds.length; + } + + get children(): readonly AnyBoardNode[] { + return this.props.children; + } + + get parentId(): EntityId | undefined { + const parentId = this.hasParent() ? this.ancestorIds[this.ancestorIds.length - 1] : undefined; + return parentId; + } + + hasParent() { + return this.ancestorIds.length > 0; + } + + get ancestorIds(): readonly EntityId[] { + const parentIds = this.props.path.split(PATH_SEPARATOR).filter((id) => id !== ''); + return parentIds; + } + + isRoot() { + return this.ancestorIds.length === 0; + } + + get rootId(): EntityId { + return this.isRoot() ? this.id : this.ancestorIds[0]; + } + + get path(): string { + return this.props.path; + } + + get position(): number { + return this.props.position; + } + + addChild(child: AnyBoardNode, position?: number): void { + if (!this.canHaveChild(child)) { + throw new ForbiddenException(`Cannot add child of type '${child.constructor.name}'`); + } + + const { children } = this.props; + + position = position ?? children.length; + if (position < 0 || position > children.length) { + throw new BadRequestException(`Invalid child position '${position}'`); + } + if (this.hasChild(child)) { + throw new BadRequestException(`Cannot add existing child id='${child.id}'`); + } + children.splice(position, 0, child); + + child.updatePath(this); + this.props.children.forEach((c, pos) => c.updatePosition(pos)); + } + + hasChild(child: AnyBoardNode): boolean { + const exists = this.children.includes(child); + return exists; + } + + abstract canHaveChild(childNode: AnyBoardNode): boolean; + + removeChild(child: AnyBoardNode): void { + this.props.children = this.children.filter((ch) => ch.id !== child.id); + this.props.children.forEach((c, pos) => c.updatePosition(pos)); + child.resetPath(); + } + + get createdAt(): Date { + return this.props.createdAt; + } + + get updatedAt(): Date { + return this.props.createdAt; + } + + private updatePath(parent: BoardNode): void { + this.props.path = joinPath(parent.path, parent.id); + this.props.level = parent.level + 1; + this.props.children.forEach((child) => { + child.updatePath(this); + }); + } + + private resetPath(): void { + this.props.path = ROOT_PATH; + this.props.level = 0; + this.props.children.forEach((child) => { + child.resetPath(); + }); + } + + private updatePosition(position: number): void { + this.props.position = position; + } +} diff --git a/apps/server/src/modules/board/domain/board-node.factory.ts b/apps/server/src/modules/board/domain/board-node.factory.ts new file mode 100644 index 00000000000..805bc20875f --- /dev/null +++ b/apps/server/src/modules/board/domain/board-node.factory.ts @@ -0,0 +1,115 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { Injectable, NotImplementedException } from '@nestjs/common'; +import { EntityId, InputFormat } from '@shared/domain/types'; +import { Card } from './card.do'; +import { CollaborativeTextEditorElement } from './collaborative-text-editor.do'; +import { ColumnBoard } from './colum-board.do'; +import { Column } from './column.do'; +import { DrawingElement } from './drawing-element.do'; +import { ExternalToolElement } from './external-tool-element.do'; +import { FileElement } from './file-element.do'; +import { LinkElement } from './link-element.do'; +import { ROOT_PATH } from './path-utils'; +import { RichTextElement } from './rich-text-element.do'; +import { SubmissionContainerElement } from './submission-container-element.do'; +import { SubmissionItem } from './submission-item.do'; +import { handleNonExhaustiveSwitch } from './type-mapping'; +import { AnyContentElement, BoardExternalReference, BoardLayout, BoardNodeProps, ContentElementType } from './types'; + +@Injectable() +export class BoardNodeFactory { + buildColumnBoard(props: { context: BoardExternalReference; title: string; layout: BoardLayout }): ColumnBoard { + const columnBoard = new ColumnBoard({ ...this.getBaseProps(), isVisible: false, ...props }); + + return columnBoard; + } + + buildColumn(): Column { + const column = new Column({ ...this.getBaseProps() }); + + return column; + } + + buildCard(children: AnyContentElement[] = []): Card { + // TODO right way to specify default card height? + const card = new Card({ ...this.getBaseProps(), height: 150, children }); + + return card; + } + + buildContentElement(type: ContentElementType): AnyContentElement { + let element!: AnyContentElement; + + switch (type) { + case ContentElementType.FILE: + element = new FileElement({ + ...this.getBaseProps(), + caption: '', + alternativeText: '', + }); + break; + case ContentElementType.LINK: + element = new LinkElement({ + ...this.getBaseProps(), + url: '', + title: '', + }); + break; + case ContentElementType.RICH_TEXT: + element = new RichTextElement({ + ...this.getBaseProps(), + text: '', + inputFormat: InputFormat.RICH_TEXT_CK5, + }); + break; + case ContentElementType.DRAWING: + element = new DrawingElement({ + ...this.getBaseProps(), + description: '', + }); + break; + case ContentElementType.SUBMISSION_CONTAINER: + element = new SubmissionContainerElement({ + ...this.getBaseProps(), + dueDate: undefined, + }); + break; + case ContentElementType.EXTERNAL_TOOL: + element = new ExternalToolElement({ + ...this.getBaseProps(), + }); + break; + case ContentElementType.COLLABORATIVE_TEXT_EDITOR: + element = new CollaborativeTextEditorElement({ + ...this.getBaseProps(), + }); + break; + default: + handleNonExhaustiveSwitch(type); + } + + if (!element) { + throw new NotImplementedException(`unknown type ${type} of element`); + } + + return element; + } + + buildSubmissionItem(props: { completed: boolean; userId: EntityId }): SubmissionItem { + const submissionItem = new SubmissionItem({ ...this.getBaseProps(), ...props }); + + return submissionItem; + } + + private getBaseProps(): BoardNodeProps { + return { + id: new ObjectId().toHexString(), + path: ROOT_PATH, + level: 0, + position: 0, + children: [], + createdAt: new Date(), + updatedAt: new Date(), + }; + } +} diff --git a/apps/server/src/modules/board/domain/card.do.ts b/apps/server/src/modules/board/domain/card.do.ts new file mode 100644 index 00000000000..76f2944379a --- /dev/null +++ b/apps/server/src/modules/board/domain/card.do.ts @@ -0,0 +1,26 @@ +import { BoardNode } from './board-node.do'; +import { AnyBoardNode, CardProps, isContentElement } from './types'; + +export class Card extends BoardNode { + get title(): string | undefined { + return this.props.title; + } + + set title(title: string | undefined) { + this.props.title = title; + } + + get height(): number { + return this.props.height; + } + + set height(height: number) { + this.props.height = height; + } + + canHaveChild(childNode: AnyBoardNode): boolean { + return isContentElement(childNode); + } +} + +export const isCard = (reference: unknown): reference is Card => reference instanceof Card; diff --git a/apps/server/src/modules/board/domain/collaborative-text-editor.do.spec.ts b/apps/server/src/modules/board/domain/collaborative-text-editor.do.spec.ts new file mode 100644 index 00000000000..d4f46c4fea5 --- /dev/null +++ b/apps/server/src/modules/board/domain/collaborative-text-editor.do.spec.ts @@ -0,0 +1,35 @@ +import { CollaborativeTextEditorElement, isCollaborativeTextEditorElement } from './collaborative-text-editor.do'; +import { BoardNodeProps } from './types'; + +describe('CollaborativeTextEditorElement', () => { + let collaborativeTextEditorElement: CollaborativeTextEditorElement; + + const boardNodeProps: BoardNodeProps = { + id: '1', + path: '', + level: 1, + position: 1, + children: [], + createdAt: new Date(), + updatedAt: new Date(), + }; + + beforeEach(() => { + collaborativeTextEditorElement = new CollaborativeTextEditorElement({ + ...boardNodeProps, + children: [], + }); + }); + + it('should be instance of CollaborativeTextEditorElement', () => { + expect(isCollaborativeTextEditorElement(collaborativeTextEditorElement)).toBe(true); + }); + + it('should not be instance of CollaborativeTextEditorElement', () => { + expect(isCollaborativeTextEditorElement({})).toBe(false); + }); + + it('should not have child', () => { + expect(collaborativeTextEditorElement.canHaveChild()).toBe(false); + }); +}); diff --git a/apps/server/src/modules/board/domain/collaborative-text-editor.do.ts b/apps/server/src/modules/board/domain/collaborative-text-editor.do.ts new file mode 100644 index 00000000000..90618b78d13 --- /dev/null +++ b/apps/server/src/modules/board/domain/collaborative-text-editor.do.ts @@ -0,0 +1,11 @@ +import { BoardNode } from './board-node.do'; +import type { CollaborativeTextEditorElementProps } from './types'; + +export class CollaborativeTextEditorElement extends BoardNode { + canHaveChild(): boolean { + return false; + } +} + +export const isCollaborativeTextEditorElement = (reference: unknown): reference is CollaborativeTextEditorElement => + reference instanceof CollaborativeTextEditorElement; diff --git a/apps/server/src/modules/board/domain/colum-board.do.ts b/apps/server/src/modules/board/domain/colum-board.do.ts new file mode 100644 index 00000000000..5b74b1adc61 --- /dev/null +++ b/apps/server/src/modules/board/domain/colum-board.do.ts @@ -0,0 +1,40 @@ +import { BoardNode } from './board-node.do'; +import { Column } from './column.do'; +import type { AnyBoardNode, BoardExternalReference, BoardLayout, ColumnBoardProps } from './types'; + +export class ColumnBoard extends BoardNode { + get title(): string { + return this.props.title; + } + + set title(title: string) { + this.props.title = title; + } + + get context(): BoardExternalReference { + return this.props.context; + } + + set context(context: BoardExternalReference) { + this.props.context = context; + } + + get isVisible(): boolean { + return this.props.isVisible; + } + + set isVisible(isVisible: boolean) { + this.props.isVisible = isVisible; + } + + get layout(): BoardLayout { + return this.props.layout; + } + + canHaveChild(childNode: AnyBoardNode): boolean { + const allowed = childNode instanceof Column; + return allowed; + } +} + +export const isColumnBoard = (reference: unknown): reference is ColumnBoard => reference instanceof ColumnBoard; diff --git a/apps/server/src/modules/board/domain/column.do.ts b/apps/server/src/modules/board/domain/column.do.ts new file mode 100644 index 00000000000..20230670370 --- /dev/null +++ b/apps/server/src/modules/board/domain/column.do.ts @@ -0,0 +1,19 @@ +import { BoardNode } from './board-node.do'; +import { Card } from './card.do'; +import type { AnyBoardNode, ColumnProps } from './types'; + +export class Column extends BoardNode { + get title(): string | undefined { + return this.props.title; + } + + set title(title: string | undefined) { + this.props.title = title; + } + + canHaveChild(childNode: AnyBoardNode): boolean { + return childNode instanceof Card; + } +} + +export const isColumn = (reference: unknown): reference is Column => reference instanceof Column; diff --git a/apps/server/src/modules/board/domain/drawing-element.do.spec.ts b/apps/server/src/modules/board/domain/drawing-element.do.spec.ts new file mode 100644 index 00000000000..7f21bf50375 --- /dev/null +++ b/apps/server/src/modules/board/domain/drawing-element.do.spec.ts @@ -0,0 +1,44 @@ +import { DrawingElement, isDrawingElement } from './drawing-element.do'; +import { BoardNodeProps } from './types'; + +describe('DrawingElement', () => { + let drawingElement: DrawingElement; + + const boardNodeProps: BoardNodeProps = { + id: '1', + path: '', + level: 1, + position: 1, + children: [], + createdAt: new Date(), + updatedAt: new Date(), + }; + + beforeEach(() => { + drawingElement = new DrawingElement({ + ...boardNodeProps, + description: 'Test description', + }); + }); + + it('should be instance of DrawingElement', () => { + expect(isDrawingElement(drawingElement)).toBe(true); + }); + + it('should not be instance of DrawingElement', () => { + expect(isDrawingElement({})).toBe(false); + }); + + it('should return description', () => { + expect(drawingElement.description).toBe('Test description'); + }); + + it('should set description', () => { + drawingElement.description = 'New description'; + expect(drawingElement.description).toBe('New description'); + }); + + it('should not have child', () => { + expect(drawingElement.canHaveChild()).toBe(false); + }); +}); diff --git a/apps/server/src/modules/board/domain/drawing-element.do.ts b/apps/server/src/modules/board/domain/drawing-element.do.ts new file mode 100644 index 00000000000..9c1890ef3f1 --- /dev/null +++ b/apps/server/src/modules/board/domain/drawing-element.do.ts @@ -0,0 +1,19 @@ +import { BoardNode } from './board-node.do'; +import type { DrawingElementProps } from './types'; + +export class DrawingElement extends BoardNode { + get description(): string { + return this.props.description || ''; + } + + set description(value: string) { + this.props.description = value; + } + + canHaveChild(): boolean { + return false; + } +} + +export const isDrawingElement = (reference: unknown): reference is DrawingElement => + reference instanceof DrawingElement; diff --git a/apps/server/src/modules/board/domain/external-tool-element.do.spec.ts b/apps/server/src/modules/board/domain/external-tool-element.do.spec.ts new file mode 100644 index 00000000000..d4058b76fc9 --- /dev/null +++ b/apps/server/src/modules/board/domain/external-tool-element.do.spec.ts @@ -0,0 +1,44 @@ +import { ExternalToolElement, isExternalToolElement } from './external-tool-element.do'; +import { BoardNodeProps } from './types'; + +describe('ExternalToolElement', () => { + let externalToolElement: ExternalToolElement; + + const boardNodeProps: BoardNodeProps = { + id: '1', + path: '', + level: 1, + position: 1, + children: [], + createdAt: new Date(), + updatedAt: new Date(), + }; + + beforeEach(() => { + externalToolElement = new ExternalToolElement({ + ...boardNodeProps, + contextExternalToolId: 'test-id', + }); + }); + + it('should be instance of ExternalToolElement', () => { + expect(isExternalToolElement(externalToolElement)).toBe(true); + }); + + it('should not be instance of ExternalToolElement', () => { + expect(isExternalToolElement({})).toBe(false); + }); + + it('should return contextExternalToolId', () => { + expect(externalToolElement.contextExternalToolId).toBe('test-id'); + }); + + it('should set contextExternalToolId', () => { + externalToolElement.contextExternalToolId = 'new-id'; + expect(externalToolElement.contextExternalToolId).toBe('new-id'); + }); + + it('should not have child', () => { + expect(externalToolElement.canHaveChild()).toBe(false); + }); +}); diff --git a/apps/server/src/modules/board/domain/external-tool-element.do.ts b/apps/server/src/modules/board/domain/external-tool-element.do.ts new file mode 100644 index 00000000000..7202688b1fb --- /dev/null +++ b/apps/server/src/modules/board/domain/external-tool-element.do.ts @@ -0,0 +1,19 @@ +import { BoardNode } from './board-node.do'; +import type { ExternalToolElementProps } from './types'; + +export class ExternalToolElement extends BoardNode { + get contextExternalToolId(): string | undefined { + return this.props.contextExternalToolId; + } + + set contextExternalToolId(value: string | undefined) { + this.props.contextExternalToolId = value; + } + + canHaveChild(): boolean { + return false; + } +} + +export const isExternalToolElement = (reference: unknown): reference is ExternalToolElement => + reference instanceof ExternalToolElement; diff --git a/apps/server/src/modules/board/domain/file-element.do.spec.ts b/apps/server/src/modules/board/domain/file-element.do.spec.ts new file mode 100644 index 00000000000..31a93ec48cd --- /dev/null +++ b/apps/server/src/modules/board/domain/file-element.do.spec.ts @@ -0,0 +1,54 @@ +import { FileElement, isFileElement } from './file-element.do'; +import { BoardNodeProps } from './types'; + +describe('FileElement', () => { + let fileElement: FileElement; + + const boardNodeProps: BoardNodeProps = { + id: '1', + path: '', + level: 1, + position: 1, + children: [], + createdAt: new Date(), + updatedAt: new Date(), + }; + + beforeEach(() => { + fileElement = new FileElement({ + ...boardNodeProps, + alternativeText: 'Test alt text', + caption: 'Test caption', + }); + }); + + it('should be instance of FileElement', () => { + expect(isFileElement(fileElement)).toBe(true); + }); + + it('should not be instance of FileElement', () => { + expect(isFileElement({})).toBe(false); + }); + + it('should return alternativeText', () => { + expect(fileElement.alternativeText).toBe('Test alt text'); + }); + + it('should set alternativeText', () => { + fileElement.alternativeText = 'New alt text'; + expect(fileElement.alternativeText).toBe('New alt text'); + }); + + it('should return caption', () => { + expect(fileElement.caption).toBe('Test caption'); + }); + + it('should set caption', () => { + fileElement.caption = 'New caption'; + expect(fileElement.caption).toBe('New caption'); + }); + + it('should not have child', () => { + expect(fileElement.canHaveChild()).toBe(false); + }); +}); diff --git a/apps/server/src/modules/board/domain/file-element.do.ts b/apps/server/src/modules/board/domain/file-element.do.ts new file mode 100644 index 00000000000..5ff7ef65d12 --- /dev/null +++ b/apps/server/src/modules/board/domain/file-element.do.ts @@ -0,0 +1,26 @@ +import { BoardNode } from './board-node.do'; +import type { FileElementProps } from './types'; + +export class FileElement extends BoardNode { + get alternativeText(): string { + return this.props.alternativeText || ''; + } + + set alternativeText(value: string) { + this.props.alternativeText = value; + } + + get caption(): string { + return this.props.caption || ''; + } + + set caption(value: string) { + this.props.caption = value; + } + + canHaveChild(): boolean { + return false; + } +} + +export const isFileElement = (reference: unknown): reference is FileElement => reference instanceof FileElement; diff --git a/apps/server/src/modules/board/domain/index.ts b/apps/server/src/modules/board/domain/index.ts index a12a1e477d7..efc5293a454 100644 --- a/apps/server/src/modules/board/domain/index.ts +++ b/apps/server/src/modules/board/domain/index.ts @@ -1 +1,18 @@ -export { MediaBoardColors, MediaBoardLayoutType } from './interface'; +export * from './board-node.do'; +export * from './board-node-authorizable.do'; +export * from './board-node.factory'; +export * from './collaborative-text-editor.do'; +export * from './card.do'; +export * from './column.do'; +export * from './colum-board.do'; +export * from './drawing-element.do'; +export * from './external-tool-element.do'; +export * from './file-element.do'; +export * from './link-element.do'; +export * from './media-board'; +export * from './rich-text-element.do'; +export * from './submission-container-element.do'; +export * from './submission-item.do'; +export * from './path-utils'; +export * from './types'; +export * from './type-mapping'; diff --git a/apps/server/src/modules/board/domain/interface/index.ts b/apps/server/src/modules/board/domain/interface/index.ts deleted file mode 100644 index 221e7ecf1ab..00000000000 --- a/apps/server/src/modules/board/domain/interface/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { MediaBoardColors } from './media-colors.enum'; -export { MediaBoardLayoutType } from './layout-type.enum'; diff --git a/apps/server/src/modules/board/domain/interface/layout-type.enum.ts b/apps/server/src/modules/board/domain/interface/layout-type.enum.ts deleted file mode 100644 index 4e180586eb8..00000000000 --- a/apps/server/src/modules/board/domain/interface/layout-type.enum.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum MediaBoardLayoutType { - GRID = 'grid', - LIST = 'list', -} diff --git a/apps/server/src/modules/board/domain/link-element.do.spec.ts b/apps/server/src/modules/board/domain/link-element.do.spec.ts new file mode 100644 index 00000000000..c900cdba889 --- /dev/null +++ b/apps/server/src/modules/board/domain/link-element.do.spec.ts @@ -0,0 +1,74 @@ +import { LinkElement, isLinkElement } from './link-element.do'; +import { BoardNodeProps } from './types/board-node-props'; + +describe('LinkElement', () => { + let linkElement: LinkElement; + + const boardNodeProps: BoardNodeProps = { + id: '1', + path: '', + level: 1, + position: 1, + children: [], + createdAt: new Date(), + updatedAt: new Date(), + }; + + beforeEach(() => { + linkElement = new LinkElement({ + ...boardNodeProps, + url: 'https://example.com', + title: 'Example', + description: 'Example description', + imageUrl: 'https://example.com/image.jpg', + }); + }); + + it('should be instance of LinkElement', () => { + expect(isLinkElement(linkElement)).toBe(true); + }); + + it('should not be instance of LinkElement', () => { + expect(isLinkElement({})).toBe(false); + }); + + it('should return url', () => { + expect(linkElement.url).toBe('https://example.com'); + }); + + it('should set url', () => { + linkElement.url = 'https://newurl.com'; + expect(linkElement.url).toBe('https://newurl.com'); + }); + + it('should return title', () => { + expect(linkElement.title).toBe('Example'); + }); + + it('should set title', () => { + linkElement.title = 'New title'; + expect(linkElement.title).toBe('New title'); + }); + + it('should return description', () => { + expect(linkElement.description).toBe('Example description'); + }); + + it('should set description', () => { + linkElement.description = 'New description'; + expect(linkElement.description).toBe('New description'); + }); + + it('should return imageUrl', () => { + expect(linkElement.imageUrl).toBe('https://example.com/image.jpg'); + }); + + it('should set imageUrl', () => { + linkElement.imageUrl = 'https://newurl.com/newimage.jpg'; + expect(linkElement.imageUrl).toBe('https://newurl.com/newimage.jpg'); + }); + + it('should not have child', () => { + expect(linkElement.canHaveChild()).toBe(false); + }); +}); diff --git a/apps/server/src/modules/board/domain/link-element.do.ts b/apps/server/src/modules/board/domain/link-element.do.ts new file mode 100644 index 00000000000..dfaf1746cd2 --- /dev/null +++ b/apps/server/src/modules/board/domain/link-element.do.ts @@ -0,0 +1,42 @@ +import { BoardNode } from './board-node.do'; +import type { LinkElementProps } from './types'; + +export class LinkElement extends BoardNode { + get url(): string { + return this.props.url ?? ''; + } + + set url(value: string) { + this.props.url = value; + } + + get title(): string { + return this.props.title ?? ''; + } + + set title(value: string) { + this.props.title = value; + } + + get description(): string { + return this.props.description ?? ''; + } + + set description(value: string) { + this.props.description = value ?? ''; + } + + get imageUrl(): string { + return this.props.imageUrl ?? ''; + } + + set imageUrl(value: string) { + this.props.imageUrl = value; + } + + canHaveChild(): boolean { + return false; + } +} + +export const isLinkElement = (reference: unknown): reference is LinkElement => reference instanceof LinkElement; diff --git a/apps/server/src/shared/domain/domainobject/board/media-board/index.ts b/apps/server/src/modules/board/domain/media-board/index.ts similarity index 53% rename from apps/server/src/shared/domain/domainobject/board/media-board/index.ts rename to apps/server/src/modules/board/domain/media-board/index.ts index 1bb7df7d2b9..e2dfaaef28f 100644 --- a/apps/server/src/shared/domain/domainobject/board/media-board/index.ts +++ b/apps/server/src/modules/board/domain/media-board/index.ts @@ -1,10 +1,14 @@ -export { MediaBoard, MediaBoardProps } from './media-board.do'; -export { MediaLine, MediaLineProps, MediaLineInitProps, isMediaLine } from './media-line.do'; +export { MediaBoard, isMediaBoard } from './media-board.do'; +export { MediaBoardNodeFactory } from './media-board-node-factory'; +export { MediaLine, isMediaLine } from './media-line.do'; export { MediaExternalToolElement, - MediaExternalToolElementProps, - MediaExternalToolElementInitProps, + // TODO check this + // MediaExternalToolElementInitProps, isMediaExternalToolElement, } from './media-external-tool-element.do'; + export { MediaAvailableLine, MediaAvailableLineProps } from './media-available-line.do'; export { MediaAvailableLineElement, MediaAvailableLineElementProps } from './media-available-line-element.do'; + +export * from './types'; diff --git a/apps/server/src/shared/domain/domainobject/board/media-board/media-available-line-element.do.ts b/apps/server/src/modules/board/domain/media-board/media-available-line-element.do.ts similarity index 98% rename from apps/server/src/shared/domain/domainobject/board/media-board/media-available-line-element.do.ts rename to apps/server/src/modules/board/domain/media-board/media-available-line-element.do.ts index 6c4bed0ef7d..6d7af7d922a 100644 --- a/apps/server/src/shared/domain/domainobject/board/media-board/media-available-line-element.do.ts +++ b/apps/server/src/modules/board/domain/media-board/media-available-line-element.do.ts @@ -1,3 +1,4 @@ +// TODO export class MediaAvailableLineElement { schoolExternalToolId: string; diff --git a/apps/server/src/shared/domain/domainobject/board/media-board/media-available-line.do.ts b/apps/server/src/modules/board/domain/media-board/media-available-line.do.ts similarity index 89% rename from apps/server/src/shared/domain/domainobject/board/media-board/media-available-line.do.ts rename to apps/server/src/modules/board/domain/media-board/media-available-line.do.ts index 24b50b62848..fd6427e18f6 100644 --- a/apps/server/src/shared/domain/domainobject/board/media-board/media-available-line.do.ts +++ b/apps/server/src/modules/board/domain/media-board/media-available-line.do.ts @@ -1,6 +1,6 @@ -import { MediaBoardColors } from '@modules/board/domain'; +import { MediaBoardColors } from './types'; import { MediaAvailableLineElement } from './media-available-line-element.do'; - +// TODO export class MediaAvailableLine { elements: MediaAvailableLineElement[]; diff --git a/apps/server/src/modules/board/domain/media-board/media-board-node-factory.spec.ts b/apps/server/src/modules/board/domain/media-board/media-board-node-factory.spec.ts new file mode 100644 index 00000000000..375ca792234 --- /dev/null +++ b/apps/server/src/modules/board/domain/media-board/media-board-node-factory.spec.ts @@ -0,0 +1,47 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BoardExternalReference, BoardExternalReferenceType, BoardLayout } from '../types'; +import { MediaBoardNodeFactory } from './media-board-node-factory'; +import { MediaBoardColors } from './types'; + +describe(MediaBoardNodeFactory.name, () => { + const setup = () => { + const factory = new MediaBoardNodeFactory(); + const context: BoardExternalReference = { + id: new ObjectId().toHexString(), + type: BoardExternalReferenceType.User, + }; + const layout = BoardLayout.GRID; + const backgroundColor = MediaBoardColors.BLUE; + + return { factory, context, layout, backgroundColor }; + }; + + it('build media board', () => { + const { factory, context, layout, backgroundColor } = setup(); + + const mediaBoard = factory.buildMediaBoard({ + context, + layout, + backgroundColor, + collapsed: false, + }); + + expect(mediaBoard).toBeDefined(); + }); + + it('build media line', () => { + const { factory, backgroundColor } = setup(); + + const mediaLine = factory.buildMediaLine({ title: 'media line', backgroundColor, collapsed: true }); + + expect(mediaLine).toBeDefined(); + }); + + it('build external tool element', () => { + const { factory } = setup(); + + const toolElement = factory.buildExternalToolElement({ contextExternalToolId: new ObjectId().toHexString() }); + + expect(toolElement).toBeDefined(); + }); +}); diff --git a/apps/server/src/modules/board/domain/media-board/media-board-node-factory.ts b/apps/server/src/modules/board/domain/media-board/media-board-node-factory.ts new file mode 100644 index 00000000000..a1e2fbb5252 --- /dev/null +++ b/apps/server/src/modules/board/domain/media-board/media-board-node-factory.ts @@ -0,0 +1,48 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain/types'; + +import { ROOT_PATH } from '../path-utils'; +import { BoardExternalReference, BoardLayout, BoardNodeProps } from '../types'; +import { MediaBoard } from './media-board.do'; +import { MediaExternalToolElement } from './media-external-tool-element.do'; +import { MediaLine } from './media-line.do'; +import { MediaBoardColors } from './types'; + +@Injectable() +export class MediaBoardNodeFactory { + buildMediaBoard(props: { + context: BoardExternalReference; + layout: BoardLayout; + backgroundColor: MediaBoardColors; + collapsed: boolean; + }): MediaBoard { + const mediaBoard = new MediaBoard({ ...this.getBaseProps(), ...props }); + + return mediaBoard; + } + + buildMediaLine(props: { title: string; backgroundColor: MediaBoardColors; collapsed: boolean }): MediaLine { + const mediaLine = new MediaLine({ ...this.getBaseProps(), ...props }); + + return mediaLine; + } + + buildExternalToolElement(props: { contextExternalToolId: EntityId }): MediaExternalToolElement { + const mediaExternalToolElement = new MediaExternalToolElement({ ...this.getBaseProps(), ...props }); + + return mediaExternalToolElement; + } + + private getBaseProps(): BoardNodeProps { + return { + id: new ObjectId().toHexString(), + path: ROOT_PATH, + level: 0, + position: 0, + children: [], + createdAt: new Date(), + updatedAt: new Date(), + }; + } +} diff --git a/apps/server/src/modules/board/domain/media-board/media-board.do.ts b/apps/server/src/modules/board/domain/media-board/media-board.do.ts new file mode 100644 index 00000000000..754539b6a0f --- /dev/null +++ b/apps/server/src/modules/board/domain/media-board/media-board.do.ts @@ -0,0 +1,42 @@ +import type { BoardExternalReference, BoardLayout, MediaBoardProps } from '../types'; +import type { AnyMediaBoardNode, MediaBoardColors } from './types'; +import { MediaLine } from './media-line.do'; +import { BoardNode } from '../board-node.do'; + +export class MediaBoard extends BoardNode { + get context(): BoardExternalReference { + return this.props.context; + } + + set layout(layout: BoardLayout) { + this.props.layout = layout; + } + + get layout(): BoardLayout { + return this.props.layout; + } + + get backgroundColor(): MediaBoardColors { + return this.props.backgroundColor; + } + + set backgroundColor(backgroundColor: MediaBoardColors) { + this.props.backgroundColor = backgroundColor; + } + + get collapsed(): boolean { + return this.props.collapsed; + } + + set collapsed(collapsed: boolean) { + this.props.collapsed = collapsed; + } + + canHaveChild(childNode: AnyMediaBoardNode): boolean { + const allowed: boolean = childNode instanceof MediaLine; + + return allowed; + } +} + +export const isMediaBoard = (reference: unknown): reference is MediaBoard => reference instanceof MediaBoard; diff --git a/apps/server/src/modules/board/domain/media-board/media-external-tool-element.do.spec.ts b/apps/server/src/modules/board/domain/media-board/media-external-tool-element.do.spec.ts new file mode 100644 index 00000000000..d5e0642107b --- /dev/null +++ b/apps/server/src/modules/board/domain/media-board/media-external-tool-element.do.spec.ts @@ -0,0 +1,39 @@ +import { MediaExternalToolElement, isMediaExternalToolElement } from './media-external-tool-element.do'; +import { BoardNodeProps } from '../types'; + +describe('MediaExternalToolElement', () => { + let mediaExternalToolElement: MediaExternalToolElement; + + const boardNodeProps: BoardNodeProps = { + id: '1', + path: '', + level: 1, + position: 1, + children: [], + createdAt: new Date(), + updatedAt: new Date(), + }; + + beforeEach(() => { + mediaExternalToolElement = new MediaExternalToolElement({ + ...boardNodeProps, + contextExternalToolId: 'test-id', + }); + }); + + it('should be instance of MediaExternalToolElement', () => { + expect(isMediaExternalToolElement(mediaExternalToolElement)).toBe(true); + }); + + it('should not be instance of MediaExternalToolElement', () => { + expect(isMediaExternalToolElement({})).toBe(false); + }); + + it('should return contextExternalToolId', () => { + expect(mediaExternalToolElement.contextExternalToolId).toBe('test-id'); + }); + + it('should not have child', () => { + expect(mediaExternalToolElement.canHaveChild()).toBe(false); + }); +}); diff --git a/apps/server/src/modules/board/domain/media-board/media-external-tool-element.do.ts b/apps/server/src/modules/board/domain/media-board/media-external-tool-element.do.ts new file mode 100644 index 00000000000..68216cf4ea3 --- /dev/null +++ b/apps/server/src/modules/board/domain/media-board/media-external-tool-element.do.ts @@ -0,0 +1,18 @@ +import { EntityId } from '@shared/domain/types'; +import type { MediaExternalToolElementProps } from '../types'; +import { BoardNode } from '../board-node.do'; + +export class MediaExternalToolElement extends BoardNode { + get contextExternalToolId(): EntityId { + return this.props.contextExternalToolId; + } + + canHaveChild(): boolean { + return false; + } +} + +// export type MediaExternalToolElementInitProps = Omit; + +export const isMediaExternalToolElement = (reference: unknown): reference is MediaExternalToolElement => + reference instanceof MediaExternalToolElement; diff --git a/apps/server/src/modules/board/domain/media-board/media-line.do.ts b/apps/server/src/modules/board/domain/media-board/media-line.do.ts new file mode 100644 index 00000000000..dc3a88b46b8 --- /dev/null +++ b/apps/server/src/modules/board/domain/media-board/media-line.do.ts @@ -0,0 +1,40 @@ +import { AnyMediaBoardNode, MediaBoardColors } from './types'; +import type { MediaLineProps } from '../types'; +import { MediaExternalToolElement } from './media-external-tool-element.do'; +import { BoardNode } from '../board-node.do'; + +export class MediaLine extends BoardNode { + get title(): string { + return this.props.title; + } + + set title(title: string) { + this.props.title = title; + } + + set backgroundColor(backgroundColor: MediaBoardColors) { + this.props.backgroundColor = backgroundColor; + } + + get backgroundColor(): MediaBoardColors { + return this.props.backgroundColor; + } + + set collapsed(collapsed: boolean) { + this.props.collapsed = collapsed; + } + + get collapsed(): boolean { + return this.props.collapsed; + } + + canHaveChild(childNode: AnyMediaBoardNode): boolean { + const allowed: boolean = childNode instanceof MediaExternalToolElement; + + return allowed; + } +} + +// export type MediaLineInitProps = Omit; + +export const isMediaLine = (reference: unknown): reference is MediaLine => reference instanceof MediaLine; diff --git a/apps/server/src/modules/board/domain/media-board/types/any-media-board-node.ts b/apps/server/src/modules/board/domain/media-board/types/any-media-board-node.ts new file mode 100644 index 00000000000..7ea3d2f2c76 --- /dev/null +++ b/apps/server/src/modules/board/domain/media-board/types/any-media-board-node.ts @@ -0,0 +1,15 @@ +import type { MediaBoard } from '../media-board.do'; +import type { MediaExternalToolElement } from '../media-external-tool-element.do'; +import type { MediaLine } from '../media-line.do'; + +export type AnyMediaBoardNode = MediaBoard | MediaLine | MediaExternalToolElement; + +// TODO remove if not needed +// export type AnyMediaBoardNode = MediaExternalToolElement; +/* +export const isAnyMediaContentElement = (element: AnyMediaBoardNode): element is AnyMediaBoardNode => { + const result: boolean = element instanceof MediaExternalToolElement; + + return result; +}; +*/ diff --git a/apps/server/src/modules/board/domain/media-board/types/index.ts b/apps/server/src/modules/board/domain/media-board/types/index.ts new file mode 100644 index 00000000000..a6978b94ff8 --- /dev/null +++ b/apps/server/src/modules/board/domain/media-board/types/index.ts @@ -0,0 +1,2 @@ +export * from './any-media-board-node'; +export * from './media-colors.enum'; diff --git a/apps/server/src/modules/board/domain/interface/media-colors.enum.ts b/apps/server/src/modules/board/domain/media-board/types/media-colors.enum.ts similarity index 100% rename from apps/server/src/modules/board/domain/interface/media-colors.enum.ts rename to apps/server/src/modules/board/domain/media-board/types/media-colors.enum.ts diff --git a/apps/server/src/modules/board/domain/path-utils.ts b/apps/server/src/modules/board/domain/path-utils.ts new file mode 100644 index 00000000000..2b977a57859 --- /dev/null +++ b/apps/server/src/modules/board/domain/path-utils.ts @@ -0,0 +1,9 @@ +import { EntityId } from '@shared/domain/types'; + +export const PATH_SEPARATOR = ','; + +export const ROOT_PATH = PATH_SEPARATOR; + +export const joinPath = (path: string, id: EntityId): string => `${path}${id}${PATH_SEPARATOR}`; + +export const pathOfChildren = (props: { id: EntityId; path: string }): string => joinPath(props.path, props.id); diff --git a/apps/server/src/modules/board/domain/rich-text-element.do.spec.ts b/apps/server/src/modules/board/domain/rich-text-element.do.spec.ts new file mode 100644 index 00000000000..8996839e930 --- /dev/null +++ b/apps/server/src/modules/board/domain/rich-text-element.do.spec.ts @@ -0,0 +1,55 @@ +import { InputFormat } from '@shared/domain/types'; +import { RichTextElement, isRichTextElement } from './rich-text-element.do'; +import { BoardNodeProps } from './types'; + +describe('RichTextElement', () => { + let richTextElement: RichTextElement; + + const boardNodeProps: BoardNodeProps = { + id: '1', + path: '', + level: 1, + position: 1, + children: [], + createdAt: new Date(), + updatedAt: new Date(), + }; + + beforeEach(() => { + richTextElement = new RichTextElement({ + ...boardNodeProps, + text: 'Test text', + inputFormat: InputFormat.RICH_TEXT_CK5, + }); + }); + + it('should be instance of RichTextElement', () => { + expect(isRichTextElement(richTextElement)).toBe(true); + }); + + it('should not be instance of RichTextElement', () => { + expect(isRichTextElement({})).toBe(false); + }); + + it('should return text', () => { + expect(richTextElement.text).toBe('Test text'); + }); + + it('should set text', () => { + richTextElement.text = 'New text'; + expect(richTextElement.text).toBe('New text'); + }); + + it('should return inputFormat', () => { + expect(richTextElement.inputFormat).toBe(InputFormat.RICH_TEXT_CK5); + }); + + it('should set inputFormat', () => { + richTextElement.inputFormat = InputFormat.PLAIN_TEXT; + expect(richTextElement.inputFormat).toBe(InputFormat.PLAIN_TEXT); + }); + + it('should not have child', () => { + expect(richTextElement.canHaveChild()).toBe(false); + }); +}); diff --git a/apps/server/src/modules/board/domain/rich-text-element.do.ts b/apps/server/src/modules/board/domain/rich-text-element.do.ts new file mode 100644 index 00000000000..dae4e4704a3 --- /dev/null +++ b/apps/server/src/modules/board/domain/rich-text-element.do.ts @@ -0,0 +1,28 @@ +import { InputFormat } from '@shared/domain/types'; +import { BoardNode } from './board-node.do'; +import type { RichTextElementProps } from './types'; + +export class RichTextElement extends BoardNode { + get text(): string { + return this.props.text; + } + + set text(value: string) { + this.props.text = value; + } + + get inputFormat(): InputFormat { + return this.props.inputFormat; + } + + set inputFormat(value: InputFormat) { + this.props.inputFormat = value; + } + + canHaveChild(): boolean { + return false; + } +} + +export const isRichTextElement = (reference: unknown): reference is RichTextElement => + reference instanceof RichTextElement; diff --git a/apps/server/src/modules/board/domain/submission-container-element.do.ts b/apps/server/src/modules/board/domain/submission-container-element.do.ts new file mode 100644 index 00000000000..8b628d93787 --- /dev/null +++ b/apps/server/src/modules/board/domain/submission-container-element.do.ts @@ -0,0 +1,21 @@ +import { BoardNode } from './board-node.do'; +import type { AnyBoardNode, SubmissionContainerElementProps } from './types'; +import { SubmissionItem } from './submission-item.do'; + +export class SubmissionContainerElement extends BoardNode { + get dueDate(): Date | undefined { + return this.props.dueDate ?? undefined; + } + + set dueDate(value: Date | undefined) { + // TODO check if should be null instead of undefined + this.props.dueDate = value ?? undefined; + } + + canHaveChild(childNode: AnyBoardNode): boolean { + return childNode instanceof SubmissionItem; + } +} + +export const isSubmissionContainerElement = (reference: unknown): reference is SubmissionContainerElement => + reference instanceof SubmissionContainerElement; diff --git a/apps/server/src/modules/board/domain/submission-item.do.spec.ts b/apps/server/src/modules/board/domain/submission-item.do.spec.ts new file mode 100644 index 00000000000..df9316708bd --- /dev/null +++ b/apps/server/src/modules/board/domain/submission-item.do.spec.ts @@ -0,0 +1,87 @@ +import { SubmissionItem } from './submission-item.do'; +import { BoardNodeProps } from './types'; +import { fileElementFactory, linkElementFactory, richTextElementFactory } from '../testing'; + +describe('SubmissionItem', () => { + const boardNodeProps: BoardNodeProps = { + id: '1', + path: '', + level: 1, + position: 1, + children: [], + createdAt: new Date(), + updatedAt: new Date(), + }; + + describe('constructor', () => { + it('should create an instance of SubmissionItem', () => { + const submissionItem = new SubmissionItem({ ...boardNodeProps, completed: false, userId: '' }); + expect(submissionItem).toBeInstanceOf(SubmissionItem); + }); + }); + + describe('canHaveChild', () => { + const setup = () => { + const submissionItem = new SubmissionItem({ ...boardNodeProps, completed: false, userId: '' }); + + const linkElement = linkElementFactory.build(); + const fileElement = fileElementFactory.build(); + const richTextElement = richTextElementFactory.build(); + + return { submissionItem, linkElement, fileElement, richTextElement }; + }; + + it('should return true for RichTextElement child', () => { + const { submissionItem, richTextElement } = setup(); + + const result = submissionItem.canHaveChild(richTextElement); + + expect(result).toBe(true); + }); + + it('should return true for FileElement child', () => { + const { submissionItem, fileElement } = setup(); + + const result = submissionItem.canHaveChild(fileElement); + + expect(result).toBe(true); + }); + + it('should return false for non-RichTextElement and non-FileElement child', () => { + const { submissionItem, linkElement } = setup(); + + const result = submissionItem.canHaveChild(linkElement); + + expect(result).toBe(false); + }); + }); + + describe('completed property', () => { + it('should get completed prop', () => { + const submissionItem = new SubmissionItem({ ...boardNodeProps, completed: false, userId: '' }); + expect(submissionItem.completed).toBe(false); + }); + + it('should set completed prop', () => { + const submissionItem = new SubmissionItem({ ...boardNodeProps, completed: false, userId: '' }); + + submissionItem.completed = true; + expect(submissionItem.completed).toBe(true); + }); + }); + + describe('userId property', () => { + it('should get userId', () => { + const submissionItem = new SubmissionItem({ ...boardNodeProps, completed: false, userId: '' }); + expect(submissionItem.userId).toBe(''); + }); + + it('should set userId', () => { + const submissionItem = new SubmissionItem({ ...boardNodeProps, completed: false, userId: '' }); + + const userId = 'testUserId'; + submissionItem.userId = userId; + expect(submissionItem.userId).toBe(userId); + }); + }); +}); diff --git a/apps/server/src/modules/board/domain/submission-item.do.ts b/apps/server/src/modules/board/domain/submission-item.do.ts new file mode 100644 index 00000000000..2a36ef7c7dd --- /dev/null +++ b/apps/server/src/modules/board/domain/submission-item.do.ts @@ -0,0 +1,33 @@ +import { EntityId } from '@shared/domain/types'; +import { BoardNode } from './board-node.do'; +import { FileElement, isFileElement } from './file-element.do'; +import { isRichTextElement, RichTextElement } from './rich-text-element.do'; +import type { AnyBoardNode, SubmissionItemProps } from './types'; + +export class SubmissionItem extends BoardNode { + get completed(): boolean { + return this.props.completed ?? false; + } + + set completed(value: boolean) { + this.props.completed = value; + } + + get userId(): EntityId { + return this.props.userId ?? ''; + } + + set userId(value: EntityId) { + this.props.userId = value; + } + + canHaveChild(childNode: AnyBoardNode): boolean { + return isRichTextElement(childNode) || isFileElement(childNode); + } +} + +export const isSubmissionItem = (reference: unknown): reference is SubmissionItem => + reference instanceof SubmissionItem; + +export const isSubmissionItemContent = (element: AnyBoardNode): element is RichTextElement | FileElement => + isRichTextElement(element) || isFileElement(element); diff --git a/apps/server/src/modules/board/domain/type-mapping.spec.ts b/apps/server/src/modules/board/domain/type-mapping.spec.ts new file mode 100644 index 00000000000..55c1917f681 --- /dev/null +++ b/apps/server/src/modules/board/domain/type-mapping.spec.ts @@ -0,0 +1,52 @@ +import { getBoardNodeType, handleNonExhaustiveSwitch } from './type-mapping'; + +import { BoardNodeType } from './types/board-node-type.enum'; + +import { + cardFactory, + collaborativeTextEditorFactory, + columnFactory, + columnBoardFactory, + drawingElementFactory, + externalToolElementFactory, + fileElementFactory, + linkElementFactory, + mediaBoardFactory, + mediaExternalToolElementFactory, + mediaLineFactory, + richTextElementFactory, + submissionContainerElementFactory, + submissionItemFactory, +} from '../testing'; + +describe('getBoardNodeType', () => { + it('should return correct type for each instance', () => { + expect(getBoardNodeType(cardFactory.build())).toBe(BoardNodeType.CARD); + expect(getBoardNodeType(collaborativeTextEditorFactory.build())).toBe(BoardNodeType.COLLABORATIVE_TEXT_EDITOR); + expect(getBoardNodeType(columnFactory.build())).toBe(BoardNodeType.COLUMN); + expect(getBoardNodeType(columnBoardFactory.build())).toBe(BoardNodeType.COLUMN_BOARD); + expect(getBoardNodeType(drawingElementFactory.build())).toBe(BoardNodeType.DRAWING_ELEMENT); + expect(getBoardNodeType(externalToolElementFactory.build())).toBe(BoardNodeType.EXTERNAL_TOOL); + expect(getBoardNodeType(fileElementFactory.build())).toBe(BoardNodeType.FILE_ELEMENT); + expect(getBoardNodeType(linkElementFactory.build())).toBe(BoardNodeType.LINK_ELEMENT); + expect(getBoardNodeType(mediaBoardFactory.build())).toBe(BoardNodeType.MEDIA_BOARD); + expect(getBoardNodeType(mediaExternalToolElementFactory.build())).toBe(BoardNodeType.MEDIA_EXTERNAL_TOOL_ELEMENT); + expect(getBoardNodeType(mediaLineFactory.build())).toBe(BoardNodeType.MEDIA_LINE); + expect(getBoardNodeType(richTextElementFactory.build())).toBe(BoardNodeType.RICH_TEXT_ELEMENT); + expect(getBoardNodeType(submissionContainerElementFactory.build())).toBe( + BoardNodeType.SUBMISSION_CONTAINER_ELEMENT + ); + expect(getBoardNodeType(submissionItemFactory.build())).toBe(BoardNodeType.SUBMISSION_ITEM); + }); + + it('should throw error for unknown type', () => { + class UnknownType {} + expect(() => getBoardNodeType(new UnknownType() as any)).toThrow(); + }); +}); + +describe('handleNonExhaustiveSwitch', () => { + it('should throw error', () => { + expect(() => handleNonExhaustiveSwitch('unknown' as never)).toThrow(); + }); +}); diff --git a/apps/server/src/modules/board/domain/type-mapping.ts b/apps/server/src/modules/board/domain/type-mapping.ts new file mode 100644 index 00000000000..983e9a2d332 --- /dev/null +++ b/apps/server/src/modules/board/domain/type-mapping.ts @@ -0,0 +1,51 @@ +import { NotImplementedException } from '@nestjs/common'; +import { Card } from './card.do'; +import { CollaborativeTextEditorElement } from './collaborative-text-editor.do'; +import { ColumnBoard } from './colum-board.do'; +import { Column } from './column.do'; +import { DrawingElement } from './drawing-element.do'; +import { ExternalToolElement } from './external-tool-element.do'; +import { FileElement } from './file-element.do'; +import { LinkElement } from './link-element.do'; +import { MediaBoard, MediaExternalToolElement, MediaLine } from './media-board'; +import { RichTextElement } from './rich-text-element.do'; +import { SubmissionContainerElement } from './submission-container-element.do'; +import { SubmissionItem } from './submission-item.do'; +import type { AnyBoardNode } from './types/any-board-node'; +import { BoardNodeType } from './types/board-node-type.enum'; + +// register node types +const BoardNodeTypeToConstructor = { + [BoardNodeType.CARD]: Card, + [BoardNodeType.COLLABORATIVE_TEXT_EDITOR]: CollaborativeTextEditorElement, + [BoardNodeType.COLUMN]: Column, + [BoardNodeType.COLUMN_BOARD]: ColumnBoard, + [BoardNodeType.DRAWING_ELEMENT]: DrawingElement, + [BoardNodeType.EXTERNAL_TOOL]: ExternalToolElement, + [BoardNodeType.FILE_ELEMENT]: FileElement, + [BoardNodeType.LINK_ELEMENT]: LinkElement, + [BoardNodeType.MEDIA_BOARD]: MediaBoard, + [BoardNodeType.MEDIA_EXTERNAL_TOOL_ELEMENT]: MediaExternalToolElement, + [BoardNodeType.MEDIA_LINE]: MediaLine, + [BoardNodeType.RICH_TEXT_ELEMENT]: RichTextElement, + [BoardNodeType.SUBMISSION_CONTAINER_ELEMENT]: SubmissionContainerElement, + [BoardNodeType.SUBMISSION_ITEM]: SubmissionItem, +} as const; + +export const getBoardNodeConstructor = (type: T): typeof BoardNodeTypeToConstructor[T] => + BoardNodeTypeToConstructor[type]; + +export const getBoardNodeType = (boardNode: T): BoardNodeType => { + const type = Object.keys(BoardNodeTypeToConstructor).find((key) => { + const Constructor = BoardNodeTypeToConstructor[key as BoardNodeType]; + return boardNode instanceof Constructor; + }); + if (type === undefined) { + throw new Error(`Cannot get type of board node class '${boardNode.constructor.name}'`); + } + return type as BoardNodeType; +}; + +export const handleNonExhaustiveSwitch = (type: never): never => { + throw new NotImplementedException(`unknown board node type '${JSON.stringify(type)}'`); +}; diff --git a/apps/server/src/modules/board/domain/types/any-board-node.ts b/apps/server/src/modules/board/domain/types/any-board-node.ts new file mode 100644 index 00000000000..8d9d08a8603 --- /dev/null +++ b/apps/server/src/modules/board/domain/types/any-board-node.ts @@ -0,0 +1,16 @@ +import type { Card } from '../card.do'; +import type { Column } from '../column.do'; +import type { ColumnBoard } from '../colum-board.do'; +import type { SubmissionItem } from '../submission-item.do'; +import type { CollaborativeTextEditorElement } from '../collaborative-text-editor.do'; +import type { AnyContentElement } from './any-content-element'; +import type { AnyMediaBoardNode } from '../media-board/types/any-media-board-node'; + +export type AnyBoardNode = + | AnyContentElement + | AnyMediaBoardNode + | Card + | CollaborativeTextEditorElement + | Column + | ColumnBoard + | SubmissionItem; diff --git a/apps/server/src/modules/board/domain/types/any-content-element.ts b/apps/server/src/modules/board/domain/types/any-content-element.ts new file mode 100644 index 00000000000..1751287d35e --- /dev/null +++ b/apps/server/src/modules/board/domain/types/any-content-element.ts @@ -0,0 +1,30 @@ +import { type CollaborativeTextEditorElement, isCollaborativeTextEditorElement } from '../collaborative-text-editor.do'; +import { type DrawingElement, isDrawingElement } from '../drawing-element.do'; +import { type ExternalToolElement, isExternalToolElement } from '../external-tool-element.do'; +import { type FileElement, isFileElement } from '../file-element.do'; +import { isLinkElement, type LinkElement } from '../link-element.do'; +import { isRichTextElement, type RichTextElement } from '../rich-text-element.do'; +import { isSubmissionContainerElement, type SubmissionContainerElement } from '../submission-container-element.do'; +import { type AnyBoardNode } from './any-board-node'; + +export type AnyContentElement = + | CollaborativeTextEditorElement + | DrawingElement + | ExternalToolElement + | FileElement + | LinkElement + | RichTextElement + | SubmissionContainerElement; + +export const isContentElement = (boardNode: AnyBoardNode): boardNode is AnyContentElement => { + const result = + isCollaborativeTextEditorElement(boardNode) || + isDrawingElement(boardNode) || + isExternalToolElement(boardNode) || + isFileElement(boardNode) || + isLinkElement(boardNode) || + isRichTextElement(boardNode) || + isSubmissionContainerElement(boardNode); + + return result; +}; diff --git a/apps/server/src/shared/domain/domainobject/board/types/board-external-reference.ts b/apps/server/src/modules/board/domain/types/board-external-reference.ts similarity index 63% rename from apps/server/src/shared/domain/domainobject/board/types/board-external-reference.ts rename to apps/server/src/modules/board/domain/types/board-external-reference.ts index 4f3c11de420..5d9fd94a599 100644 --- a/apps/server/src/shared/domain/domainobject/board/types/board-external-reference.ts +++ b/apps/server/src/modules/board/domain/types/board-external-reference.ts @@ -1,8 +1,10 @@ -import { EntityId } from '@shared/domain/types'; +import type { EntityId } from '@shared/domain/types'; export enum BoardExternalReferenceType { 'Course' = 'course', 'User' = 'user', + // TODO + // 'ExternalTool' = 'external-tool', } export interface BoardExternalReference { diff --git a/apps/server/src/shared/domain/domainobject/board/types/board-layout.enum.ts b/apps/server/src/modules/board/domain/types/board-layout.enum.ts similarity index 80% rename from apps/server/src/shared/domain/domainobject/board/types/board-layout.enum.ts rename to apps/server/src/modules/board/domain/types/board-layout.enum.ts index 9d41bde2592..afcc059e330 100644 --- a/apps/server/src/shared/domain/domainobject/board/types/board-layout.enum.ts +++ b/apps/server/src/modules/board/domain/types/board-layout.enum.ts @@ -1,4 +1,5 @@ export enum BoardLayout { COLUMNS = 'columns', LIST = 'list', + GRID = 'grid', } diff --git a/apps/server/src/modules/board/domain/types/board-node-props.ts b/apps/server/src/modules/board/domain/types/board-node-props.ts new file mode 100644 index 00000000000..a89dab6a802 --- /dev/null +++ b/apps/server/src/modules/board/domain/types/board-node-props.ts @@ -0,0 +1,100 @@ +import type { EntityId, InputFormat } from '@shared/domain/types'; +import type { AnyBoardNode } from './any-board-node'; +import type { BoardExternalReference } from './board-external-reference'; +import { BoardLayout } from './board-layout.enum'; +import type { MediaBoardColors } from '../media-board'; + +export interface BoardNodeProps { + id: EntityId; + path: string; + level: number; + position: number; + children: AnyBoardNode[]; + createdAt: Date; + updatedAt: Date; +} + +export interface ColumnBoardProps extends BoardNodeProps { + title: string; + context: BoardExternalReference; + isVisible: boolean; + layout: BoardLayout; +} + +export interface ColumnProps extends BoardNodeProps { + title?: string; +} + +export interface CardProps extends BoardNodeProps { + title?: string; + height: number; +} + +export interface CollaborativeTextEditorElementProps extends BoardNodeProps {} + +export interface DrawingElementProps extends BoardNodeProps { + description: string; +} + +export interface ExternalToolElementProps extends BoardNodeProps { + contextExternalToolId?: string; +} + +export interface FileElementProps extends BoardNodeProps { + alternativeText?: string; + caption?: string; +} +export interface LinkElementProps extends BoardNodeProps { + title: string; + url: string; + description?: string; + imageUrl?: string; +} + +export interface RichTextElementProps extends BoardNodeProps { + text: string; + inputFormat: InputFormat; +} + +export interface SubmissionContainerElementProps extends BoardNodeProps { + dueDate?: Date; +} + +export interface SubmissionItemProps extends BoardNodeProps { + completed: boolean; + userId: EntityId; +} + +export interface MediaBoardProps extends BoardNodeProps { + context: BoardExternalReference; + backgroundColor: MediaBoardColors; + collapsed: boolean; + layout: BoardLayout; +} + +// TODO use only one interface for media-external-tool and external-tool +export interface MediaExternalToolElementProps extends BoardNodeProps { + contextExternalToolId: string; +} + +export interface MediaLineProps extends BoardNodeProps { + backgroundColor: MediaBoardColors; + collapsed: boolean; + title: string; +} + +export type MediaBoardNodeProps = MediaBoardProps | MediaExternalToolElementProps | MediaLineProps; + +export type AnyBoardNodeProps = + | CardProps + | CollaborativeTextEditorElementProps + | ColumnBoardProps + | ColumnProps + | DrawingElementProps + | ExternalToolElementProps + | FileElementProps + | LinkElementProps + | RichTextElementProps + | SubmissionContainerElementProps + | SubmissionItemProps + | MediaBoardNodeProps; diff --git a/apps/server/src/shared/domain/entity/boardnode/types/board-node-type.ts b/apps/server/src/modules/board/domain/types/board-node-type.enum.ts similarity index 100% rename from apps/server/src/shared/domain/entity/boardnode/types/board-node-type.ts rename to apps/server/src/modules/board/domain/types/board-node-type.enum.ts diff --git a/apps/server/src/shared/domain/domainobject/board/types/content-elements.enum.ts b/apps/server/src/modules/board/domain/types/content-element-type.enum.ts similarity index 100% rename from apps/server/src/shared/domain/domainobject/board/types/content-elements.enum.ts rename to apps/server/src/modules/board/domain/types/content-element-type.enum.ts diff --git a/apps/server/src/modules/board/domain/types/index.ts b/apps/server/src/modules/board/domain/types/index.ts new file mode 100644 index 00000000000..32854076180 --- /dev/null +++ b/apps/server/src/modules/board/domain/types/index.ts @@ -0,0 +1,7 @@ +export * from './any-board-node'; +export * from './any-content-element'; +export * from './board-external-reference'; +export * from './board-layout.enum'; +export * from './board-node-props'; +export * from './board-node-type.enum'; +export * from './content-element-type.enum'; diff --git a/apps/server/src/modules/board/gateway/api-test/board-collaboration.gateway.spec.ts b/apps/server/src/modules/board/gateway/api-test/board-collaboration.gateway.spec.ts index 206579c57fc..1d35a9d10c8 100644 --- a/apps/server/src/modules/board/gateway/api-test/board-collaboration.gateway.spec.ts +++ b/apps/server/src/modules/board/gateway/api-test/board-collaboration.gateway.spec.ts @@ -3,19 +3,17 @@ import { INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { MongoIoAdapter } from '@infra/socketio'; -import { BoardExternalReferenceType, CardProps, ContentElementType } from '@shared/domain/domainobject'; import { InputFormat } from '@shared/domain/types'; -import { - cardNodeFactory, - cleanupCollections, - columnBoardNodeFactory, - columnNodeFactory, - courseFactory, - richTextElementNodeFactory, - userFactory, -} from '@shared/testing'; +import { cleanupCollections, courseFactory, userFactory } from '@shared/testing'; import { getSocketApiClient, waitForEvent } from '@shared/testing/test-socket-api-client'; import { Socket } from 'socket.io-client'; +import { + cardEntityFactory, + columnBoardEntityFactory, + columnEntityFactory, + richTextElementEntityFactory, +} from '../../testing'; +import { BoardExternalReferenceType, CardProps, ContentElementType } from '../../domain'; import { BoardCollaborationTestingModule } from '../../board-collaboration.testing.module'; import { BoardCollaborationGateway } from '../board-collaboration.gateway'; @@ -59,15 +57,18 @@ describe(BoardCollaborationGateway.name, () => { ioClient = await getSocketApiClient(app, user); unauthorizedIoClient = await getSocketApiClient(app, unauthorizedUser); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.buildWithId({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); - const columnNode2 = columnNodeFactory.buildWithId({ parent: columnBoardNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); + const columnNode2 = columnEntityFactory.withParent(columnBoardNode).build(); - const cardNodes = cardNodeFactory.buildListWithId(2, { parent: columnNode }); - const elementNodes = richTextElementNodeFactory.buildListWithId(3, { parent: cardNodes[0] }); + const cardNodes = [ + cardEntityFactory.withParent(columnNode).build(), + cardEntityFactory.withParent(columnNode).build(), + ]; + const elementNodes = richTextElementEntityFactory.withParent(cardNodes[0]).buildList(3); await em.persistAndFlush([columnBoardNode, columnNode, columnNode2, ...cardNodes, ...elementNodes]); @@ -486,7 +487,9 @@ describe(BoardCollaborationGateway.name, () => { }); describe('when an error is thrown', () => { - it('should answer with failure', async () => { + // the error cannot be provoked easily anymore because passing a column id + // ignores the id now + it.skip('should answer with failure', async () => { const { cardNodes, columnNode } = await setup(); // passing a column id instead of a card id to force an error diff --git a/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts b/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts index dac4ed834fb..a9abebe4625 100644 --- a/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts +++ b/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts @@ -2,10 +2,14 @@ import { WsValidationPipe, Socket } from '@infra/socketio'; import { MikroORM, UseRequestContext } from '@mikro-orm/core'; import { UseGuards, UsePipes } from '@nestjs/common'; import { SubscribeMessage, WebSocketGateway, WsException } from '@nestjs/websockets'; -import { WsJwtAuthGuard } from '@src/modules/authentication/guard/ws-jwt-auth.guard'; -import { BoardResponseMapper, CardResponseMapper, ContentElementResponseFactory } from '../controller/mapper'; -import { ColumnResponseMapper } from '../controller/mapper/column-response.mapper'; -import { BoardDoAuthorizableService } from '../service'; +import { WsJwtAuthGuard } from '@modules/authentication/guard/ws-jwt-auth.guard'; +import { + BoardResponseMapper, + CardResponseMapper, + ColumnResponseMapper, + ContentElementResponseFactory, +} from '../controller/mapper'; +import { BoardNodeAuthorizableService } from '../service'; import { BoardUc, CardUc, ColumnUc, ElementUc } from '../uc'; import { CreateCardMessageParams, @@ -39,7 +43,7 @@ export class BoardCollaborationGateway { private readonly columnUc: ColumnUc, private readonly cardUc: CardUc, private readonly elementUc: ElementUc, - private readonly authorizableService: BoardDoAuthorizableService // to be removed + private readonly authorizableService: BoardNodeAuthorizableService // to be removed ) {} private getCurrentUser(socket: Socket) { @@ -125,9 +129,11 @@ export class BoardCollaborationGateway { const { userId } = this.getCurrentUser(socket); try { const card = await this.columnUc.createCard(userId, data.columnId); + const newCard = CardResponseMapper.mapToResponse(card); + const responsePayload = { ...data, - newCard: card.getProps(), + newCard, }; await emitter.emitToClientAndRoom(responsePayload); @@ -341,7 +347,7 @@ export class BoardCollaborationGateway { private async getRootIdForId(id: string) { const authorizable = await this.authorizableService.findById(id); - const rootId = authorizable.rootDo.id; + const rootId = authorizable.rootNode.id; return rootId; } diff --git a/apps/server/src/modules/board/gateway/dto/create-content-element.message.param.ts b/apps/server/src/modules/board/gateway/dto/create-content-element.message.param.ts index 70885d03146..6db0073bf63 100644 --- a/apps/server/src/modules/board/gateway/dto/create-content-element.message.param.ts +++ b/apps/server/src/modules/board/gateway/dto/create-content-element.message.param.ts @@ -1,5 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { ContentElementType } from '@shared/domain/domainobject'; +import { ContentElementType } from '@modules/board/domain'; import { IsEnum, IsInt, IsMongoId, IsOptional, Min } from 'class-validator'; export class CreateContentElementMessageParams { diff --git a/apps/server/src/modules/board/index.ts b/apps/server/src/modules/board/index.ts index 151284bf10b..1148ed77b61 100644 --- a/apps/server/src/modules/board/index.ts +++ b/apps/server/src/modules/board/index.ts @@ -1,8 +1,29 @@ export { BoardModule } from './board.module'; -export * from './service/board-do-authorizable.service'; -export * from './service/card.service'; -export * from './service/column-board.service'; -export * from './service/column.service'; -export * from './service/content-element.service'; -export * from './service/column-board-copy.service'; export { BoardConfig } from './board.config'; +export { + BoardNode, + BoardNodeAuthorizable, + BoardExternalReferenceType, + BoardExternalReference, + BoardLayout, + BoardNodeFactory, + ColumnBoard, + // @modules/authorization/domain/rules/board-node.rule.ts + BoardRoles, + isDrawingElement, + isSubmissionItem, + isSubmissionItemContent, + SubmissionItem, + UserWithBoardRoles, + // @modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-name.strategy.ts + MediaBoard, +} from './domain'; + +export { + BoardNodeAuthorizableService, + BoardNodeService, + BoardCommonToolService, + ColumnBoardService, + MediaAvailableLineService, + MediaBoardService, +} from './service'; diff --git a/apps/server/src/modules/board/loggable/media-board/invalid-board-type.loggable-exception.spec.ts b/apps/server/src/modules/board/loggable/media-board/invalid-board-type.loggable-exception.spec.ts index 1be206d469c..0d7c29e2779 100644 --- a/apps/server/src/modules/board/loggable/media-board/invalid-board-type.loggable-exception.spec.ts +++ b/apps/server/src/modules/board/loggable/media-board/invalid-board-type.loggable-exception.spec.ts @@ -1,4 +1,4 @@ -import { MediaBoard } from '@shared/domain/domainobject'; +import { MediaBoard } from '../../domain'; import { InvalidBoardTypeLoggableException } from './invalid-board-type.loggable-exception'; describe(InvalidBoardTypeLoggableException.name, () => { diff --git a/apps/server/src/modules/board/loggable/media-board/invalid-board-type.loggable-exception.ts b/apps/server/src/modules/board/loggable/media-board/invalid-board-type.loggable-exception.ts index 3619148cec5..acfa7dc4330 100644 --- a/apps/server/src/modules/board/loggable/media-board/invalid-board-type.loggable-exception.ts +++ b/apps/server/src/modules/board/loggable/media-board/invalid-board-type.loggable-exception.ts @@ -1,7 +1,7 @@ import { BadRequestException } from '@nestjs/common'; -import { ColumnBoard, MediaBoard } from '@shared/domain/domainobject'; import { EntityId } from '@shared/domain/types'; import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; +import { ColumnBoard, MediaBoard } from '../../domain'; export class InvalidBoardTypeLoggableException extends BadRequestException implements Loggable { constructor( diff --git a/apps/server/src/modules/board/media-board-api.module.ts b/apps/server/src/modules/board/media-board-api.module.ts index 84467581744..0f41b134cb4 100644 --- a/apps/server/src/modules/board/media-board-api.module.ts +++ b/apps/server/src/modules/board/media-board-api.module.ts @@ -1,16 +1,24 @@ import { AuthorizationModule } from '@modules/authorization'; import { UserLicenseModule } from '@modules/user-license'; -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; import { ToolModule } from '../tool'; import { BoardModule } from './board.module'; import { MediaBoardController, MediaElementController, MediaLineController } from './controller'; import { MediaBoardModule } from './media-board.module'; import { MediaAvailableLineUc, MediaBoardUc, MediaElementUc, MediaLineUc } from './uc'; +import { BoardNodePermissionService } from './service'; @Module({ - imports: [BoardModule, LoggerModule, AuthorizationModule, MediaBoardModule, ToolModule, UserLicenseModule], + imports: [ + BoardModule, + LoggerModule, + forwardRef(() => AuthorizationModule), + MediaBoardModule, + ToolModule, + UserLicenseModule, + ], controllers: [MediaBoardController, MediaLineController, MediaElementController], - providers: [MediaBoardUc, MediaLineUc, MediaElementUc, MediaAvailableLineUc], + providers: [BoardNodePermissionService, MediaBoardUc, MediaLineUc, MediaElementUc, MediaAvailableLineUc], }) export class MediaBoardApiModule {} diff --git a/apps/server/src/modules/board/media-board.module.ts b/apps/server/src/modules/board/media-board.module.ts index 81479119f97..1dc63164b7e 100644 --- a/apps/server/src/modules/board/media-board.module.ts +++ b/apps/server/src/modules/board/media-board.module.ts @@ -1,10 +1,12 @@ import { ToolModule } from '@modules/tool'; import { Module } from '@nestjs/common'; -import { MediaAvailableLineService } from './service'; +import { MediaBoardNodeFactory } from './domain'; +import { BoardNodeRepo } from './repo'; +import { MediaBoardService, MediaAvailableLineService } from './service'; @Module({ imports: [ToolModule], - providers: [MediaAvailableLineService], - exports: [MediaAvailableLineService], + providers: [BoardNodeRepo, MediaBoardNodeFactory, MediaBoardService, MediaAvailableLineService], + exports: [MediaBoardNodeFactory, MediaBoardService, MediaAvailableLineService], }) export class MediaBoardModule {} diff --git a/apps/server/src/modules/board/repo/board-do.builder-impl.spec.ts b/apps/server/src/modules/board/repo/board-do.builder-impl.spec.ts deleted file mode 100644 index 1e86ec1b408..00000000000 --- a/apps/server/src/modules/board/repo/board-do.builder-impl.spec.ts +++ /dev/null @@ -1,410 +0,0 @@ -import { - ExternalToolElement, - LinkElement, - MediaBoard, - MediaExternalToolElement, - MediaLine, -} from '@shared/domain/domainobject'; -import { BoardNodeType } from '@shared/domain/entity'; -import { - cardNodeFactory, - columnBoardNodeFactory, - columnNodeFactory, - externalToolElementNodeFactory, - fileElementNodeFactory, - linkElementNodeFactory, - mediaBoardNodeFactory, - mediaExternalToolElementNodeFactory, - mediaLineNodeFactory, - richTextElementNodeFactory, - setupEntities, - submissionContainerElementNodeFactory, -} from '@shared/testing'; -import { drawingElementNodeFactory } from '@shared/testing/factory/boardnode/drawing-element-node.factory'; -import { BoardDoBuilderImpl } from './board-do.builder-impl'; - -describe(BoardDoBuilderImpl.name, () => { - beforeAll(async () => { - await setupEntities(); - }); - - describe('when building a column board', () => { - it('should work without descendants', () => { - const columnBoardNode = columnBoardNodeFactory.build(); - - const domainObject = new BoardDoBuilderImpl().buildColumnBoard(columnBoardNode); - - expect(domainObject.constructor.name).toBe('ColumnBoard'); - }); - - it('should throw error with wrong type of children', () => { - const columnBoardNode1 = columnBoardNodeFactory.buildWithId(); - const columnBoardNode2 = columnBoardNodeFactory.buildWithId({ parent: columnBoardNode1 }); - - expect(() => { - new BoardDoBuilderImpl([columnBoardNode2]).buildColumnBoard(columnBoardNode1); - }).toThrowError(); - }); - - it('should assign the children', () => { - const columnBoardNode = columnBoardNodeFactory.buildWithId(); - const columnNode1 = columnNodeFactory.buildWithId({ parent: columnBoardNode }); - const columnNode2 = columnNodeFactory.buildWithId({ parent: columnBoardNode }); - - const domainObject = new BoardDoBuilderImpl([columnNode1, columnNode2]).buildColumnBoard(columnBoardNode); - - expect(domainObject.children.map((el) => el.id).sort()).toEqual([columnNode1.id, columnNode2.id]); - }); - - it('should sort the children by their node position', () => { - const columnBoardNode = columnBoardNodeFactory.buildWithId(); - const columnNode1 = columnNodeFactory.buildWithId({ parent: columnBoardNode, position: 3 }); - const columnNode2 = columnNodeFactory.buildWithId({ parent: columnBoardNode, position: 2 }); - const columnNode3 = columnNodeFactory.buildWithId({ parent: columnBoardNode, position: 1 }); - - const domainObject = new BoardDoBuilderImpl([columnNode1, columnNode2, columnNode3]).buildColumnBoard( - columnBoardNode - ); - - const elementIds = domainObject.children.map((el) => el.id); - expect(elementIds).toEqual([columnNode3.id, columnNode2.id, columnNode1.id]); - }); - - it('should be able to use the builder', () => { - const columnBoardNode = columnBoardNodeFactory.buildWithId(); - const builder = new BoardDoBuilderImpl(); - const domainObject = columnBoardNode.useDoBuilder(builder); - expect(domainObject.id).toEqual(columnBoardNode.id); - }); - }); - - describe('when building a column', () => { - it('should work without descendants', () => { - const columnNode = columnNodeFactory.build(); - - const domainObject = new BoardDoBuilderImpl().buildColumn(columnNode); - - expect(domainObject.constructor.name).toBe('Column'); - }); - - it('should throw error with wrong type of children', () => { - const columnNode1 = columnNodeFactory.buildWithId(); - const columnNode2 = columnNodeFactory.buildWithId({ parent: columnNode1 }); - - expect(() => { - new BoardDoBuilderImpl([columnNode2]).buildColumn(columnNode1); - }).toThrowError(); - }); - - it('should assign the children', () => { - const columnNode = columnNodeFactory.buildWithId(); - const cardNode1 = cardNodeFactory.buildWithId({ parent: columnNode }); - const cardNode2 = cardNodeFactory.buildWithId({ parent: columnNode }); - - const domainObject = new BoardDoBuilderImpl([cardNode1, cardNode2]).buildColumn(columnNode); - - expect(domainObject.children.map((el) => el.id).sort()).toEqual([cardNode1.id, cardNode2.id]); - }); - - it('should sort the children by their node position', () => { - const columnNode = columnNodeFactory.buildWithId(); - const cardNode1 = cardNodeFactory.buildWithId({ parent: columnNode, position: 3 }); - const cardNode2 = cardNodeFactory.buildWithId({ parent: columnNode, position: 2 }); - const cardNode3 = cardNodeFactory.buildWithId({ parent: columnNode, position: 1 }); - - const domainObject = new BoardDoBuilderImpl([cardNode1, cardNode2, cardNode3]).buildColumn(columnNode); - - const cardIds = domainObject.children.map((el) => el.id); - expect(cardIds).toEqual([cardNode3.id, cardNode2.id, cardNode1.id]); - }); - }); - - describe('when building a card', () => { - it('should work without descendants', () => { - const cardNode = cardNodeFactory.build(); - - const domainObject = new BoardDoBuilderImpl().buildCard(cardNode); - - expect(domainObject.constructor.name).toBe('Card'); - }); - - it('should throw error with wrong type of children', () => { - const cardNode = cardNodeFactory.buildWithId(); - const columnNode = columnNodeFactory.buildWithId({ parent: cardNode }); - - expect(() => { - new BoardDoBuilderImpl([columnNode]).buildCard(cardNode); - }).toThrowError(); - }); - - it('should assign the children', () => { - const cardNode = cardNodeFactory.buildWithId(); - const elementNode1 = richTextElementNodeFactory.buildWithId({ parent: cardNode }); - const elementNode2 = richTextElementNodeFactory.buildWithId({ parent: cardNode }); - - const domainObject = new BoardDoBuilderImpl([elementNode1, elementNode2]).buildCard(cardNode); - - expect(domainObject.children.map((el) => el.id).sort()).toEqual([elementNode1.id, elementNode2.id]); - }); - - it('should sort the children by their node position', () => { - const cardNode = cardNodeFactory.buildWithId(); - const elementNode1 = richTextElementNodeFactory.buildWithId({ parent: cardNode, position: 2 }); - const elementNode2 = richTextElementNodeFactory.buildWithId({ parent: cardNode, position: 3 }); - const elementNode3 = richTextElementNodeFactory.buildWithId({ parent: cardNode, position: 1 }); - - const domainObject = new BoardDoBuilderImpl([elementNode1, elementNode2, elementNode3]).buildCard(cardNode); - - const elementIds = domainObject.children.map((el) => el.id); - expect(elementIds).toEqual([elementNode3.id, elementNode1.id, elementNode2.id]); - }); - }); - - describe('when building a rich text element', () => { - it('should work without descendants', () => { - const richTextElementNode = richTextElementNodeFactory.build(); - - const domainObject = new BoardDoBuilderImpl().buildRichTextElement(richTextElementNode); - - expect(domainObject.constructor.name).toBe('RichTextElement'); - }); - - it('should throw error if richTextElement is not a leaf', () => { - const richTextElementNode = richTextElementNodeFactory.buildWithId(); - const columnNode = columnNodeFactory.buildWithId({ parent: richTextElementNode }); - - expect(() => { - new BoardDoBuilderImpl([columnNode]).buildRichTextElement(richTextElementNode); - }).toThrowError(); - }); - }); - - describe('when building a drawing element', () => { - it('should work without descendants', () => { - const drawingElementNode = drawingElementNodeFactory.build(); - - const domainObject = new BoardDoBuilderImpl().buildDrawingElement(drawingElementNode); - - expect(domainObject.constructor.name).toBe('DrawingElement'); - }); - - it('should throw error if drawingElement is not a leaf', () => { - const drawingElementNode = drawingElementNodeFactory.buildWithId(); - const columnNode = columnNodeFactory.buildWithId({ parent: drawingElementNode }); - - expect(() => { - new BoardDoBuilderImpl([columnNode]).buildDrawingElement(drawingElementNode); - }).toThrowError(); - }); - }); - - describe('when building a submission container element', () => { - it('should work without descendants', () => { - const submissionContainerElementNode = submissionContainerElementNodeFactory.build(); - - const domainObject = new BoardDoBuilderImpl().buildSubmissionContainerElement(submissionContainerElementNode); - - expect(domainObject.constructor.name).toBe('SubmissionContainerElement'); - }); - - it('should throw error if submissionContainerElement is not a leaf', () => { - const submissionContainerElementNode = submissionContainerElementNodeFactory.buildWithId(); - const columnNode = columnNodeFactory.buildWithId({ parent: submissionContainerElementNode }); - - expect(() => { - new BoardDoBuilderImpl([columnNode]).buildSubmissionContainerElement(submissionContainerElementNode); - }).toThrowError(); - }); - }); - - describe('when building a external tool element', () => { - it('should work without descendants', () => { - const externalToolElementNode = externalToolElementNodeFactory.build(); - - const domainObject = new BoardDoBuilderImpl().buildExternalToolElement(externalToolElementNode); - - expect(domainObject.constructor.name).toBe(ExternalToolElement.name); - }); - - it('should throw error if externalToolElement is not a leaf', () => { - const externalToolElementNode = externalToolElementNodeFactory.buildWithId(); - const columnNode = columnNodeFactory.buildWithId({ parent: externalToolElementNode }); - - expect(() => { - new BoardDoBuilderImpl([columnNode]).buildExternalToolElement(externalToolElementNode); - }).toThrowError(); - }); - }); - - describe('when building a link element', () => { - it('should work without descendants', () => { - const linkElementNode = linkElementNodeFactory.buildWithId(); - - const domainObject = new BoardDoBuilderImpl().buildLinkElement(linkElementNode); - - expect(domainObject.constructor.name).toBe(LinkElement.name); - }); - - it('should throw error if linkElement is not a leaf', () => { - const linkElementNode = linkElementNodeFactory.buildWithId(); - const columnNode = columnNodeFactory.buildWithId({ parent: linkElementNode }); - - expect(() => { - new BoardDoBuilderImpl([columnNode]).buildLinkElement(linkElementNode); - }).toThrowError(); - }); - }); - - describe('when building a media board', () => { - it('should work without descendants', () => { - const mediaBoardNode = mediaBoardNodeFactory.build(); - - const domainObject = new BoardDoBuilderImpl().buildMediaBoard(mediaBoardNode); - - expect(domainObject.constructor.name).toBe(MediaBoard.name); - }); - - it('should throw error with wrong type of children', () => { - const mediaBoardNode1 = mediaBoardNodeFactory.buildWithId(); - const mediaBoardNode2 = mediaBoardNodeFactory.buildWithId({ parent: mediaBoardNode1 }); - - expect(() => { - new BoardDoBuilderImpl([mediaBoardNode2]).buildMediaBoard(mediaBoardNode1); - }).toThrowError(); - }); - - it('should assign the children', () => { - const mediaBoardNode = mediaBoardNodeFactory.buildWithId(); - const lineNode1 = mediaLineNodeFactory.buildWithId({ parent: mediaBoardNode }); - const lineNode2 = mediaLineNodeFactory.buildWithId({ parent: mediaBoardNode }); - - const domainObject = new BoardDoBuilderImpl([lineNode1, lineNode2]).buildMediaBoard(mediaBoardNode); - - expect(domainObject.children.map((el) => el.id).sort()).toEqual([lineNode1.id, lineNode2.id]); - }); - - it('should sort the children by their node position', () => { - const mediaBoardNode = mediaBoardNodeFactory.buildWithId(); - const lineNode1 = mediaLineNodeFactory.buildWithId({ parent: mediaBoardNode, position: 3 }); - const lineNode2 = mediaLineNodeFactory.buildWithId({ parent: mediaBoardNode, position: 2 }); - const lineNode3 = mediaLineNodeFactory.buildWithId({ parent: mediaBoardNode, position: 1 }); - - const domainObject = new BoardDoBuilderImpl([lineNode1, lineNode2, lineNode3]).buildMediaBoard(mediaBoardNode); - - const elementIds = domainObject.children.map((el) => el.id); - expect(elementIds).toEqual([lineNode3.id, lineNode2.id, lineNode1.id]); - }); - - it('should be able to use the builder', () => { - const mediaBoardNode = mediaBoardNodeFactory.buildWithId(); - const builder = new BoardDoBuilderImpl(); - const domainObject = mediaBoardNode.useDoBuilder(builder); - expect(domainObject.id).toEqual(mediaBoardNode.id); - }); - }); - - describe('when building a media line', () => { - it('should work without descendants', () => { - const columnNode = mediaLineNodeFactory.build(); - - const domainObject = new BoardDoBuilderImpl().buildMediaLine(columnNode); - - expect(domainObject.constructor.name).toBe(MediaLine.name); - }); - - it('should throw error with wrong type of children', () => { - const lineNode1 = mediaLineNodeFactory.buildWithId(); - const lineNode2 = mediaLineNodeFactory.buildWithId({ parent: lineNode1 }); - - expect(() => { - new BoardDoBuilderImpl([lineNode2]).buildMediaLine(lineNode1); - }).toThrowError(); - }); - - it('should assign the children', () => { - const lineNode = mediaLineNodeFactory.buildWithId(); - const elementNode1 = mediaExternalToolElementNodeFactory.buildWithId({ parent: lineNode }); - const elementNode2 = mediaExternalToolElementNodeFactory.buildWithId({ parent: lineNode }); - - const domainObject = new BoardDoBuilderImpl([elementNode1, elementNode2]).buildMediaLine(lineNode); - - expect(domainObject.children.map((el) => el.id).sort()).toEqual([elementNode1.id, elementNode2.id]); - }); - - it('should sort the children by their node position', () => { - const lineNode = mediaLineNodeFactory.buildWithId(); - const elementNode1 = mediaExternalToolElementNodeFactory.buildWithId({ parent: lineNode, position: 3 }); - const elementNode2 = mediaExternalToolElementNodeFactory.buildWithId({ parent: lineNode, position: 2 }); - const elementNode3 = mediaExternalToolElementNodeFactory.buildWithId({ parent: lineNode, position: 1 }); - - const domainObject = new BoardDoBuilderImpl([elementNode1, elementNode2, elementNode3]).buildMediaLine(lineNode); - - const cardIds = domainObject.children.map((el) => el.id); - expect(cardIds).toEqual([elementNode3.id, elementNode2.id, elementNode1.id]); - }); - }); - - describe('when building a media external tool element', () => { - it('should work without descendants', () => { - const mediaExternalToolElementNode = mediaExternalToolElementNodeFactory.build(); - - const domainObject = new BoardDoBuilderImpl().buildMediaExternalToolElement(mediaExternalToolElementNode); - - expect(domainObject.constructor.name).toBe(MediaExternalToolElement.name); - }); - - it('should throw error if externalToolElement is not a leaf', () => { - const mediaExternalToolElementNode = mediaExternalToolElementNodeFactory.buildWithId(); - const columnNode = columnNodeFactory.buildWithId({ parent: mediaExternalToolElementNode }); - - expect(() => { - new BoardDoBuilderImpl([columnNode]).buildMediaExternalToolElement(mediaExternalToolElementNode); - }).toThrowError(); - }); - }); - - describe('ensure board node types', () => { - it('should do nothing if type is correct', () => { - const card = cardNodeFactory.build(); - expect(() => new BoardDoBuilderImpl().ensureBoardNodeType(card, BoardNodeType.CARD)).not.toThrowError(); - }); - - it('should do nothing if one of the types is correct', () => { - const card = cardNodeFactory.build(); - expect(() => - new BoardDoBuilderImpl().ensureBoardNodeType(card, [BoardNodeType.COLUMN, BoardNodeType.CARD]) - ).not.toThrowError(); - }); - - it('should throw error if wrong type', () => { - const card = cardNodeFactory.build(); - expect(() => new BoardDoBuilderImpl().ensureBoardNodeType(card, BoardNodeType.COLUMN)).toThrowError(); - }); - - it('should throw error if one of multiple board nodes has the wrong type', () => { - const column = columnNodeFactory.build(); - const card = cardNodeFactory.build(); - expect(() => new BoardDoBuilderImpl().ensureBoardNodeType([card, column], BoardNodeType.COLUMN)).toThrowError(); - }); - }); - - it('should delegate to the board node', () => { - const richTextElementNode = richTextElementNodeFactory.build(); - jest.spyOn(richTextElementNode, 'useDoBuilder'); - - const builder = new BoardDoBuilderImpl(); - builder.buildDomainObject(richTextElementNode); - - expect(richTextElementNode.useDoBuilder).toHaveBeenCalledWith(builder); - }); - - it('should delegate to the board node', () => { - const fileElementNode = fileElementNodeFactory.build(); - jest.spyOn(fileElementNode, 'useDoBuilder'); - - const builder = new BoardDoBuilderImpl(); - builder.buildDomainObject(fileElementNode); - - expect(fileElementNode.useDoBuilder).toHaveBeenCalledWith(builder); - }); -}); diff --git a/apps/server/src/modules/board/repo/board-do.builder-impl.ts b/apps/server/src/modules/board/repo/board-do.builder-impl.ts deleted file mode 100644 index 96781b7c41e..00000000000 --- a/apps/server/src/modules/board/repo/board-do.builder-impl.ts +++ /dev/null @@ -1,310 +0,0 @@ -import { NotImplementedException } from '@nestjs/common'; -import { - AnyBoardDo, - Card, - CollaborativeTextEditorElement, - Column, - ColumnBoard, - DrawingElement, - ExternalToolElement, - FileElement, - LinkElement, - MediaBoard, - MediaExternalToolElement, - MediaLine, - RichTextElement, - SubmissionContainerElement, - SubmissionItem, -} from '@shared/domain/domainobject'; -import { - type BoardDoBuilder, - type BoardNode, - BoardNodeType, - type CardNode, - type CollaborativeTextEditorElementNode, - type ColumnBoardNode, - type ColumnNode, - type DrawingElementNode, - type ExternalToolElementNodeEntity, - type FileElementNode, - type LinkElementNode, - type MediaBoardNode, - type MediaExternalToolElementNode, - type MediaLineNode, - type RichTextElementNode, - type SubmissionContainerElementNode, - type SubmissionItemNode, -} from '@shared/domain/entity'; - -export class BoardDoBuilderImpl implements BoardDoBuilder { - private childrenMap: Record = {}; - - constructor(descendants: BoardNode[] = []) { - for (const boardNode of descendants) { - this.childrenMap[boardNode.path] ||= []; - this.childrenMap[boardNode.path].push(boardNode); - } - } - - public buildDomainObject(boardNode: BoardNode): T { - return boardNode.useDoBuilder(this) as T; - } - - public buildColumnBoard(boardNode: ColumnBoardNode): ColumnBoard { - this.ensureBoardNodeType(this.getChildren(boardNode), BoardNodeType.COLUMN); - - const columns = this.buildChildren(boardNode); - - const columnBoard = new ColumnBoard({ - id: boardNode.id, - title: boardNode.title ?? '', - children: columns, - createdAt: boardNode.createdAt, - updatedAt: boardNode.updatedAt, - context: boardNode.context, - isVisible: boardNode.isVisible ?? false, - layout: boardNode.layout, - }); - - return columnBoard; - } - - public buildColumn(boardNode: ColumnNode): Column { - this.ensureBoardNodeType(this.getChildren(boardNode), BoardNodeType.CARD); - - const cards = this.buildChildren(boardNode); - - const column = new Column({ - id: boardNode.id, - title: boardNode.title ?? '', - children: cards, - createdAt: boardNode.createdAt, - updatedAt: boardNode.updatedAt, - }); - return column; - } - - public buildCard(boardNode: CardNode): Card { - this.ensureBoardNodeType(this.getChildren(boardNode), [ - BoardNodeType.FILE_ELEMENT, - BoardNodeType.LINK_ELEMENT, - BoardNodeType.RICH_TEXT_ELEMENT, - BoardNodeType.DRAWING_ELEMENT, - BoardNodeType.SUBMISSION_CONTAINER_ELEMENT, - BoardNodeType.EXTERNAL_TOOL, - BoardNodeType.COLLABORATIVE_TEXT_EDITOR, - ]); - - const elements = this.buildChildren< - ExternalToolElement | FileElement | LinkElement | RichTextElement | SubmissionContainerElement - >(boardNode); - - const card = new Card({ - id: boardNode.id, - title: boardNode.title ?? '', - height: boardNode.height, - children: elements, - createdAt: boardNode.createdAt, - updatedAt: boardNode.updatedAt, - }); - return card; - } - - public buildFileElement(boardNode: FileElementNode): FileElement { - this.ensureLeafNode(boardNode); - - const element = new FileElement({ - id: boardNode.id, - caption: boardNode.caption, - alternativeText: boardNode.alternativeText, - children: [], - createdAt: boardNode.createdAt, - updatedAt: boardNode.updatedAt, - }); - return element; - } - - public buildLinkElement(boardNode: LinkElementNode): LinkElement { - this.ensureLeafNode(boardNode); - - const element = new LinkElement({ - id: boardNode.id, - url: boardNode.url, - title: boardNode.title, - imageUrl: boardNode.imageUrl, - children: [], - createdAt: boardNode.createdAt, - updatedAt: boardNode.updatedAt, - }); - return element; - } - - public buildRichTextElement(boardNode: RichTextElementNode): RichTextElement { - this.ensureLeafNode(boardNode); - - const element = new RichTextElement({ - id: boardNode.id, - text: boardNode.text, - inputFormat: boardNode.inputFormat, - children: [], - createdAt: boardNode.createdAt, - updatedAt: boardNode.updatedAt, - }); - return element; - } - - public buildDrawingElement(boardNode: DrawingElementNode): DrawingElement { - this.ensureLeafNode(boardNode); - - const element = new DrawingElement({ - id: boardNode.id, - description: boardNode.description, - children: [], - createdAt: boardNode.createdAt, - updatedAt: boardNode.updatedAt, - }); - return element; - } - - public buildSubmissionContainerElement(boardNode: SubmissionContainerElementNode): SubmissionContainerElement { - this.ensureBoardNodeType(this.getChildren(boardNode), [BoardNodeType.SUBMISSION_ITEM]); - const elements = this.buildChildren(boardNode); - - const element = new SubmissionContainerElement({ - id: boardNode.id, - children: elements, - createdAt: boardNode.createdAt, - updatedAt: boardNode.updatedAt, - dueDate: boardNode.dueDate, - }); - - return element; - } - - public buildSubmissionItem(boardNode: SubmissionItemNode): SubmissionItem { - this.ensureBoardNodeType(this.getChildren(boardNode), [ - BoardNodeType.FILE_ELEMENT, - BoardNodeType.RICH_TEXT_ELEMENT, - ]); - const elements = this.buildChildren(boardNode); - - const element = new SubmissionItem({ - id: boardNode.id, - createdAt: boardNode.createdAt, - updatedAt: boardNode.updatedAt, - completed: boardNode.completed, - userId: boardNode.userId, - children: elements, - }); - return element; - } - - buildExternalToolElement(boardNode: ExternalToolElementNodeEntity): ExternalToolElement { - this.ensureLeafNode(boardNode); - - const element: ExternalToolElement = new ExternalToolElement({ - id: boardNode.id, - children: [], - createdAt: boardNode.createdAt, - updatedAt: boardNode.updatedAt, - contextExternalToolId: boardNode.contextExternalTool?.id, - }); - - return element; - } - - buildCollaborativeTextEditorElement(boardNode: CollaborativeTextEditorElementNode): CollaborativeTextEditorElement { - this.ensureLeafNode(boardNode); - - const element: CollaborativeTextEditorElement = new CollaborativeTextEditorElement({ - id: boardNode.id, - children: [], - createdAt: boardNode.createdAt, - updatedAt: boardNode.updatedAt, - }); - - return element; - } - - buildChildren(boardNode: BoardNode): T[] { - const children = this.getChildren(boardNode).map((node) => node.useDoBuilder(this)); - return children as T[]; - } - - getChildren(boardNode: BoardNode): BoardNode[] { - const children = this.childrenMap[boardNode.pathOfChildren] || []; - const sortedChildren = children.sort((a, b) => a.position - b.position); - return sortedChildren; - } - - ensureLeafNode(boardNode: BoardNode) { - const children = this.getChildren(boardNode); - if (children.length !== 0) throw new Error('BoardNode is a leaf node but children were provided.'); - } - - ensureBoardNodeType(boardNode: BoardNode | BoardNode[], type: BoardNodeType | BoardNodeType[]) { - const single = (bn: BoardNode, t: BoardNodeType | BoardNodeType[]) => { - const isValid = Array.isArray(t) ? type.includes(bn.type) : t === bn.type; - if (!isValid) { - throw new NotImplementedException(`Invalid node type '${bn.type}'`); - } - }; - - if (Array.isArray(boardNode)) { - boardNode.forEach((bn) => single(bn, type)); - } else { - single(boardNode, type); - } - } - - buildMediaBoard(boardNode: MediaBoardNode): MediaBoard { - this.ensureBoardNodeType(this.getChildren(boardNode), BoardNodeType.MEDIA_LINE); - - const lines: MediaLine[] = this.buildChildren(boardNode); - - const mediaBoard: MediaBoard = new MediaBoard({ - id: boardNode.id, - createdAt: boardNode.createdAt, - updatedAt: boardNode.updatedAt, - children: lines, - context: boardNode.context, - layout: boardNode.layout, - mediaAvailableLineBackgroundColor: boardNode.mediaAvailableLineBackgroundColor, - mediaAvailableLineCollapsed: boardNode.mediaAvailableLineCollapsed, - }); - - return mediaBoard; - } - - buildMediaLine(boardNode: MediaLineNode): MediaLine { - this.ensureBoardNodeType(this.getChildren(boardNode), BoardNodeType.MEDIA_EXTERNAL_TOOL_ELEMENT); - - const elements: MediaExternalToolElement[] = this.buildChildren(boardNode); - - const mediaLine: MediaLine = new MediaLine({ - id: boardNode.id, - createdAt: boardNode.createdAt, - updatedAt: boardNode.updatedAt, - children: elements, - title: boardNode.title, - backgroundColor: boardNode.backgroundColor, - collapsed: boardNode.collapsed, - }); - - return mediaLine; - } - - buildMediaExternalToolElement(boardNode: MediaExternalToolElementNode): MediaExternalToolElement { - this.ensureLeafNode(boardNode); - - const element: MediaExternalToolElement = new MediaExternalToolElement({ - id: boardNode.id, - children: [], - createdAt: boardNode.createdAt, - updatedAt: boardNode.updatedAt, - contextExternalToolId: boardNode.contextExternalTool.id, - }); - - return element; - } -} diff --git a/apps/server/src/modules/board/repo/board-do.repo.spec.ts b/apps/server/src/modules/board/repo/board-do.repo.spec.ts deleted file mode 100644 index aff55ac0609..00000000000 --- a/apps/server/src/modules/board/repo/board-do.repo.spec.ts +++ /dev/null @@ -1,661 +0,0 @@ -import { createMock } from '@golevelup/ts-jest'; -import { MongoMemoryDatabaseModule } from '@infra/database'; -import { NotFoundError } from '@mikro-orm/core'; -import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; -import { CollaborativeTextEditorService } from '@modules/collaborative-text-editor'; -import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; -import { DrawingElementAdapterService } from '@modules/tldraw-client'; -import { ContextExternalToolService } from '@modules/tool/context-external-tool/service'; -import { - contextExternalToolEntityFactory, - contextExternalToolFactory, -} from '@modules/tool/context-external-tool/testing'; -import { NotFoundException } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import { - AnyBoardDo, - BoardExternalReference, - BoardExternalReferenceType, - Card, - Column, - ColumnBoard, -} from '@shared/domain/domainobject'; -import { - BoardNode, - CardNode, - ColumnBoardNode, - ExternalToolElementNodeEntity, - RichTextElementNode, -} from '@shared/domain/entity'; -import { EntityId } from '@shared/domain/types'; -import { - cardFactory, - cardNodeFactory, - cleanupCollections, - columnBoardFactory, - columnBoardNodeFactory, - columnFactory, - columnNodeFactory, - courseFactory, - externalToolElementNodeFactory, - fileElementFactory, - mediaBoardNodeFactory, - mediaExternalToolElementNodeFactory, - mediaLineNodeFactory, - richTextElementFactory, - richTextElementNodeFactory, -} from '@shared/testing'; -import { ContextExternalTool } from '../../tool/context-external-tool/domain'; -import { ContextExternalToolEntity } from '../../tool/context-external-tool/entity'; -import { BoardDoRepo } from './board-do.repo'; -import { BoardNodeRepo } from './board-node.repo'; -import { RecursiveDeleteVisitor } from './recursive-delete.vistor'; -import { RecursiveSaveVisitor } from './recursive-save.visitor'; - -describe(BoardDoRepo.name, () => { - let module: TestingModule; - let repo: BoardDoRepo; - let em: EntityManager; - let recursiveDeleteVisitor: RecursiveDeleteVisitor; - - beforeAll(async () => { - module = await Test.createTestingModule({ - imports: [MongoMemoryDatabaseModule.forRoot()], - providers: [ - BoardDoRepo, - BoardNodeRepo, - RecursiveDeleteVisitor, - { provide: FilesStorageClientAdapterService, useValue: createMock() }, - { provide: ContextExternalToolService, useValue: createMock() }, - { provide: DrawingElementAdapterService, useValue: createMock() }, - { provide: CollaborativeTextEditorService, useValue: createMock() }, - ], - }).compile(); - repo = module.get(BoardDoRepo); - em = module.get(EntityManager); - recursiveDeleteVisitor = module.get(RecursiveDeleteVisitor); - }); - - afterAll(async () => { - await module.close(); - }); - - afterEach(async () => { - await cleanupCollections(em); - }); - - describe('findById', () => { - describe('when finding by id', () => { - const setup = async () => { - const boardNode = columnBoardNodeFactory.build(); - await em.persistAndFlush(boardNode); - em.clear(); - - return { boardNode }; - }; - - it('should return the object', async () => { - const { boardNode } = await setup(); - const result = await repo.findById(boardNode.id); - expect(result.id).toEqual(boardNode.id); - }); - - it('should throw an error when not found', async () => { - await expect(repo.findById('invalid-id')).rejects.toThrowError(NotFoundError); - }); - }); - }); - - describe('findByClassAndId', () => { - describe('when finding by class and id', () => { - const setup = async () => { - const boardNode = columnBoardNodeFactory.build(); - await em.persistAndFlush(boardNode); - - em.clear(); - - return { boardNode }; - }; - - it('should return the object', async () => { - const { boardNode } = await setup(); - - const result = await repo.findByClassAndId(ColumnBoard, boardNode.id); - - expect(result.id).toEqual(boardNode.id); - }); - - it('should throw error when id does not belong to the expected class', async () => { - const { boardNode } = await setup(); - const expectedError = new NotFoundException("There is no 'Column' with this id"); - - await expect(repo.findByClassAndId(Column, boardNode.id)).rejects.toThrow(expectedError); - }); - }); - }); - - describe('findByIds', () => { - describe('when finding by ids', () => { - const setup = async () => { - const boardNode = columnBoardNodeFactory.build(); - await em.persistAndFlush(boardNode); - const columnNodes = columnNodeFactory.buildList(2, { parent: boardNode }); - await em.persistAndFlush(columnNodes); - const cardNodes = cardNodeFactory.buildList(2, { parent: columnNodes[0] }); - await em.persistAndFlush(cardNodes); - const elementNodes = richTextElementNodeFactory.buildList(2, { parent: cardNodes[1] }); - await em.persistAndFlush(elementNodes); - em.clear(); - - return { boardNode, columnNodes, cardNodes, elementNodes }; - }; - - it('should return the objects', async () => { - const { boardNode, columnNodes } = await setup(); - const nodeIds = [boardNode.id, columnNodes[0].id]; - - const result = await repo.findByIds(nodeIds); - const resultIds = result.map((obj) => obj.id); - - expect(result[0].children.length).toBeGreaterThan(0); - expect(result[0].children[0].children.length).toBeGreaterThan(0); - - expect(resultIds).toEqual(nodeIds); - }); - - it('should return nothing for not-existing id', async () => { - await setup(); - - const result = await repo.findByIds(['not-existing id']); - - expect(result).toEqual([]); - }); - }); - }); - - describe('getTitlesByIds', () => { - const setup = async () => { - const cardsWithTitles = cardNodeFactory.buildList(3); - const cardWithoutTitle = cardNodeFactory.build({ title: undefined }); - - await em.persistAndFlush([...cardsWithTitles, cardWithoutTitle]); - - return { cardsWithTitles, cardWithoutTitle }; - }; - - it('should return titles of node for list of ids', async () => { - const { cardsWithTitles } = await setup(); - - const titleMap = await repo.getTitlesByIds(cardsWithTitles.map((card) => card.id)); - - cardsWithTitles.forEach((card) => { - expect(titleMap[card.id]).toEqual(card.title); - }); - }); - - it('should return node of card for single id', async () => { - const { cardsWithTitles } = await setup(); - - const titleMap = await repo.getTitlesByIds(cardsWithTitles[0].id); - - expect(titleMap[cardsWithTitles[0].id]).toEqual(cardsWithTitles[0].title); - }); - - it('should handle node without title', async () => { - const { cardWithoutTitle } = await setup(); - - const titleMap = await repo.getTitlesByIds(cardWithoutTitle.id); - - expect(titleMap[cardWithoutTitle.id]).toEqual(''); - }); - - it('should not return title of node that has not been asked about', async () => { - const { cardsWithTitles } = await setup(); - - const titleMap = await repo.getTitlesByIds(cardsWithTitles[0].id); - - expect(titleMap[cardsWithTitles[1].id]).toEqual(undefined); - }); - }); - - describe('findIdsByExternalReference', () => { - const setup = async () => { - const course = courseFactory.build(); - await em.persistAndFlush(course); - const boardNode = columnBoardNodeFactory.build({ - context: { - type: BoardExternalReferenceType.Course, - id: course.id, - }, - }); - await em.persistAndFlush(boardNode); - - return { boardNode, course }; - }; - - it('should find courseboard by course', async () => { - const { course, boardNode } = await setup(); - - const ids = await repo.findIdsByExternalReference({ - type: BoardExternalReferenceType.Course, - id: course.id, - }); - - expect(ids[0]).toEqual(boardNode.id); - }); - }); - - describe('findParentOfId', () => { - describe('when fetching a parent', () => { - const setup = async () => { - const cardNode = cardNodeFactory.buildWithId(); - const [richTextElement1, richTextElement2] = richTextElementNodeFactory.buildList(3, { parent: cardNode }); - const nonChildRichTextElement = richTextElementNodeFactory.buildWithId(); - - await em.persistAndFlush([cardNode, richTextElement1, richTextElement2]); - - return { cardId: cardNode.id, richTextElement1, richTextElement2, nonChildRichTextElement }; - }; - - it('should not return the parent for an incorrect childId', async () => { - const { nonChildRichTextElement } = await setup(); - - await expect(repo.findParentOfId(nonChildRichTextElement.id)).rejects.toThrow(); - }); - - it('should return the parent for a correct childId', async () => { - const { cardId, richTextElement1 } = await setup(); - - const parent = await repo.findParentOfId(richTextElement1.id); - - expect(parent?.id).toBe(cardId); - }); - - it('should return the parent including all children', async () => { - const { richTextElement1, richTextElement2 } = await setup(); - const expectedChildIds = [richTextElement1.id, richTextElement2.id]; - - const parent = await repo.findParentOfId(richTextElement1.id); - const actualChildIds = parent?.children?.map((child: AnyBoardDo) => child.id) ?? []; - - expect(parent?.children).toHaveLength(2); - expect(expectedChildIds).toContain(actualChildIds[0]); - expect(expectedChildIds).toContain(actualChildIds[1]); - }); - - it('should return undefined if board node has no parent', async () => { - const { cardId } = await setup(); - - const parent = await repo.findParentOfId(cardId); - - expect(parent).toBeUndefined(); - }); - }); - }); - - describe('countBoardUsageForExternalTools', () => { - describe('when counting the amount of boards used by the selected tools', () => { - const setup = async () => { - const contextExternalToolId: EntityId = new ObjectId().toHexString(); - const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId( - undefined, - contextExternalToolId - ); - const contextExternalToolEntity: ContextExternalToolEntity = contextExternalToolEntityFactory.buildWithId( - undefined, - contextExternalToolId - ); - const otherContextExternalToolEntity: ContextExternalToolEntity = - contextExternalToolEntityFactory.buildWithId(); - - const board: ColumnBoardNode = columnBoardNodeFactory.buildWithId(); - const otherBoard: ColumnBoardNode = columnBoardNodeFactory.buildWithId(); - const card: CardNode = cardNodeFactory.buildWithId({ parent: board }); - const otherCard: CardNode = cardNodeFactory.buildWithId({ parent: otherBoard }); - const externalToolElements: ExternalToolElementNodeEntity[] = externalToolElementNodeFactory.buildListWithId( - 2, - { - parent: card, - contextExternalTool: contextExternalToolEntity, - } - ); - const otherExternalToolElement: ExternalToolElementNodeEntity = externalToolElementNodeFactory.buildWithId({ - parent: otherCard, - contextExternalTool: otherContextExternalToolEntity, - }); - - await em.persistAndFlush([ - board, - otherBoard, - card, - otherCard, - ...externalToolElements, - otherExternalToolElement, - contextExternalToolEntity, - ]); - - return { - contextExternalTool, - }; - }; - - it('should return the amount of boards used by the selected tools', async () => { - const { contextExternalTool } = await setup(); - - const result: number = await repo.countBoardUsageForExternalTools([contextExternalTool]); - - expect(result).toEqual(1); - }); - }); - }); - - describe('getAncestorIds', () => { - describe('when having only a root boardnode', () => { - const setup = async () => { - const columnBoardNode = columnBoardNodeFactory.build(); - - await em.persistAndFlush([columnBoardNode]); - - return { boardId: columnBoardNode.id }; - }; - - it('should return an empty list', async () => { - const { boardId } = await setup(); - - const board = await repo.findById(boardId); - const ancestorIds = await repo.getAncestorIds(board); - - expect(ancestorIds).toHaveLength(0); - }); - }); - - describe('when having multiple boardnodes', () => { - const setup = async () => { - const boardNode = columnBoardNodeFactory.build(); - await em.persistAndFlush(boardNode); - const columnNodes = columnNodeFactory.buildList(2, { parent: boardNode }); - await em.persistAndFlush(columnNodes); - const cardNodes = cardNodeFactory.buildList(2, { parent: columnNodes[0] }); - await em.persistAndFlush(cardNodes); - const elementNodes = richTextElementNodeFactory.buildList(2, { parent: cardNodes[0] }); - await em.persistAndFlush(elementNodes); - em.clear(); - - const boardId = boardNode.id; - const columnId = columnNodes[0].id; - const cardId = cardNodes[0].id; - const secondElementId = elementNodes[1].id; - - return { boardId, columnId, cardId, secondElementId }; - }; - - it('should return an empty list', async () => { - const { boardId, columnId, cardId, secondElementId } = await setup(); - - const element = await repo.findById(secondElementId); - const ancestorIds = await repo.getAncestorIds(element); - - expect(ancestorIds).toEqual([boardId, columnId, cardId]); - }); - }); - }); - - describe('save', () => { - describe('when called', () => { - it('should create new board nodes', async () => { - const cards = cardFactory.buildList(3); - - await repo.save(cards); - em.clear(); - - const result = await em.find(CardNode, {}); - expect(result.map((n) => n.id).sort()).toEqual(cards.map((c) => c.id).sort()); - }); - - it('should update existing board nodes', async () => { - const node = cardNodeFactory.buildWithId({ title: 'before' }); - await em.persistAndFlush(node); - - const card = await repo.findByClassAndId(Card, node.id); - card.title = 'after'; - - await repo.save(card); - em.clear(); - - const result = await em.findOneOrFail(CardNode, node.id); - expect(result.title).toEqual('after'); - }); - - it('should be able to do both - create and update', async () => { - const node1 = cardNodeFactory.buildWithId({ title: 'before' }); - await em.persistAndFlush(node1); - em.clear(); - const card1 = await repo.findByClassAndId(Card, node1.id); - card1.title = 'after'; - const card2 = cardFactory.build({ title: 'created' }); - - await repo.save([card1, card2]); - em.clear(); - - const result = await em.find(CardNode, {}); - expect(result.map((n) => n.title).sort()).toEqual(['after', 'created']); - }); - - it('should use the visitor', async () => { - const board = columnBoardFactory.build(); - jest.spyOn(board, 'accept'); - - await repo.save(board); - - expect(board.accept).toHaveBeenCalledWith(expect.any(RecursiveSaveVisitor)); - }); - - it('should flush the changes', async () => { - const board = columnBoardFactory.build(); - jest.spyOn(em, 'flush'); - - await repo.save(board); - - expect(em.flush).toHaveBeenCalled(); - }); - }); - - describe('when parent is already persisted', () => { - it('should create child nodes', async () => { - const column = columnFactory.build(); - await repo.save(column); - - const cards = cardFactory.buildList(2); - cards.forEach((card) => column.addChild(card)); - await repo.save(cards, column); - - const result = await em.find(CardNode, {}); - expect(result.map((n) => n.parentId)).toEqual([column.id, column.id]); - }); - }); - - describe('when parent is newly built (not yet persisted)', () => { - it('should throw an error', async () => { - const card = cardFactory.build(); - const column = columnFactory.build({ children: [card] }); - - await expect(repo.save(card, column)).rejects.toThrow(); - }); - }); - - describe('when objects have different parents', () => { - it('should throw an error', async () => { - const card1 = cardFactory.build(); - const card2 = cardFactory.build(); - - const column1 = columnFactory.build({ children: [card1] }); - const column2 = columnFactory.build({ children: [card2] }); - await repo.save([column1, column2]); - - await expect(repo.save([card1, card2], column1)).rejects.toThrow(); - }); - }); - - describe('child ordering', () => { - const setup = async () => { - const board = columnBoardFactory.build(); - const cards = cardFactory.buildList(3); - const column = columnFactory.build({ children: cards }); - - const columnNode = columnNodeFactory.build({ id: column.id }); - await em.persistAndFlush(columnNode); - - return { board, column, card1: cards[0], card2: cards[1], card3: cards[2] }; - }; - - it('should persist child order to positions', async () => { - const { column, card1, card2, card3 } = await setup(); - - await repo.save([card1, card2, card3], column); - em.clear(); - - expect((await em.findOne(CardNode, card1.id))?.position).toEqual(0); - expect((await em.findOne(CardNode, card2.id))?.position).toEqual(1); - expect((await em.findOne(CardNode, card3.id))?.position).toEqual(2); - }); - }); - }); - - describe('delete', () => { - describe('when deleting a domainObject and its descendants', () => { - const setup = async () => { - const elements = [...richTextElementFactory.buildList(3), ...fileElementFactory.buildList(2)]; - const card = cardFactory.build({ children: elements }); - await repo.save(card); - await repo.save(elements, card); - const siblingCardElements = richTextElementFactory.buildList(3); - const siblingCard = cardFactory.build({ children: siblingCardElements }); - await repo.save(siblingCard); - await repo.save(siblingCardElements, siblingCard); - const column = columnFactory.build({ children: [card, siblingCard] }); - await repo.save(column); - em.clear(); - - return { card, elements, siblingCard, siblingCardElements }; - }; - - it('should delete a domain object', async () => { - const { elements } = await setup(); - - await repo.delete(elements[0]); - em.clear(); - - await expect(em.findOneOrFail(RichTextElementNode, elements[0].id)).rejects.toThrow(); - }); - - it('should delete all descendants', async () => { - const { card, elements } = await setup(); - - await repo.delete(card); - em.clear(); - - await expect(em.findOneOrFail(RichTextElementNode, elements[0].id)).rejects.toThrow(); - await expect(em.findOneOrFail(RichTextElementNode, elements[1].id)).rejects.toThrow(); - await expect(em.findOneOrFail(RichTextElementNode, elements[2].id)).rejects.toThrow(); - await expect(em.findOneOrFail(RichTextElementNode, elements[3].id)).rejects.toThrow(); - await expect(em.findOneOrFail(RichTextElementNode, elements[4].id)).rejects.toThrow(); - }); - - it('should not delete descendants of siblings', async () => { - const { card, siblingCardElements } = await setup(); - - await repo.delete(card); - em.clear(); - - await expect(em.findOneOrFail(RichTextElementNode, siblingCardElements[0].id)).resolves.toBeDefined(); - await expect(em.findOneOrFail(RichTextElementNode, siblingCardElements[1].id)).resolves.toBeDefined(); - await expect(em.findOneOrFail(RichTextElementNode, siblingCardElements[2].id)).resolves.toBeDefined(); - }); - - it('should use the visitor', async () => { - const { card } = await setup(); - card.acceptAsync = jest.fn(); - - await repo.delete(card); - - expect(card.acceptAsync).toHaveBeenCalledWith(recursiveDeleteVisitor); - }); - - it('should use the visitor', async () => { - const { card } = await setup(); - card.acceptAsync = jest.fn(); - - await repo.delete(card); - - expect(card.acceptAsync).toHaveBeenCalledWith(recursiveDeleteVisitor); - }); - }); - }); - - describe('deleteByExternalReference', () => { - describe('when deleting a board by its external reference', () => { - const setup = async () => { - const courseContext: BoardExternalReference = { - id: new ObjectId().toHexString(), - type: BoardExternalReferenceType.Course, - }; - const courseBoard = columnBoardNodeFactory.buildWithId({ context: courseContext }); - const courseColumn = columnNodeFactory.buildWithId({ parent: courseBoard }); - const courseCard = cardNodeFactory.buildWithId({ parent: courseColumn }); - const courseElement = richTextElementNodeFactory.buildWithId({ parent: courseCard }); - - const userContext: BoardExternalReference = { - id: new ObjectId().toHexString(), - type: BoardExternalReferenceType.User, - }; - const userMediaBoard = mediaBoardNodeFactory.buildWithId({ context: userContext }); - const userMediaLine = mediaLineNodeFactory.buildWithId({ parent: userMediaBoard }); - const userMediaElement = mediaExternalToolElementNodeFactory.buildWithId({ parent: userMediaLine }); - - await em.persistAndFlush([ - courseBoard, - courseColumn, - courseCard, - courseElement, - userMediaBoard, - userMediaLine, - userMediaElement, - ]); - em.clear(); - - return { - courseContext, - courseBoard, - courseColumn, - courseCard, - courseElement, - userMediaBoard, - userMediaLine, - userMediaElement, - }; - }; - - it('should delete a board with the given reference', async () => { - const { courseContext, courseBoard, courseColumn, courseCard, courseElement } = await setup(); - - await repo.deleteByExternalReference(courseContext); - em.clear(); - - await expect( - em.find(BoardNode, { id: { $in: [courseBoard.id, courseColumn.id, courseCard.id, courseElement.id] } }) - ).resolves.toHaveLength(0); - }); - - it('should not delete a board without the given reference', async () => { - const { courseContext, userMediaBoard, userMediaLine, userMediaElement } = await setup(); - - await repo.deleteByExternalReference(courseContext); - em.clear(); - - await expect( - em.find(BoardNode, { id: { $in: [userMediaBoard.id, userMediaLine.id, userMediaElement.id] } }) - ).resolves.toHaveLength(3); - }); - }); - }); -}); diff --git a/apps/server/src/modules/board/repo/board-do.repo.ts b/apps/server/src/modules/board/repo/board-do.repo.ts deleted file mode 100644 index 27d5f69d0e3..00000000000 --- a/apps/server/src/modules/board/repo/board-do.repo.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { type FilterQuery, Utils } from '@mikro-orm/core'; -import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; -import type { ContextExternalTool } from '@modules/tool/context-external-tool/domain'; -import { Injectable, NotFoundException } from '@nestjs/common'; -import type { AnyBoardDo, BoardExternalReference } from '@shared/domain/domainobject'; -import { BoardNode, ExternalToolElementNodeEntity } from '@shared/domain/entity'; -import { EntityId } from '@shared/domain/types'; -import { BoardDoBuilderImpl } from './board-do.builder-impl'; -import { BoardNodeRepo } from './board-node.repo'; -import { RecursiveDeleteVisitor } from './recursive-delete.vistor'; -import { RecursiveSaveVisitor } from './recursive-save.visitor'; - -@Injectable() -export class BoardDoRepo { - constructor( - private readonly em: EntityManager, - private readonly boardNodeRepo: BoardNodeRepo, - private readonly deleteVisitor: RecursiveDeleteVisitor - ) {} - - async findById(id: EntityId, depth?: number): Promise { - const boardNode = await this.boardNodeRepo.findById(id); - const descendants = await this.boardNodeRepo.findDescendants(boardNode, depth); - const domainObject = new BoardDoBuilderImpl(descendants).buildDomainObject(boardNode); - - return domainObject; - } - - async findByClassAndId( - doClass: { new (props: S): T }, - id: EntityId, - depth?: number - ): Promise { - const domainObject = await this.findById(id, depth); - if (!(domainObject instanceof doClass)) { - throw new NotFoundException(`There is no '${doClass.name}' with this id`); - } - - return domainObject; - } - - async findByIds(ids: EntityId[]): Promise { - const boardNodes = await this.em.find(BoardNode, { id: { $in: ids } }); - - const childrenMap = await this.boardNodeRepo.findDescendantsOfMany(boardNodes); - - const domainObjects = boardNodes.map((boardNode) => { - const children = childrenMap[boardNode.pathOfChildren]; - const domainObject = new BoardDoBuilderImpl(children).buildDomainObject(boardNode); - return domainObject; - }); - - return domainObjects; - } - - async getTitlesByIds(id: EntityId[] | EntityId): Promise> { - const ids = Utils.asArray(id); - const boardNodes = await this.em.find(BoardNode, { id: { $in: ids } }); - - const titlesMap = boardNodes.reduce((map, node) => { - map[node.id] = node.title ?? ''; - return map; - }, {}); - - return titlesMap; - } - - async findIdsByExternalReference(reference: BoardExternalReference): Promise { - // TODO Use an abstract base class for root nodes that have a contextId and a contextType. Multiple STI abstract base classes are blocked by MikroORM 6.1.2 (issue #3745) - const boardNodes: BoardNode[] = await this.em.find(BoardNode, { - _contextId: new ObjectId(reference.id), - _contextType: reference.type, - } as FilterQuery); - - const ids: EntityId[] = boardNodes.map((node) => node.id); - - return ids; - } - - async findParentOfId(childId: EntityId): Promise { - const boardNode = await this.boardNodeRepo.findById(childId); - const domainObject = boardNode.parentId ? this.findById(boardNode.parentId) : undefined; - - return domainObject; - } - - async countBoardUsageForExternalTools(contextExternalTools: ContextExternalTool[]) { - const toolIds: EntityId[] = contextExternalTools - .map((tool: ContextExternalTool): EntityId | undefined => tool.id) - .filter((id: EntityId | undefined): id is EntityId => !!id); - - const boardNodes: ExternalToolElementNodeEntity[] = await this.em.find(ExternalToolElementNodeEntity, { - contextExternalTool: { $in: toolIds }, - }); - - const boardIds: EntityId[] = boardNodes.map((node: ExternalToolElementNodeEntity): EntityId => node.ancestorIds[0]); - const boardCount: number = new Set(boardIds).size; - - return boardCount; - } - - async getAncestorIds(boardDo: AnyBoardDo): Promise { - const boardNode = await this.boardNodeRepo.findById(boardDo.id); - return boardNode.ancestorIds; - } - - async save(domainObject: AnyBoardDo | AnyBoardDo[], parent?: AnyBoardDo): Promise { - const saveVisitor = new RecursiveSaveVisitor(this.em, this.boardNodeRepo); - await saveVisitor.save(domainObject, parent); - await this.em.flush(); - } - - async delete(domainObject: AnyBoardDo): Promise { - await domainObject.acceptAsync(this.deleteVisitor); - await this.em.flush(); - } - - async deleteByExternalReference(reference: BoardExternalReference): Promise { - // TODO Use an abstract base class for root nodes that have a contextId and a contextType. Multiple STI abstract base classes are blocked by MikroORM 6.1.2 (issue #3745) - const boardNodes: BoardNode[] = await this.em.find(BoardNode, { - _contextId: new ObjectId(reference.id), - _contextType: reference.type, - } as FilterQuery); - - const boardDeletionPromises: Promise[] = boardNodes.map(async (boardNode: BoardNode): Promise => { - const descendants: BoardNode[] = await this.boardNodeRepo.findDescendants(boardNode); - - const domainObject: AnyBoardDo = new BoardDoBuilderImpl(descendants).buildDomainObject(boardNode); - - await domainObject.acceptAsync(this.deleteVisitor); - }); - - await Promise.all(boardDeletionPromises); - - await this.em.flush(); - - return boardNodes.length; - } -} diff --git a/apps/server/src/modules/board/repo/board-node.repo.spec.ts b/apps/server/src/modules/board/repo/board-node.repo.spec.ts index 35d8d0f78ea..3a6cf173d23 100644 --- a/apps/server/src/modules/board/repo/board-node.repo.spec.ts +++ b/apps/server/src/modules/board/repo/board-node.repo.spec.ts @@ -1,15 +1,12 @@ -import { MongoMemoryDatabaseModule } from '@infra/database'; import { EntityManager } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; -import { ColumnBoardNode } from '@shared/domain/entity'; -import { - cardNodeFactory, - cleanupCollections, - columnBoardNodeFactory, - columnNodeFactory, - richTextElementNodeFactory, -} from '@shared/testing'; +import { BaseEntityWithTimestamps } from '@shared/domain/entity'; +import { cleanupCollections } from '@shared/testing'; +import { MongoMemoryDatabaseModule } from '@src/infra/database'; +import { ColumnBoard } from '../domain'; +import { cardFactory, columnBoardFactory, columnFactory } from '../testing'; import { BoardNodeRepo } from './board-node.repo'; +import { BoardNodeEntity } from './entity/board-node.entity'; describe('BoardNodeRepo', () => { let module: TestingModule; @@ -18,7 +15,7 @@ describe('BoardNodeRepo', () => { beforeAll(async () => { module = await Test.createTestingModule({ - imports: [MongoMemoryDatabaseModule.forRoot()], + imports: [MongoMemoryDatabaseModule.forRoot({ entities: [BaseEntityWithTimestamps, BoardNodeEntity] })], providers: [BoardNodeRepo], }).compile(); repo = module.get(BoardNodeRepo); @@ -33,206 +30,232 @@ describe('BoardNodeRepo', () => { await cleanupCollections(em); }); - describe('findDescendants', () => { - const setup = async () => { - // root - // -- level1[0] - // ---- level2[0] - // ---- level2[1] - // ------ level3[0] - // ------ level3[1] - // ---- level2b[0] - // ---- level2b[1] - // -- level1[1] - - const root = columnBoardNodeFactory.build(); - await em.persistAndFlush(root); - const level1 = columnNodeFactory.buildList(2, { parent: root }); - await em.persistAndFlush(level1); - const level2 = cardNodeFactory.buildList(2, { parent: level1[0] }); - await em.persistAndFlush(level2); - const level2b = cardNodeFactory.buildList(2, { parent: level1[1] }); - await em.persistAndFlush(level2b); - const level3 = richTextElementNodeFactory.buildList(2, { parent: level2[1] }); - await em.persistAndFlush(level3); - em.clear(); + describe('save', () => { + const setup = () => { + const board = columnBoardFactory.build({ + children: columnFactory.buildList(2, { children: cardFactory.buildList(2) }), + }); - return { root, level1, level2, level2b, level3 }; + return { board }; }; - describe('when starting at the root node', () => { - it('should find descendents with a specific depth', async () => { - const { root, level1, level2, level2b } = await setup(); + it('should be able to persist a tree of nodes', async () => { + const { board } = setup(); - const result = await repo.findDescendants(root, 2); + await repo.save(board); + em.clear(); - const resultIds = result.map((o) => o.id).sort(); - const expectedIds = [...level1, ...level2, ...level2b].map((o) => o.id).sort(); - expect(resultIds).toEqual(expectedIds); - }); + const nodeCount = await em.count(BoardNodeEntity); + expect(nodeCount).toBe(5); }); - describe('when starting at a nested node', () => { - it('should find descendents with a specific depth', async () => { - const { level1, level2 } = await setup(); + it('should be able to persist multiple root nodes', async () => { + const { board: board1 } = setup(); + const { board: board2 } = setup(); - const result = await repo.findDescendants(level1[0], 1); + await repo.save([board1, board2]); + em.clear(); - const resultIds = result.map((o) => o.id).sort(); - const expectedIds = [...level2].map((o) => o.id).sort(); - expect(resultIds).toEqual(expectedIds); - }); + const nodeCount = await em.count(BoardNodeEntity); + expect(nodeCount).toBe(10); }); - describe('when depth is undefined', () => { - it('should return all descendants', async () => { - const { level1, level2, level3 } = await setup(); + it('should persist embedded context', async () => { + const { board } = setup(); - const result = await repo.findDescendants(level1[0]); + await repo.save(board); + em.clear(); - const resultIds = result.map((o) => o.id).sort(); - const expectedIds = [...level2, ...level3].map((o) => o.id).sort(); - expect(resultIds).toEqual(expectedIds); - }); + const result = await em.findOneOrFail(BoardNodeEntity, board.id); + expect(result.context).toBeDefined(); }); + }); - describe('when depth is 0', () => { - it('should return empty list', async () => { - const { level1 } = await setup(); + describe('findById', () => { + const setup = async () => { + const card = cardFactory.build(); - const result = await repo.findDescendants(level1[0], 0); + const column = columnFactory.build({ children: [card] }); - expect(result).toEqual([]); + const board = columnBoardFactory.build({ + children: [column], }); + + await repo.save(board); + em.clear(); + + return { board, column, card }; + }; + + it('should be able to find a node tree by root id', async () => { + const { board, column, card } = await setup(); + + const result = await repo.findById(board.id); + + // TODO implement tree matcher (by id)? + expect(result.id).toEqual(board.id); + expect(result.children[0].id).toEqual(column.id); + expect(result.children[0].children[0].id).toEqual(card.id); }); }); - describe('findDescendantsOfMany', () => { - describe('when giving ids from boardNodes of different levels', () => { - const setup = async () => { - // root - // -- level1[0] - // ---- level2[0] - // ---- level2[1] - // ------ level3[0] - // ------ level3[1] - // -- level1[1] - - const root = columnBoardNodeFactory.build(); - await em.persistAndFlush(root); - const level1 = columnNodeFactory.buildList(2, { parent: root }); - await em.persistAndFlush(level1); - const level2 = cardNodeFactory.buildList(2, { parent: level1[0] }); - await em.persistAndFlush(level2); - const level3 = richTextElementNodeFactory.buildList(2, { parent: level2[1] }); - await em.persistAndFlush(level3); - em.clear(); - - return { root, level1, level2, level3 }; - }; - - it('should find a map of children that is complete', async () => { - const { root, level1, level2 } = await setup(); - - const result = await repo.findDescendantsOfMany([root, ...level1, ...level2]); - - expect(Object.keys(result)).toEqual([root.pathOfChildren, level1[0].pathOfChildren, level2[1].pathOfChildren]); - - expect(result[root.pathOfChildren]).toHaveLength(6); - expect(result[level1[0].pathOfChildren]).toHaveLength(4); - expect(result[level2[1].pathOfChildren]).toHaveLength(2); + describe('findByIds', () => { + const setup = async () => { + const board = columnBoardFactory.build({ + children: columnFactory.buildList(1, { children: cardFactory.buildList(1) }), }); - }); - describe('when giving ids of some boardNodes', () => { - const setup = async () => { - const root = columnBoardNodeFactory.build(); - await em.persistAndFlush(root); - const [column0, column1, column2] = columnNodeFactory.buildList(3, { parent: root }); - await em.persistAndFlush([column0, column1, column2]); - const [card00, card01] = cardNodeFactory.buildList(2, { parent: column0 }); - await em.persistAndFlush([card00, card01]); - const [text000, text001] = richTextElementNodeFactory.buildList(2, { parent: card00 }); - await em.persistAndFlush([text000, text001]); - const [card20, card21] = cardNodeFactory.buildList(2, { parent: column2 }); - await em.persistAndFlush([card20, card21]); - const [text210, text211] = richTextElementNodeFactory.buildList(2, { parent: card21 }); - await em.persistAndFlush([text210, text211]); - em.clear(); - - return { root, column0, card00, card01, text000, text001, column1, column2, card20, card21, text210, text211 }; - }; - - it('should return all decendants of those part trees', async () => { - // root - // -- column0 <-- requested - // ---- card00 <-- returned - // ------ text000 <-- returned - // ------ text001 <-- returned - // ---- card01 <-- returned - // -- column1 - // -- column2 - // ---- card20 - // ---- card21 <-- requested - // ------ text210 <-- returned - // ------ text211 <-- returned - const { column0, card00, card01, text000, text001, card21, text210, text211 } = await setup(); - - const result = await repo.findDescendantsOfMany([column0, card21]); - const returnedColumnDescendantIds = result[column0.pathOfChildren].map((o) => o.id); - const returnedCardDescendantIds = result[card21.pathOfChildren].map((o) => o.id); - - expect(returnedCardDescendantIds).toEqual([text210.id, text211.id]); - expect(returnedColumnDescendantIds).toEqual([card00.id, card01.id, text000.id, text001.id]); + const extraBoard = columnBoardFactory.build({ + children: columnFactory.buildList(1, { children: cardFactory.buildList(1) }), }); - it('should return no decendants of leaf nodes', async () => { - // root - // -- column0 - // ---- card00 - // ------ text000 - // ------ text001 - // ---- card01 - // -- column1 <-- requested - // -- column2 - // ---- card20 <-- requested - // ---- card21 - // ------ text210 - // ------ text211 - const { column1, card20 } = await setup(); - - const result = await repo.findDescendantsOfMany([column1, card20]); - const returnedDescendants = Object.values(result).flat(); - - expect(returnedDescendants).toHaveLength(0); + await repo.save([board, extraBoard]); + em.clear(); + + return { board, extraBoard }; + }; + + it('should be able to find multiple board nodes', async () => { + const { board, extraBoard } = await setup(); + + const result = await repo.findByIds([board.id, extraBoard.id]); + + expect(result[0]).toBeInstanceOf(ColumnBoard); + expect(result[1]).toBeInstanceOf(ColumnBoard); + }); + + it('should be able to limit tree depth', async () => { + const { board } = await setup(); + + const result = (await repo.findByIds([board.id], 1))[0]; + + expect(result.children[0].children).toHaveLength(0); + }); + }); + + describe('findByExternalReference', () => { + it.todo('should be able to find nodes by their external reference'); + it.todo('should populate the node tree'); + describe('when depth is specified', () => { + it.todo('should limit the tree to depth'); + }); + }); + + describe('findByContextExternalToolIds', () => { + it.todo('should be able to find nodes by their external tool ids'); + it.todo('should populate the node tree'); + describe('when depth is specified', () => { + it.todo('should limit the tree to depth'); + }); + }); + + // describe('findCommonParentOfIds', () => { + // const setup = async () => { + // const card = cardFactory.build(); + // const column = columnFactory.build({ children: [card] }); + // const board = columnBoardFactory.build({ children: [column] }); + + // await repo.save(board); + // em.clear(); + + // return { board, column, card }; + // }; + + // it('should find the common parent', async () => { + // const { board, column, card } = await setup(); + + // const result = await repo.findCommonParentOfIds([column.id, card.id]); + + // expect(result.id).toEqual(board.id); + // }); + // }); + + describe('delete', () => { + const setup = async () => { + const board = columnBoardFactory.build({ + children: columnFactory.buildList(1, { children: cardFactory.buildList(1) }), }); + + await repo.save(board); + + return { board }; + }; + + it('should delete all nodes recursivevely', async () => { + const { board } = await setup(); + expect(await em.count(BoardNodeEntity)).toBe(3); + + await repo.delete(board); + + expect(await em.count(BoardNodeEntity)).toBe(0); }); }); - describe('findById', () => { - describe('when boardNode exists in the database but NOT in the unit-of-work', () => { - it('should return an equal object', async () => { - const columnBoard = columnBoardNodeFactory.build(); - await em.persistAndFlush(columnBoard); + describe('identity map', () => { + const setup = async () => { + const cards = cardFactory.buildList(2); + const column = columnFactory.build({ children: cards }); + const board = columnBoardFactory.build({ children: [column] }); - em.clear(); + await repo.save(board); + em.clear(); + + return { boardId: board.id, columnId: column.id, cardIds: cards.map((c) => c.id) }; + }; - const boardNode = await repo.findById(columnBoard.id); + describe('when loading a node twice', () => { + it('should keep referential identity', async () => { + const { boardId } = await setup(); - expect(columnBoard).toEqual(boardNode); + const result1 = await repo.findById(boardId); + const result2 = await repo.findById(boardId); + + expect(result1 === result2).toBe(true); }); }); - describe('when boardNode exists NOT in the database but in the unit of work', () => { - it('should return the exact same object', async () => { - const columnBoard = columnBoardNodeFactory.build(); - await em.persistAndFlush(columnBoard); + describe('when loading a child', () => { + it('should ensure referential identity', async () => { + const { boardId, columnId } = await setup(); + + const resultBoard = await repo.findById(boardId); + const resultColumn = await repo.findById(columnId); + + expect(resultColumn === resultBoard.children[0]).toBe(true); + }); + }); + + describe('when loading a parent', () => { + it('should ensure referential identity', async () => { + const { boardId, columnId } = await setup(); + + const resultColumn = await repo.findById(columnId); + const resultBoard = await repo.findById(boardId); + + expect(resultColumn === resultBoard.children[0]).toBe(true); + }); + + describe('with limited depth', () => { + it('should keep referential identity', async () => { + const { boardId, columnId } = await setup(); + + const resultColumn = await repo.findById(columnId); + const resultBoard = await repo.findById(boardId, 1); + + expect(resultBoard.children[0] === resultColumn).toBe(true); + }); - await em.nativeDelete(ColumnBoardNode, columnBoard.id); + it('should not overwrite any descendants', async () => { + const { boardId, columnId, cardIds } = await setup(); - const boardNode = await repo.findById(columnBoard.id); + const resultColumn = await repo.findById(columnId); + await repo.findById(boardId, 1); + const resultCard1 = await repo.findById(cardIds[0]); + const resultCard2 = await repo.findById(cardIds[1]); - expect(columnBoard === boardNode).toBe(true); + expect(resultColumn.children[0] === resultCard1).toBe(true); + expect(resultColumn.children[1] === resultCard2).toBe(true); + }); }); }); }); diff --git a/apps/server/src/modules/board/repo/board-node.repo.ts b/apps/server/src/modules/board/repo/board-node.repo.ts index 5c3f3b5ebb3..c439f1a9d36 100644 --- a/apps/server/src/modules/board/repo/board-node.repo.ts +++ b/apps/server/src/modules/board/repo/board-node.repo.ts @@ -1,62 +1,205 @@ -import { EntityManager } from '@mikro-orm/mongodb'; +import { FilterQuery, Utils } from '@mikro-orm/core'; +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Injectable } from '@nestjs/common'; -import { BoardNode } from '@shared/domain/entity'; import { EntityId } from '@shared/domain/types'; +import { AnyBoardNode, BoardExternalReference, getBoardNodeType } from '../domain'; +import { pathOfChildren } from '../domain/path-utils'; +import { BoardNodeEntity } from './entity/board-node.entity'; +import { TreeBuilder } from './tree-builder'; @Injectable() export class BoardNodeRepo { constructor(private readonly em: EntityManager) {} - async findById(id: EntityId): Promise { - let entity = this.em.getUnitOfWork().getById(BoardNode.name, id); - if (entity) { - return entity; - } + async findById(id: EntityId, depth?: number): Promise { + const props = await this.em.findOneOrFail(BoardNodeEntity, { id }); + const descendants = await this.findDescendants(props, depth); + + const builder = new TreeBuilder(descendants); + const boardNode = builder.build(props); + + return boardNode; + } + + async findByIds(ids: EntityId[], depth?: number): Promise { + const entities = await this.em.find(BoardNodeEntity, { id: { $in: ids } }); + + // TODO refactor descendants mapping, more DRY? + const descendantsMap = await this.findDescendantsOfMany(entities, depth); + + const boardNodes = entities.map((props) => { + const descentants = descendantsMap[pathOfChildren(props)]; + const builder = new TreeBuilder(descentants); + const boardNode = builder.build(props); + + return boardNode; + }); + + return boardNodes; + } + + async findByExternalReference(reference: BoardExternalReference, depth?: number): Promise { + const entities = await this.em.find(BoardNodeEntity, { + context: { + _contextId: new ObjectId(reference.id), + _contextType: reference.type, + } as FilterQuery, + }); + + // TODO refactor descendants mapping, more DRY? + const descendantsMap = await this.findDescendantsOfMany(entities, depth); + + const boardNodes = entities.map((props) => { + const children = descendantsMap[pathOfChildren(props)]; + const builder = new TreeBuilder(children); + const boardNode = builder.build(props); + + return boardNode; + }); + + return boardNodes; + } + + async findByContextExternalToolIds(contextExternalToolIds: EntityId[], depth?: number): Promise { + const entities = await this.em.find(BoardNodeEntity, { + contextExternalToolId: { $in: contextExternalToolIds }, + }); + + // TODO refactor descendants mapping, more DRY? + const descendantsMap = await this.findDescendantsOfMany(entities, depth); + + const boardNodes = entities.map((props) => { + const children = descendantsMap[pathOfChildren(props)]; + const builder = new TreeBuilder(children); + const boardNode = builder.build(props); + + return boardNode; + }); + + return boardNodes; + } + + // TODO maybe we don't need that method + // async findCommonParentOfIds(ids: EntityId[], depth?: number): Promise { + // const entities = await this.em.find(BoardNodeEntity, { id: { $in: ids } }); + // const sortedPaths = entities.map((e) => e.path).sort(); + // const commonPath = sortedPaths[0]; + // const dontMatch = sortedPaths.some((p) => !p.startsWith(commonPath)); + + // if (!commonPath || commonPath === ROOT_PATH || dontMatch) { + // throw new EntityNotFoundError(`Parent node of [${ids.join(',')}] not found`); + // } + + // const commonAncestorIds = commonPath.split(',').filter((id) => id !== ''); + // const parentId = commonAncestorIds[commonAncestorIds.length - 1]; + // const parentNode = await this.findById(parentId, depth); + + // return parentNode; + // } + + async save(boardNode: AnyBoardNode | AnyBoardNode[]): Promise { + return this.persist(boardNode).flush(); + } - entity = await this.em.findOneOrFail(BoardNode, id); - return entity; + async delete(boardNode: AnyBoardNode | AnyBoardNode[]): Promise { + await this.remove(boardNode).flush(); } - async findDescendants(node: BoardNode, depth?: number): Promise { - const levelQuery = depth !== undefined ? { $gt: node.level, $lte: node.level + depth } : { $gt: node.level }; + private async findDescendants(props: BoardNodeEntity, depth?: number): Promise { + const levelQuery = depth !== undefined ? { $gt: props.level, $lte: props.level + depth } : { $gt: props.level }; - const descendants = await this.em.find(BoardNode, { - path: { $re: `^${node.pathOfChildren}` }, + const descendants = await this.em.find(BoardNodeEntity, { + path: { $re: `^${pathOfChildren(props)}` }, level: levelQuery, }); return descendants; } - async findDescendantsOfMany(nodes: BoardNode[]): Promise> { - const pathQueries = nodes.map((node) => { - return { path: { $re: `^${node.pathOfChildren}` } }; + private async findDescendantsOfMany( + entities: BoardNodeEntity[], + depth?: number + ): Promise> { + const pathQueries = entities.map((props) => { + const levelQuery = depth !== undefined ? { $gt: props.level, $lte: props.level + depth } : { $gt: props.level }; + + return { path: { $re: `^${pathOfChildren(props)}` }, level: levelQuery }; }); - const map: Record = {}; + const map: Record = {}; if (pathQueries.length === 0) { return map; } - const descendants = await this.em.find(BoardNode, { + const descendants = await this.em.find(BoardNodeEntity, { $or: pathQueries, }); - // this is for finding tha ancestors of a descendant + // this is for finding the ancestors of a descendant // we use this to group the descendants by ancestor // TODO we probably need a more efficient way to do the grouping - const matchAncestors = (descendant: BoardNode): BoardNode[] => { - const result = nodes.filter((n) => descendant.path.match(`^${n.pathOfChildren}`)); + const matchAncestors = (descendant: BoardNodeEntity): BoardNodeEntity[] => { + const result = entities.filter((props) => descendant.path.match(`^${pathOfChildren(props)}`)); return result; }; for (const desc of descendants) { - const ancestorNodes = matchAncestors(desc); - ancestorNodes.forEach((node) => { - map[node.pathOfChildren] ||= []; - map[node.pathOfChildren].push(desc); + const ancestors = matchAncestors(desc); + ancestors.forEach((props) => { + map[pathOfChildren(props)] ||= []; + map[pathOfChildren(props)].push(desc); }); } return map; } + + private persist(boardNode: AnyBoardNode | AnyBoardNode[]): BoardNodeRepo { + const boardNodes = Utils.asArray(boardNode); + + boardNodes.forEach((bn) => { + bn.children.forEach((child) => this.persist(child)); + + const props = this.getProps(bn); + + if (!(props instanceof BoardNodeEntity)) { + const entity = this.em.create(BoardNodeEntity, props); + entity.type = getBoardNodeType(bn); + this.setProps(bn, entity); + this.em.persist(entity); + } else { + // for the unlikely case that the props are not managed yet + this.em.persist(props); + } + }); + + return this; + } + + private remove(boardNode: AnyBoardNode | AnyBoardNode[]): BoardNodeRepo { + const boardNodes = Utils.asArray(boardNode); + + boardNodes.forEach((bn) => { + this.em.remove(this.getProps(bn)); + bn.children.forEach((child) => this.remove(child)); + }); + + return this; + } + + private async flush(): Promise { + return this.em.flush(); + } + + private getProps(boardNode: AnyBoardNode): BoardNodeEntity { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const { props } = boardNode; + return props as BoardNodeEntity; + } + + private setProps(boardNode: AnyBoardNode, props: BoardNodeEntity): void { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + boardNode.props = props; + } } diff --git a/apps/server/src/modules/board/repo/entity/board-node.entity.spec.ts b/apps/server/src/modules/board/repo/entity/board-node.entity.spec.ts new file mode 100644 index 00000000000..55c76a6cb66 --- /dev/null +++ b/apps/server/src/modules/board/repo/entity/board-node.entity.spec.ts @@ -0,0 +1,70 @@ +import { MikroORM, ObjectId } from '@mikro-orm/mongodb'; +import { BaseEntityWithTimestamps } from '@shared/domain/entity'; +import { BoardExternalReferenceType, BoardNodeType } from '../../domain'; +import { columnBoardEntityFactory } from '../../testing'; +import { BoardNodeEntity } from './board-node.entity'; +import { Context } from './embeddables'; + +describe('entity', () => { + let orm: MikroORM; + + beforeAll(async () => { + orm = await MikroORM.init({ + entities: [BaseEntityWithTimestamps, BoardNodeEntity], + clientUrl: 'mongodb://localhost:27017/boardtest', + type: 'mongo', + validate: true, + allowGlobalContext: true, + }); + }); + + beforeEach(async () => { + await orm.schema.clearDatabase(); + }); + + afterAll(async () => { + await orm.schema.dropSchema(); + await orm.close(true); + }); + + describe('context', () => { + it('should persist the property', async () => { + const entity = new BoardNodeEntity(); + entity.type = BoardNodeType.COLUMN_BOARD; + entity.context = new Context({ + type: BoardExternalReferenceType.Course, + id: new ObjectId().toHexString(), + }); + + await orm.em.persistAndFlush(entity); + orm.em.clear(); + + const result = await orm.em.findOneOrFail(BoardNodeEntity, { id: entity.id }); + expect(result.context).toEqual(entity.context); + }); + + it('should persist factory generated object', async () => { + const entity = columnBoardEntityFactory.build(); + + await orm.em.persistAndFlush(entity); + orm.em.clear(); + + const result = await orm.em.findOneOrFail(BoardNodeEntity, { id: entity.id }); + expect(result.context).toEqual(entity.context); + }); + }); + + describe('contextExternalToolId', () => { + it('should persist the property', async () => { + const entity = new BoardNodeEntity(); + entity.type = BoardNodeType.EXTERNAL_TOOL; + entity.contextExternalToolId = new ObjectId().toHexString(); + + await orm.em.persistAndFlush(entity); + orm.em.clear(); + + const result = await orm.em.findOneOrFail(BoardNodeEntity, { id: entity.id }); + expect(result.contextExternalToolId).toBe(entity.contextExternalToolId); + }); + }); +}); diff --git a/apps/server/src/modules/board/repo/entity/board-node.entity.ts b/apps/server/src/modules/board/repo/entity/board-node.entity.ts new file mode 100644 index 00000000000..a19ea9cea70 --- /dev/null +++ b/apps/server/src/modules/board/repo/entity/board-node.entity.ts @@ -0,0 +1,110 @@ +import { Embedded, Entity, Enum, Index, Property } from '@mikro-orm/core'; +import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; +import { EntityId, InputFormat } from '@shared/domain/types'; +import { ObjectIdType } from '@shared/repo/types/object-id.type'; +import { AnyBoardNode, BoardLayout, BoardNodeType, ROOT_PATH } from '../../domain'; +import { MediaBoardColors } from '../../domain/media-board/types'; +import type { BoardNodeEntityProps } from '../types'; +import { Context } from './embeddables'; + +@Entity({ tableName: 'boardnodes' }) +export class BoardNodeEntity extends BaseEntityWithTimestamps implements BoardNodeEntityProps { + // Generic Tree + // -------------------------------------------------------------------------- + @Index() + @Property({ nullable: false }) + path = ROOT_PATH; + + @Property({ nullable: false, type: 'integer' }) + level = 0; + + @Property({ nullable: false, type: 'integer' }) + position = 0; + + @Index() + @Enum(() => BoardNodeType) + type!: BoardNodeType; + + @Property({ persist: false }) + children: AnyBoardNode[] = []; + + @Property({ persist: false }) + domainObject: AnyBoardNode | undefined; + + // Card, Column, ColumnBoard, LinkElement, MedialLine + // -------------------------------------------------------------------------- + @Property({ nullable: true }) + title: string | undefined; + + // LinkElement, DrawingElement + @Property({ type: 'string', nullable: true }) + description: string | undefined; + + // ColumnBoard, MediaBoard + // -------------------------------------------------------------------------- + @Embedded(() => Context, { prefix: false, nullable: true }) + context: BoardNodeEntityProps['context'] | undefined; + + @Enum({ type: 'BoardLayout', nullable: true }) + layout: BoardLayout | undefined; + + // ColumnBoard + // -------------------------------------------------------------------------- + @Property({ type: 'boolean', nullable: true }) + isVisible: boolean | undefined; + + // Card + // -------------------------------------------------------------------------- + @Property({ type: 'integer', nullable: true }) + height: number | undefined; + + // RichTextElement + // -------------------------------------------------------------------------- + @Property({ type: 'string', nullable: true }) + text: string | undefined; + + @Enum({ type: 'InputFormat', nullable: true }) + inputFormat: InputFormat | undefined; + + // LinkElement + // -------------------------------------------------------------------------- + @Property({ type: 'string', nullable: true }) + url: string | undefined; + + @Property({ type: 'string', nullable: true }) + imageUrl: string | undefined; + + // FileElement + // -------------------------------------------------------------------------- + @Property({ type: 'string', nullable: true }) + caption: string | undefined; + + @Property({ type: 'string', nullable: true }) + alternativeText: string | undefined; + + // SubmissionContainerElement + // -------------------------------------------------------------------------- + @Property({ type: 'Date', nullable: true }) + dueDate: Date | undefined; + + // SubmissionItem + // -------------------------------------------------------------------------- + @Property({ type: 'boolean', nullable: true }) + completed: boolean | undefined; + + @Property({ type: ObjectIdType, nullable: true }) + userId: EntityId | undefined; + + // ExternalToolElement, MediaExternalToolElement + // -------------------------------------------------------------------------- + @Property({ type: ObjectIdType, fieldName: 'contextExternalTool', nullable: true }) + contextExternalToolId: EntityId | undefined; + + // MediaLine, MediaBoard + // -------------------------------------------------------------------------- + @Property({ type: 'boolean', nullable: true }) + collapsed: boolean | undefined; + + @Property({ type: 'MediaBoardColors', nullable: true }) + backgroundColor: MediaBoardColors | undefined; +} diff --git a/apps/server/src/modules/board/repo/entity/embeddables/context.ts b/apps/server/src/modules/board/repo/entity/embeddables/context.ts new file mode 100644 index 00000000000..3ec17f9d78f --- /dev/null +++ b/apps/server/src/modules/board/repo/entity/embeddables/context.ts @@ -0,0 +1,26 @@ +import { Embeddable, Property } from '@mikro-orm/core'; +import type { EntityId } from '@shared/domain/types'; +import { ObjectIdType } from '@shared/repo/types/object-id.type'; +import { BoardExternalReference, BoardExternalReferenceType } from '../../../domain'; + +@Embeddable() +export class Context implements BoardExternalReference { + constructor(props: BoardExternalReference) { + this._contextType = props.type; + this._contextId = props.id; + } + + @Property({ fieldName: 'contextType' }) + _contextType: BoardExternalReferenceType; + + @Property({ fieldName: 'context', type: ObjectIdType }) + _contextId: EntityId; + + get type(): BoardExternalReferenceType { + return this._contextType; + } + + get id(): EntityId { + return this._contextId; + } +} diff --git a/apps/server/src/modules/board/repo/entity/embeddables/index.ts b/apps/server/src/modules/board/repo/entity/embeddables/index.ts new file mode 100644 index 00000000000..c38e8e82152 --- /dev/null +++ b/apps/server/src/modules/board/repo/entity/embeddables/index.ts @@ -0,0 +1 @@ +export * from './context'; diff --git a/apps/server/src/modules/board/repo/entity/index.ts b/apps/server/src/modules/board/repo/entity/index.ts new file mode 100644 index 00000000000..29bfeb886c0 --- /dev/null +++ b/apps/server/src/modules/board/repo/entity/index.ts @@ -0,0 +1 @@ +export * from './board-node.entity'; diff --git a/apps/server/src/modules/board/repo/index.ts b/apps/server/src/modules/board/repo/index.ts index a771a40437b..b0c2e106878 100644 --- a/apps/server/src/modules/board/repo/index.ts +++ b/apps/server/src/modules/board/repo/index.ts @@ -1,3 +1,2 @@ -export * from './board-do.repo'; export * from './board-node.repo'; -export * from './recursive-delete.vistor'; +export * from './entity'; 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 deleted file mode 100644 index 7a4acaf96ab..00000000000 --- a/apps/server/src/modules/board/repo/recursive-delete.visitor.spec.ts +++ /dev/null @@ -1,528 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { FileRecordParentType } from '@infra/rabbitmq'; -import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; -import { CollaborativeTextEditorService } from '@modules/collaborative-text-editor'; -import { FileDto, FilesStorageClientAdapterService } from '@modules/files-storage-client'; -import { DrawingElementAdapterService } from '@modules/tldraw-client'; -import { ContextExternalToolService } from '@modules/tool/context-external-tool/service'; -import { contextExternalToolFactory } from '@modules/tool/context-external-tool/testing'; -import { Test, TestingModule } from '@nestjs/testing'; -import { - collaborativeTextEditorElementFactory, - columnBoardFactory, - columnFactory, - drawingElementFactory, - externalToolElementFactory, - fileElementFactory, - linkElementFactory, - mediaBoardFactory, - mediaExternalToolElementFactory, - mediaLineFactory, - setupEntities, - submissionContainerElementFactory, - submissionItemFactory, -} from '@shared/testing'; -import { RecursiveDeleteVisitor } from './recursive-delete.vistor'; - -describe(RecursiveDeleteVisitor.name, () => { - let module: TestingModule; - let service: RecursiveDeleteVisitor; - - let em: DeepMocked; - let filesStorageClientAdapterService: DeepMocked; - let contextExternalToolService: DeepMocked; - let drawingElementAdapterService: DeepMocked; - let collaborativeTextEditorService: DeepMocked; - - beforeAll(async () => { - module = await Test.createTestingModule({ - providers: [ - RecursiveDeleteVisitor, - { provide: EntityManager, useValue: createMock() }, - { provide: FilesStorageClientAdapterService, useValue: createMock() }, - { provide: ContextExternalToolService, useValue: createMock() }, - { provide: DrawingElementAdapterService, useValue: createMock() }, - { provide: CollaborativeTextEditorService, useValue: createMock() }, - ], - }).compile(); - - service = module.get(RecursiveDeleteVisitor); - em = module.get(EntityManager); - filesStorageClientAdapterService = module.get(FilesStorageClientAdapterService); - contextExternalToolService = module.get(ContextExternalToolService); - drawingElementAdapterService = module.get(DrawingElementAdapterService); - collaborativeTextEditorService = module.get(CollaborativeTextEditorService); - - await setupEntities(); - }); - - afterAll(async () => { - await module.close(); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - describe('when used as a visitor on a board composite', () => { - describe('acceptAsync', () => { - it('should delete the board node', async () => { - const board = columnBoardFactory.build(); - - await board.acceptAsync(service); - - expect(em.remove).toHaveBeenCalled(); - }); - - it('should make the children accept the service', async () => { - const columns = columnFactory.buildList(2).map((col) => { - col.acceptAsync = jest.fn(); - return col; - }); - const board = columnBoardFactory.build({ children: columns }); - - await board.acceptAsync(service); - - expect(columns[0].acceptAsync).toHaveBeenCalledWith(service); - expect(columns[1].acceptAsync).toHaveBeenCalledWith(service); - }); - }); - }); - - describe('when used as a visitor on a media board composite', () => { - describe('acceptAsync', () => { - it('should delete the board node', async () => { - const board = mediaBoardFactory.build(); - - await board.acceptAsync(service); - - expect(em.remove).toHaveBeenCalled(); - }); - - it('should make the children accept the service', async () => { - const lines = mediaLineFactory.buildList(2).map((lin) => { - lin.acceptAsync = jest.fn(); - return lin; - }); - const board = mediaBoardFactory.build({ children: lines }); - - await board.acceptAsync(service); - - expect(lines[0].acceptAsync).toHaveBeenCalledWith(service); - expect(lines[1].acceptAsync).toHaveBeenCalledWith(service); - }); - }); - }); - - describe('visitFileElementAsync', () => { - describe('WHEN file element, child and files are deleted successfully', () => { - const setup = () => { - const childFileElement = fileElementFactory.build(); - const fileElement = fileElementFactory.build({ children: [childFileElement] }); - - return { fileElement, childFileElement }; - }; - - it('should call deleteFilesOfParent', async () => { - const { fileElement, childFileElement } = setup(); - - await service.visitFileElementAsync(fileElement); - - expect(filesStorageClientAdapterService.deleteFilesOfParent).toHaveBeenCalledWith(fileElement.id); - expect(filesStorageClientAdapterService.deleteFilesOfParent).toHaveBeenCalledWith(childFileElement.id); - }); - - it('should call deleteNode', async () => { - const { fileElement, childFileElement } = setup(); - - await service.visitFileElementAsync(fileElement); - - expect(em.remove).toHaveBeenCalledWith(em.getReference(fileElement.constructor, fileElement.id)); - expect(em.remove).toHaveBeenCalledWith(em.getReference(childFileElement.constructor, childFileElement.id)); - }); - }); - - describe('WHEN deleteFilesOfParent of file element returns an error', () => { - const setup = () => { - const fileElement = fileElementFactory.build(); - const error = new Error('testError'); - filesStorageClientAdapterService.deleteFilesOfParent.mockRejectedValueOnce(error); - - return { fileElement, error }; - }; - - it('should pass error', async () => { - const { fileElement, error } = setup(); - - await expect(service.visitFileElementAsync(fileElement)).rejects.toThrowError(error); - }); - - it('should not call deleteNode', async () => { - const { fileElement, error } = setup(); - - await expect(service.visitFileElementAsync(fileElement)).rejects.toThrowError(error); - expect(em.remove).not.toHaveBeenCalled(); - }); - }); - - describe('WHEN deleteFilesOfParent of child file element returns an error', () => { - const setup = () => { - const childFileElement = fileElementFactory.build(); - const fileElement = fileElementFactory.build({ children: [childFileElement] }); - const error = new Error('testError'); - const fileDto = new FileDto({ - id: 'testId', - name: 'testName', - parentType: FileRecordParentType.BoardNode, - parentId: 'testParentId', - }); - - filesStorageClientAdapterService.deleteFilesOfParent.mockResolvedValueOnce([fileDto]); - filesStorageClientAdapterService.deleteFilesOfParent.mockRejectedValueOnce(error); - - return { fileElement, childFileElement, error }; - }; - - it('should pass error', async () => { - const { fileElement, error } = setup(); - - await expect(service.visitFileElementAsync(fileElement)).rejects.toThrowError(error); - }); - }); - }); - - describe('visitLinkElementAsync', () => { - const setup = () => { - const childLinkElement = linkElementFactory.build(); - const linkElement = linkElementFactory.build({ - children: [childLinkElement], - }); - - return { linkElement, childLinkElement }; - }; - - it('should call entity remove', async () => { - const { linkElement, childLinkElement } = setup(); - - await service.visitLinkElementAsync(linkElement); - - 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('visitDrawingElementAsync', () => { - const setup = () => { - const childDrawingElement = drawingElementFactory.build(); - - return { childDrawingElement }; - }; - - it('should call entity remove', async () => { - const { childDrawingElement } = setup(); - - await service.visitDrawingElementAsync(childDrawingElement); - - expect(em.remove).toHaveBeenCalledWith(em.getReference(childDrawingElement.constructor, childDrawingElement.id)); - }); - - it('should trigger deletion of tldraw data via adapter', async () => { - const { childDrawingElement } = setup(); - - await service.visitDrawingElementAsync(childDrawingElement); - - expect(drawingElementAdapterService.deleteDrawingBinData).toHaveBeenCalledWith(childDrawingElement.id); - }); - }); - - describe('visitSubmissionContainerElementAsync', () => { - const setup = () => { - const childSubmissionContainerElement = submissionContainerElementFactory.build(); - const submissionContainerElement = submissionContainerElementFactory.build({ - children: [childSubmissionContainerElement], - }); - - return { submissionContainerElement, childSubmissionContainerElement }; - }; - - it('should call entity remove', async () => { - const { submissionContainerElement, childSubmissionContainerElement } = setup(); - - await service.visitSubmissionContainerElementAsync(submissionContainerElement); - - expect(em.remove).toHaveBeenCalledWith( - em.getReference(submissionContainerElement.constructor, submissionContainerElement.id) - ); - expect(em.remove).toHaveBeenCalledWith( - em.getReference(childSubmissionContainerElement.constructor, childSubmissionContainerElement.id) - ); - }); - }); - - describe('visitSubmissionItemAsync', () => { - const setup = () => { - const childSubmissionItem = submissionItemFactory.build(); - const submissionItem = submissionItemFactory.build({ - children: [childSubmissionItem], - }); - - return { submissionItem, childSubmissionItem }; - }; - - it('should call entity remove', async () => { - const { submissionItem, childSubmissionItem } = setup(); - - await service.visitSubmissionItemAsync(submissionItem); - - expect(em.remove).toHaveBeenCalledWith(em.getReference(submissionItem.constructor, submissionItem.id)); - expect(em.remove).toHaveBeenCalledWith(em.getReference(childSubmissionItem.constructor, childSubmissionItem.id)); - }); - }); - - describe('visitExternalToolElementAsync', () => { - describe('when the linked context external tool exists', () => { - const setup = () => { - const contextExternalTool = contextExternalToolFactory.buildWithId(); - const childExternalToolElement = externalToolElementFactory.build(); - const externalToolElement = externalToolElementFactory.build({ - children: [childExternalToolElement], - contextExternalToolId: contextExternalTool.id, - }); - - contextExternalToolService.findById.mockResolvedValue(contextExternalTool); - - return { - externalToolElement, - childExternalToolElement, - contextExternalTool, - }; - }; - - it('should delete the context external tool that is linked to the element', async () => { - const { externalToolElement, contextExternalTool } = setup(); - - await service.visitExternalToolElementAsync(externalToolElement); - - expect(contextExternalToolService.deleteContextExternalTool).toHaveBeenCalledWith(contextExternalTool); - }); - - it('should call entity remove', async () => { - const { externalToolElement, childExternalToolElement } = setup(); - - await service.visitExternalToolElementAsync(externalToolElement); - - expect(em.remove).toHaveBeenCalledWith( - em.getReference(externalToolElement.constructor, externalToolElement.id) - ); - expect(em.remove).toHaveBeenCalledWith( - em.getReference(childExternalToolElement.constructor, childExternalToolElement.id) - ); - }); - }); - - describe('when the external tool does not exist anymore', () => { - const setup = () => { - const childExternalToolElement = externalToolElementFactory.build(); - const externalToolElement = externalToolElementFactory.build({ - children: [childExternalToolElement], - contextExternalToolId: new ObjectId().toHexString(), - }); - - contextExternalToolService.findById.mockResolvedValue(null); - - return { - externalToolElement, - childExternalToolElement, - }; - }; - - it('should not try to delete the context external tool that is linked to the element', async () => { - const { externalToolElement } = setup(); - - await service.visitExternalToolElementAsync(externalToolElement); - - expect(contextExternalToolService.deleteContextExternalTool).not.toHaveBeenCalled(); - }); - - it('should call entity remove', async () => { - const { externalToolElement, childExternalToolElement } = setup(); - - await service.visitExternalToolElementAsync(externalToolElement); - - expect(em.remove).toHaveBeenCalledWith( - em.getReference(externalToolElement.constructor, externalToolElement.id) - ); - expect(em.remove).toHaveBeenCalledWith( - em.getReference(childExternalToolElement.constructor, childExternalToolElement.id) - ); - }); - }); - }); - - describe('visitMediaExternalToolElementAsync', () => { - describe('when the linked context external tool exists', () => { - const setup = () => { - const contextExternalTool = contextExternalToolFactory.buildWithId(); - const childMediaExternalToolElement = mediaExternalToolElementFactory.build(); - const mediaExternalToolElement = mediaExternalToolElementFactory.build({ - children: [childMediaExternalToolElement], - contextExternalToolId: contextExternalTool.id, - }); - - contextExternalToolService.findById.mockResolvedValue(contextExternalTool); - - return { - mediaExternalToolElement, - childMediaExternalToolElement, - contextExternalTool, - }; - }; - - it('should delete the context external tool that is linked to the element', async () => { - const { mediaExternalToolElement, contextExternalTool } = setup(); - - await service.visitMediaExternalToolElementAsync(mediaExternalToolElement); - - expect(contextExternalToolService.deleteContextExternalTool).toHaveBeenCalledWith(contextExternalTool); - }); - - it('should call entity remove', async () => { - const { mediaExternalToolElement, childMediaExternalToolElement } = setup(); - - await service.visitMediaExternalToolElementAsync(mediaExternalToolElement); - - expect(em.remove).toHaveBeenCalledWith( - em.getReference(mediaExternalToolElement.constructor, mediaExternalToolElement.id) - ); - expect(em.remove).toHaveBeenCalledWith( - em.getReference(childMediaExternalToolElement.constructor, childMediaExternalToolElement.id) - ); - }); - }); - - describe('when the external tool does not exist anymore', () => { - const setup = () => { - const childMediaExternalToolElement = mediaExternalToolElementFactory.build(); - const mediaExternalToolElement = mediaExternalToolElementFactory.build({ - children: [childMediaExternalToolElement], - contextExternalToolId: new ObjectId().toHexString(), - }); - - contextExternalToolService.findById.mockResolvedValue(null); - - return { - mediaExternalToolElement, - childMediaExternalToolElement, - }; - }; - - it('should not try to delete the context external tool that is linked to the element', async () => { - const { mediaExternalToolElement } = setup(); - - await service.visitMediaExternalToolElementAsync(mediaExternalToolElement); - - expect(contextExternalToolService.deleteContextExternalTool).not.toHaveBeenCalled(); - }); - - it('should call entity remove', async () => { - const { mediaExternalToolElement, childMediaExternalToolElement } = setup(); - - await service.visitMediaExternalToolElementAsync(mediaExternalToolElement); - - expect(em.remove).toHaveBeenCalledWith( - em.getReference(mediaExternalToolElement.constructor, mediaExternalToolElement.id) - ); - expect(em.remove).toHaveBeenCalledWith( - em.getReference(childMediaExternalToolElement.constructor, childMediaExternalToolElement.id) - ); - }); - }); - }); - - describe('visitCollaborativeTextEditorAsync', () => { - describe('WHEN collaborative text editor element is deleted successfully', () => { - const setup = () => { - const childCollaborativeTextEditorElement = collaborativeTextEditorElementFactory.build(); - const collaborativeTextEditorElement = collaborativeTextEditorElementFactory.build({ - children: [childCollaborativeTextEditorElement], - }); - - return { collaborativeTextEditorElement, childCollaborativeTextEditorElement }; - }; - - it('should call deleteCollaborativeTextEditorByParentId', async () => { - const { collaborativeTextEditorElement } = setup(); - - await service.visitCollaborativeTextEditorElementAsync(collaborativeTextEditorElement); - - expect(collaborativeTextEditorService.deleteCollaborativeTextEditorByParentId).toHaveBeenCalledWith( - collaborativeTextEditorElement.id - ); - }); - - it('should call entity remove', async () => { - const { collaborativeTextEditorElement, childCollaborativeTextEditorElement } = setup(); - - await service.visitCollaborativeTextEditorElementAsync(collaborativeTextEditorElement); - - expect(em.remove).toHaveBeenCalledWith( - em.getReference(collaborativeTextEditorElement.constructor, collaborativeTextEditorElement.id) - ); - expect(em.remove).toHaveBeenCalledWith( - em.getReference(childCollaborativeTextEditorElement.constructor, childCollaborativeTextEditorElement.id) - ); - }); - }); - - describe('WHEN deleteCollaborativeTextEditorByParentId returns error', () => { - const setup = () => { - const childCollaborativeTextEditorElement = collaborativeTextEditorElementFactory.build(); - const collaborativeTextEditorElement = collaborativeTextEditorElementFactory.build({ - children: [childCollaborativeTextEditorElement], - }); - const error = new Error('testError'); - collaborativeTextEditorService.deleteCollaborativeTextEditorByParentId.mockRejectedValueOnce(error); - - return { collaborativeTextEditorElement, childCollaborativeTextEditorElement, error }; - }; - - it('should return error', async () => { - const { collaborativeTextEditorElement, error } = setup(); - - await expect( - service.visitCollaborativeTextEditorElementAsync(collaborativeTextEditorElement) - ).rejects.toThrowError(error); - }); - }); - - describe('WHEN visitChildrenAsync returns error', () => { - const setup = () => { - const childCollaborativeTextEditorElement = collaborativeTextEditorElementFactory.build(); - const collaborativeTextEditorElement = collaborativeTextEditorElementFactory.build({ - children: [childCollaborativeTextEditorElement], - }); - const error = new Error('testError'); - collaborativeTextEditorService.deleteCollaborativeTextEditorByParentId.mockResolvedValueOnce(); - collaborativeTextEditorService.deleteCollaborativeTextEditorByParentId.mockRejectedValueOnce(error); - - return { collaborativeTextEditorElement, childCollaborativeTextEditorElement, error }; - }; - - it('should return error', async () => { - const { collaborativeTextEditorElement, error } = setup(); - - await expect( - service.visitCollaborativeTextEditorElementAsync(collaborativeTextEditorElement) - ).rejects.toThrowError(error); - }); - }); - }); -}); diff --git a/apps/server/src/modules/board/repo/recursive-delete.vistor.ts b/apps/server/src/modules/board/repo/recursive-delete.vistor.ts deleted file mode 100644 index 3b29e82edd5..00000000000 --- a/apps/server/src/modules/board/repo/recursive-delete.vistor.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { EntityManager } from '@mikro-orm/mongodb'; -import { CollaborativeTextEditorService } from '@modules/collaborative-text-editor'; -import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; -import { DrawingElementAdapterService } from '@modules/tldraw-client'; -import type { ContextExternalTool } from '@modules/tool/context-external-tool/domain'; -import { ContextExternalToolService } from '@modules/tool/context-external-tool/service'; -import { Injectable } from '@nestjs/common'; -import type { - AnyBoardDo, - BoardCompositeVisitorAsync, - Card, - CollaborativeTextEditorElement, - Column, - ColumnBoard, - DrawingElement, - ExternalToolElement, - FileElement, - LinkElement, - MediaBoard, - MediaExternalToolElement, - MediaLine, - RichTextElement, - SubmissionContainerElement, - SubmissionItem, -} from '@shared/domain/domainobject'; -import { BoardNode } from '@shared/domain/entity'; - -@Injectable() -export class RecursiveDeleteVisitor implements BoardCompositeVisitorAsync { - constructor( - private readonly em: EntityManager, - private readonly filesStorageClientAdapterService: FilesStorageClientAdapterService, - private readonly contextExternalToolService: ContextExternalToolService, - private readonly drawingElementAdapterService: DrawingElementAdapterService, - private readonly collaborativeTextEditorService: CollaborativeTextEditorService - ) {} - - async visitColumnBoardAsync(columnBoard: ColumnBoard): Promise { - this.deleteNode(columnBoard); - await this.visitChildrenAsync(columnBoard); - } - - async visitColumnAsync(column: Column): Promise { - this.deleteNode(column); - await this.visitChildrenAsync(column); - } - - async visitCardAsync(card: Card): Promise { - this.deleteNode(card); - await this.visitChildrenAsync(card); - } - - async visitFileElementAsync(fileElement: FileElement): Promise { - await this.filesStorageClientAdapterService.deleteFilesOfParent(fileElement.id); - this.deleteNode(fileElement); - - await this.visitChildrenAsync(fileElement); - } - - async visitLinkElementAsync(linkElement: LinkElement): Promise { - await this.filesStorageClientAdapterService.deleteFilesOfParent(linkElement.id); - this.deleteNode(linkElement); - - await this.visitChildrenAsync(linkElement); - } - - async visitRichTextElementAsync(richTextElement: RichTextElement): Promise { - this.deleteNode(richTextElement); - await this.visitChildrenAsync(richTextElement); - } - - async visitDrawingElementAsync(drawingElement: DrawingElement): Promise { - await this.drawingElementAdapterService.deleteDrawingBinData(drawingElement.id); - await this.filesStorageClientAdapterService.deleteFilesOfParent(drawingElement.id); - - this.deleteNode(drawingElement); - await this.visitChildrenAsync(drawingElement); - } - - async visitSubmissionContainerElementAsync(submissionContainerElement: SubmissionContainerElement): Promise { - this.deleteNode(submissionContainerElement); - await this.visitChildrenAsync(submissionContainerElement); - } - - async visitSubmissionItemAsync(submission: SubmissionItem): Promise { - this.deleteNode(submission); - await this.visitChildrenAsync(submission); - } - - async visitExternalToolElementAsync(externalToolElement: ExternalToolElement): Promise { - if (externalToolElement.contextExternalToolId) { - const linkedTool: ContextExternalTool | null = await this.contextExternalToolService.findById( - externalToolElement.contextExternalToolId - ); - - if (linkedTool) { - await this.contextExternalToolService.deleteContextExternalTool(linkedTool); - } - } - - this.deleteNode(externalToolElement); - - await this.visitChildrenAsync(externalToolElement); - } - - async visitCollaborativeTextEditorElementAsync( - collaborativeTextEditorElement: CollaborativeTextEditorElement - ): Promise { - await this.collaborativeTextEditorService.deleteCollaborativeTextEditorByParentId( - collaborativeTextEditorElement.id - ); - this.deleteNode(collaborativeTextEditorElement); - await this.visitChildrenAsync(collaborativeTextEditorElement); - } - - async visitMediaBoardAsync(mediaBoard: MediaBoard): Promise { - this.deleteNode(mediaBoard); - await this.visitChildrenAsync(mediaBoard); - } - - async visitMediaLineAsync(mediaLine: MediaLine): Promise { - this.deleteNode(mediaLine); - await this.visitChildrenAsync(mediaLine); - } - - async visitMediaExternalToolElementAsync(mediaElement: MediaExternalToolElement): Promise { - this.deleteNode(mediaElement); - - const linkedTool: ContextExternalTool | null = await this.contextExternalToolService.findById( - mediaElement.contextExternalToolId - ); - - if (linkedTool) { - await this.contextExternalToolService.deleteContextExternalTool(linkedTool); - } - - await this.visitChildrenAsync(mediaElement); - } - - deleteNode(domainObject: AnyBoardDo): void { - this.em.remove(this.em.getReference(BoardNode, domainObject.id)); - } - - async visitChildrenAsync(domainObject: AnyBoardDo): Promise { - await Promise.all(domainObject.children.map(async (child) => child.acceptAsync(this))); - } -} diff --git a/apps/server/src/modules/board/repo/recursive-save.visitor.spec.ts b/apps/server/src/modules/board/repo/recursive-save.visitor.spec.ts deleted file mode 100644 index 947c50d8dc1..00000000000 --- a/apps/server/src/modules/board/repo/recursive-save.visitor.spec.ts +++ /dev/null @@ -1,404 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { EntityManager } from '@mikro-orm/mongodb'; -import { contextExternalToolEntityFactory } from '@modules/tool/context-external-tool/testing'; -import { - BoardNodeType, - CardNode, - CollaborativeTextEditorElementNode, - ColumnBoardNode, - ColumnNode, - DrawingElementNode, - ExternalToolElementNodeEntity, - FileElementNode, - LinkElementNode, - MediaBoardNode, - MediaExternalToolElementNode, - MediaLineNode, - RichTextElementNode, - SubmissionContainerElementNode, - SubmissionItemNode, -} from '@shared/domain/entity'; -import { - cardFactory, - collaborativeTextEditorElementFactory, - columnBoardFactory, - columnBoardNodeFactory, - columnFactory, - drawingElementFactory, - externalToolElementFactory, - fileElementFactory, - linkElementFactory, - mediaBoardFactory, - mediaExternalToolElementFactory, - mediaLineFactory, - richTextElementFactory, - setupEntities, - submissionContainerElementFactory, - submissionItemFactory, -} from '@shared/testing'; -import { MediaBoardColors, MediaBoardLayoutType } from '../domain'; -import { BoardNodeRepo } from './board-node.repo'; -import { RecursiveSaveVisitor } from './recursive-save.visitor'; - -describe(RecursiveSaveVisitor.name, () => { - let visitor: RecursiveSaveVisitor; - let em: DeepMocked; - let boardNodeRepo: DeepMocked; - - beforeAll(async () => { - em = createMock(); - boardNodeRepo = createMock(); - - await setupEntities(); - - visitor = new RecursiveSaveVisitor(em, boardNodeRepo); - }); - - describe('when visiting a board composite', () => { - it('should create or update the node', () => { - const board = columnBoardFactory.build(); - jest.spyOn(visitor, 'createOrUpdateBoardNode'); - - visitor.visitColumnBoard(board); - - const expectedNode: Partial = { - id: board.id, - type: BoardNodeType.COLUMN_BOARD, - title: board.title, - }; - expect(visitor.createOrUpdateBoardNode).toHaveBeenCalledWith(expect.objectContaining(expectedNode)); - }); - - it('should visit the children', () => { - const column = columnFactory.build(); - jest.spyOn(column, 'accept'); - const board = columnBoardFactory.build({ children: [column] }); - - board.accept(visitor); - - expect(column.accept).toHaveBeenCalledWith(visitor); - }); - }); - - describe('when visiting a column composite', () => { - it('should create or update the node', () => { - const column = columnFactory.build(); - jest.spyOn(visitor, 'createOrUpdateBoardNode'); - - visitor.visitColumn(column); - - const expectedNode: Partial = { - id: column.id, - type: BoardNodeType.COLUMN, - title: column.title, - }; - expect(visitor.createOrUpdateBoardNode).toHaveBeenCalledWith(expect.objectContaining(expectedNode)); - }); - - it('should visit the children', () => { - const card = cardFactory.build(); - jest.spyOn(card, 'accept'); - const column = columnFactory.build({ children: [card] }); - - column.accept(visitor); - - expect(card.accept).toHaveBeenCalledWith(visitor); - }); - }); - - describe('when visiting a card composite', () => { - it('should create or update the node', () => { - const card = cardFactory.build(); - jest.spyOn(visitor, 'createOrUpdateBoardNode'); - - visitor.visitCard(card); - - const expectedNode: Partial = { - id: card.id, - type: BoardNodeType.CARD, - height: card.height, - title: card.title, - }; - expect(visitor.createOrUpdateBoardNode).toHaveBeenCalledWith(expect.objectContaining(expectedNode)); - }); - - it('should visit the children', () => { - const richTextElement = richTextElementFactory.build(); - jest.spyOn(richTextElement, 'accept'); - const card = cardFactory.build({ children: [richTextElement] }); - - card.accept(visitor); - - expect(richTextElement.accept).toHaveBeenCalledWith(visitor); - }); - - it('should visit the children (drawing)', () => { - const drawingElement = drawingElementFactory.build(); - jest.spyOn(drawingElement, 'accept'); - const card = cardFactory.build({ children: [drawingElement] }); - - card.accept(visitor); - - expect(drawingElement.accept).toHaveBeenCalledWith(visitor); - }); - }); - - describe('when visiting a file element composite', () => { - it('should create or update the node', () => { - const fileElement = fileElementFactory.build(); - jest.spyOn(visitor, 'createOrUpdateBoardNode'); - - visitor.visitFileElement(fileElement); - - const expectedNode: Partial = { - id: fileElement.id, - type: BoardNodeType.FILE_ELEMENT, - caption: fileElement.caption, - alternativeText: fileElement.alternativeText, - }; - expect(visitor.createOrUpdateBoardNode).toHaveBeenCalledWith(expect.objectContaining(expectedNode)); - }); - }); - - describe('when visiting a link element composite', () => { - it('should create or update the node', () => { - const linkElement = linkElementFactory.build(); - jest.spyOn(visitor, 'createOrUpdateBoardNode'); - - visitor.visitLinkElement(linkElement); - - const expectedNode: Partial = { - id: linkElement.id, - type: BoardNodeType.LINK_ELEMENT, - url: linkElement.url, - }; - expect(visitor.createOrUpdateBoardNode).toHaveBeenCalledWith(expect.objectContaining(expectedNode)); - }); - }); - - describe('when visiting a collaborative text editor element composite', () => { - it('should create or update the node', () => { - const collaborativeTextEditorElement = collaborativeTextEditorElementFactory.build(); - jest.spyOn(visitor, 'createOrUpdateBoardNode'); - - visitor.visitCollaborativeTextEditorElement(collaborativeTextEditorElement); - - const expectedNode: Partial = { - id: collaborativeTextEditorElement.id, - type: BoardNodeType.COLLABORATIVE_TEXT_EDITOR, - }; - expect(visitor.createOrUpdateBoardNode).toHaveBeenCalledWith(expect.objectContaining(expectedNode)); - }); - }); - - describe('when visiting a rich text element composite', () => { - it('should create or update the node', () => { - const richTextElement = richTextElementFactory.build(); - jest.spyOn(visitor, 'createOrUpdateBoardNode'); - - visitor.visitRichTextElement(richTextElement); - - const expectedNode: Partial = { - id: richTextElement.id, - type: BoardNodeType.RICH_TEXT_ELEMENT, - text: richTextElement.text, - }; - expect(visitor.createOrUpdateBoardNode).toHaveBeenCalledWith(expect.objectContaining(expectedNode)); - }); - }); - - describe('when visiting a drawing element composite', () => { - it('should create or update the node', () => { - const drawingElement = drawingElementFactory.build(); - jest.spyOn(visitor, 'createOrUpdateBoardNode'); - - visitor.visitDrawingElement(drawingElement); - - const expectedNode: Partial = { - id: drawingElement.id, - type: BoardNodeType.DRAWING_ELEMENT, - }; - expect(visitor.createOrUpdateBoardNode).toHaveBeenCalledWith(expect.objectContaining(expectedNode)); - }); - }); - - describe('when visiting a submission container element composite', () => { - it('should create or update the node', () => { - const submissionContainerElement = submissionContainerElementFactory.build(); - jest.spyOn(visitor, 'createOrUpdateBoardNode'); - - visitor.visitSubmissionContainerElement(submissionContainerElement); - - const expectedNode: Partial = { - id: submissionContainerElement.id, - type: BoardNodeType.SUBMISSION_CONTAINER_ELEMENT, - dueDate: submissionContainerElement.dueDate, - }; - expect(visitor.createOrUpdateBoardNode).toHaveBeenCalledWith(expect.objectContaining(expectedNode)); - }); - }); - - describe('when visiting a submission item composite', () => { - it('should create or update the node', () => { - const submissionItem = submissionItemFactory.build(); - jest.spyOn(visitor, 'createOrUpdateBoardNode'); - - visitor.visitSubmissionItem(submissionItem); - - const expectedNode: Partial = { - id: submissionItem.id, - type: BoardNodeType.SUBMISSION_ITEM, - completed: submissionItem.completed, - }; - expect(visitor.createOrUpdateBoardNode).toHaveBeenCalledWith(expect.objectContaining(expectedNode)); - }); - }); - - describe('when visiting a external tool element', () => { - it('should create or update the node', () => { - const contextExternalTool = contextExternalToolEntityFactory.buildWithId(); - const externalToolElement = externalToolElementFactory.build({ - contextExternalToolId: contextExternalTool.id, - }); - jest.spyOn(visitor, 'createOrUpdateBoardNode'); - - visitor.visitExternalToolElement(externalToolElement); - - const expectedNode: Partial = { - id: externalToolElement.id, - type: BoardNodeType.EXTERNAL_TOOL, - contextExternalTool, - }; - expect(visitor.createOrUpdateBoardNode).toHaveBeenCalledWith(expect.objectContaining(expectedNode)); - }); - }); - - describe('when visiting a media board composite', () => { - const setup = () => { - const line = mediaLineFactory.build(); - const board = mediaBoardFactory.build({ children: [line] }); - - jest.spyOn(line, 'accept'); - jest.spyOn(visitor, 'createOrUpdateBoardNode'); - - return { - board, - line, - }; - }; - - it('should create or update the node', () => { - const { board } = setup(); - - visitor.visitMediaBoard(board); - - const expectedNode: Partial = { - id: board.id, - type: BoardNodeType.MEDIA_BOARD, - mediaAvailableLineBackgroundColor: MediaBoardColors.TRANSPARENT, - mediaAvailableLineCollapsed: false, - layout: MediaBoardLayoutType.LIST, - }; - expect(visitor.createOrUpdateBoardNode).toHaveBeenCalledWith(expect.objectContaining(expectedNode)); - }); - - it('should visit the children', () => { - const { board, line } = setup(); - - board.accept(visitor); - - expect(line.accept).toHaveBeenCalledWith(visitor); - }); - }); - - describe('when visiting a media line composite', () => { - const setup = () => { - const element = mediaExternalToolElementFactory.build(); - const line = mediaLineFactory.build({ children: [element] }); - - jest.spyOn(element, 'accept'); - jest.spyOn(visitor, 'createOrUpdateBoardNode'); - - return { - line, - element, - }; - }; - - it('should create or update the node', () => { - const { line } = setup(); - - visitor.visitMediaLine(line); - - const expectedNode: Partial = { - id: line.id, - type: BoardNodeType.MEDIA_LINE, - title: line.title, - }; - expect(visitor.createOrUpdateBoardNode).toHaveBeenCalledWith(expect.objectContaining(expectedNode)); - }); - - it('should visit the children', () => { - const { line, element } = setup(); - - line.accept(visitor); - - expect(element.accept).toHaveBeenCalledWith(visitor); - }); - }); - - describe('when visiting a media external tool element', () => { - const setup = () => { - const contextExternalTool = contextExternalToolEntityFactory.buildWithId(); - const mediaExternalToolElement = mediaExternalToolElementFactory.build({ - contextExternalToolId: contextExternalTool.id, - }); - - jest.spyOn(visitor, 'createOrUpdateBoardNode'); - - return { - contextExternalTool, - mediaExternalToolElement, - }; - }; - - it('should create or update the node', () => { - const { contextExternalTool, mediaExternalToolElement } = setup(); - - visitor.visitMediaExternalToolElement(mediaExternalToolElement); - - const expectedNode: Partial = { - id: mediaExternalToolElement.id, - type: BoardNodeType.MEDIA_EXTERNAL_TOOL_ELEMENT, - contextExternalTool, - }; - expect(visitor.createOrUpdateBoardNode).toHaveBeenCalledWith(expect.objectContaining(expectedNode)); - }); - }); - - describe('createOrUpdateBoardNode', () => { - describe('when the board is new', () => { - it('should persist the board node', () => { - const board = columnBoardFactory.build(); - em.getUnitOfWork().getById.mockReturnValue(undefined); - - visitor.visitColumnBoard(board); - - expect(em.persist).toHaveBeenCalledWith(expect.any(ColumnBoardNode)); - }); - }); - - describe('when the board is already persisted', () => { - it('should persist the board node', () => { - const board = columnBoardFactory.build(); - const boardNode = columnBoardNodeFactory.build(); - - em.getUnitOfWork().getById.mockReturnValue(boardNode); - - visitor.visitColumnBoard(board); - - expect(em.assign).toHaveBeenCalledWith(boardNode, expect.any(ColumnBoardNode)); - }); - }); - }); -}); diff --git a/apps/server/src/modules/board/repo/recursive-save.visitor.ts b/apps/server/src/modules/board/repo/recursive-save.visitor.ts deleted file mode 100644 index b06603daed5..00000000000 --- a/apps/server/src/modules/board/repo/recursive-save.visitor.ts +++ /dev/null @@ -1,294 +0,0 @@ -import { Utils } from '@mikro-orm/core'; -import { EntityManager } from '@mikro-orm/mongodb'; -import { ContextExternalToolEntity } from '@modules/tool/context-external-tool/entity'; -import type { - AnyBoardDo, - BoardCompositeVisitor, - Card, - CollaborativeTextEditorElement, - Column, - ColumnBoard, - DrawingElement, - ExternalToolElement, - FileElement, - LinkElement, - MediaBoard, - MediaExternalToolElement, - MediaLine, - RichTextElement, - SubmissionContainerElement, - SubmissionItem, -} from '@shared/domain/domainobject'; -import { - BoardNode, - CardNode, - CollaborativeTextEditorElementNode, - ColumnBoardNode, - ColumnNode, - DrawingElementNode, - ExternalToolElementNodeEntity, - FileElementNode, - LinkElementNode, - MediaBoardNode, - MediaExternalToolElementNode, - MediaLineNode, - RichTextElementNode, - SubmissionContainerElementNode, - SubmissionItemNode, -} from '@shared/domain/entity'; -import { EntityId } from '@shared/domain/types'; - -import { BoardNodeRepo } from './board-node.repo'; - -type ParentData = { - boardNode: BoardNode; - position: number; -}; - -export class RecursiveSaveVisitor implements BoardCompositeVisitor { - private parentsMap: Map = new Map(); - - constructor(private readonly em: EntityManager, private readonly boardNodeRepo: BoardNodeRepo) {} - - async save(domainObject: AnyBoardDo | AnyBoardDo[], parent?: AnyBoardDo): Promise { - const domainObjects = Utils.asArray(domainObject); - - if (parent) { - const parentNode = await this.boardNodeRepo.findById(parent.id); - - domainObjects.forEach((child) => { - this.registerParentData(parent, child, parentNode); - }); - } - - domainObjects.forEach((child) => child.accept(this)); - } - - visitColumnBoard(columnBoard: ColumnBoard): void { - const parentData = this.parentsMap.get(columnBoard.id); - - const boardNode = new ColumnBoardNode({ - id: columnBoard.id, - title: columnBoard.title, - parent: parentData?.boardNode, - position: parentData?.position, - context: columnBoard.context, - isVisible: columnBoard.isVisible, - layout: columnBoard.layout, - }); - - this.saveRecursive(boardNode, columnBoard); - } - - visitColumn(column: Column): void { - const parentData = this.parentsMap.get(column.id); - - const boardNode = new ColumnNode({ - id: column.id, - title: column.title, - parent: parentData?.boardNode, - position: parentData?.position, - }); - - this.saveRecursive(boardNode, column); - } - - visitCard(card: Card): void { - const parentData = this.parentsMap.get(card.id); - - const boardNode = new CardNode({ - id: card.id, - height: card.height, - title: card.title, - parent: parentData?.boardNode, - position: parentData?.position, - }); - - this.saveRecursive(boardNode, card); - } - - visitFileElement(fileElement: FileElement): void { - const parentData = this.parentsMap.get(fileElement.id); - - const boardNode = new FileElementNode({ - id: fileElement.id, - caption: fileElement.caption, - alternativeText: fileElement.alternativeText, - parent: parentData?.boardNode, - position: parentData?.position, - }); - - this.saveRecursive(boardNode, fileElement); - } - - visitLinkElement(linkElement: LinkElement): void { - const parentData = this.parentsMap.get(linkElement.id); - - const boardNode = new LinkElementNode({ - id: linkElement.id, - url: linkElement.url, - title: linkElement.title, - imageUrl: linkElement.imageUrl, - parent: parentData?.boardNode, - position: parentData?.position, - }); - - this.createOrUpdateBoardNode(boardNode); - this.visitChildren(linkElement, boardNode); - } - - visitRichTextElement(richTextElement: RichTextElement): void { - const parentData = this.parentsMap.get(richTextElement.id); - - const boardNode = new RichTextElementNode({ - id: richTextElement.id, - text: richTextElement.text, - inputFormat: richTextElement.inputFormat, - parent: parentData?.boardNode, - position: parentData?.position, - }); - - this.saveRecursive(boardNode, richTextElement); - } - - visitDrawingElement(drawingElement: DrawingElement): void { - const parentData = this.parentsMap.get(drawingElement.id); - - const boardNode = new DrawingElementNode({ - id: drawingElement.id, - description: drawingElement.description ?? '', - parent: parentData?.boardNode, - position: parentData?.position, - }); - - this.saveRecursive(boardNode, drawingElement); - } - - visitSubmissionContainerElement(submissionContainerElement: SubmissionContainerElement): void { - const parentData = this.parentsMap.get(submissionContainerElement.id); - - const boardNode = new SubmissionContainerElementNode({ - id: submissionContainerElement.id, - parent: parentData?.boardNode, - position: parentData?.position, - dueDate: submissionContainerElement.dueDate, - }); - - this.saveRecursive(boardNode, submissionContainerElement); - } - - visitSubmissionItem(submissionItem: SubmissionItem): void { - const parentData = this.parentsMap.get(submissionItem.id); - const boardNode = new SubmissionItemNode({ - id: submissionItem.id, - parent: parentData?.boardNode, - position: parentData?.position, - completed: submissionItem.completed, - userId: submissionItem.userId, - }); - - this.saveRecursive(boardNode, submissionItem); - } - - visitExternalToolElement(externalToolElement: ExternalToolElement): void { - const parentData: ParentData | undefined = this.parentsMap.get(externalToolElement.id); - - const boardNode: ExternalToolElementNodeEntity = new ExternalToolElementNodeEntity({ - id: externalToolElement.id, - contextExternalTool: externalToolElement.contextExternalToolId - ? this.em.getReference(ContextExternalToolEntity, externalToolElement.contextExternalToolId) - : undefined, - parent: parentData?.boardNode, - position: parentData?.position, - }); - - this.createOrUpdateBoardNode(boardNode); - this.visitChildren(externalToolElement, boardNode); - } - - visitCollaborativeTextEditorElement(collaborativeTextEditorElement: CollaborativeTextEditorElement): void { - const parentData = this.parentsMap.get(collaborativeTextEditorElement.id); - - const boardNode = new CollaborativeTextEditorElementNode({ - id: collaborativeTextEditorElement.id, - parent: parentData?.boardNode, - position: parentData?.position, - }); - - this.saveRecursive(boardNode, collaborativeTextEditorElement); - } - - private visitChildren(parent: AnyBoardDo, parentNode: BoardNode) { - parent.children.forEach((child) => { - this.registerParentData(parent, child, parentNode); - child.accept(this); - }); - } - - private registerParentData(parent: AnyBoardDo, child: AnyBoardDo, parentNode: BoardNode) { - const position = parent.children.findIndex((obj) => obj.id === child.id); - if (position === -1) { - throw new Error(`Cannot get child position. Child doesnt belong to parent`); - } - this.parentsMap.set(child.id, { boardNode: parentNode, position }); - } - - private saveRecursive(boardNode: BoardNode, anyBoardDo: AnyBoardDo): void { - this.createOrUpdateBoardNode(boardNode); - this.visitChildren(anyBoardDo, boardNode); - } - - // TODO make private (change tests) - createOrUpdateBoardNode(boardNode: BoardNode): void { - const existing = this.em.getUnitOfWork().getById(BoardNode.name, boardNode.id); - if (existing) { - this.em.assign(existing, boardNode); - } else { - this.em.persist(boardNode); - } - } - - visitMediaBoard(mediaBoard: MediaBoard): void { - const parentData: ParentData | undefined = this.parentsMap.get(mediaBoard.id); - - const boardNode: MediaBoardNode = new MediaBoardNode({ - id: mediaBoard.id, - parent: parentData?.boardNode, - position: parentData?.position, - context: mediaBoard.context, - layout: mediaBoard.layout, - mediaAvailableLineCollapsed: mediaBoard.mediaAvailableLineCollapsed, - mediaAvailableLineBackgroundColor: mediaBoard.mediaAvailableLineBackgroundColor, - }); - - this.saveRecursive(boardNode, mediaBoard); - } - - visitMediaLine(mediaLine: MediaLine): void { - const parentData: ParentData | undefined = this.parentsMap.get(mediaLine.id); - - const boardNode: MediaLineNode = new MediaLineNode({ - id: mediaLine.id, - parent: parentData?.boardNode, - position: parentData?.position, - title: mediaLine.title, - backgroundColor: mediaLine.backgroundColor, - collapsed: mediaLine.collapsed, - }); - - this.saveRecursive(boardNode, mediaLine); - } - - visitMediaExternalToolElement(mediaElement: MediaExternalToolElement): void { - const parentData: ParentData | undefined = this.parentsMap.get(mediaElement.id); - - const boardNode: MediaExternalToolElementNode = new MediaExternalToolElementNode({ - id: mediaElement.id, - parent: parentData?.boardNode, - position: parentData?.position, - contextExternalTool: this.em.getReference(ContextExternalToolEntity, mediaElement.contextExternalToolId), - }); - - this.saveRecursive(boardNode, mediaElement); - } -} diff --git a/apps/server/src/modules/board/repo/tree-builder.ts b/apps/server/src/modules/board/repo/tree-builder.ts new file mode 100644 index 00000000000..cb796708517 --- /dev/null +++ b/apps/server/src/modules/board/repo/tree-builder.ts @@ -0,0 +1,46 @@ +import { AnyBoardNode, getBoardNodeConstructor, joinPath } from '../domain'; +import { BoardNodeEntity } from './entity'; + +export class TreeBuilder { + private childrenMap: Record = {}; + + constructor(descendants: BoardNodeEntity[] = []) { + for (const props of descendants) { + this.childrenMap[props.path] ||= []; + this.childrenMap[props.path].push(props); + } + } + + build(entity: BoardNodeEntity): AnyBoardNode { + const children = this.getChildren(entity).map((childProps) => this.build(childProps)); + + // Assign children only when not present. + // This prevents already loaded children from being overwritten + // when building a tree with a smaller depth. + if (!entity.children || entity.children.length === 0) { + entity.children = children; + } + + // check identity map reference + if (entity.domainObject) { + return entity.domainObject; + } + + const Constructor = getBoardNodeConstructor(entity.type); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const boardNode = new Constructor(entity); + // attach to identity map + entity.domainObject = boardNode; + + return boardNode; + } + + private getChildren(entity: BoardNodeEntity): BoardNodeEntity[] { + const pathOfChildren = joinPath(entity.path, entity.id); + const children = this.childrenMap[pathOfChildren] || []; + const sortedChildren = children.sort((a, b) => a.position - b.position); + return sortedChildren; + } +} diff --git a/apps/server/src/modules/board/repo/types/board-node-entity-props.ts b/apps/server/src/modules/board/repo/types/board-node-entity-props.ts new file mode 100644 index 00000000000..40c756264d0 --- /dev/null +++ b/apps/server/src/modules/board/repo/types/board-node-entity-props.ts @@ -0,0 +1,58 @@ +import type { + AnyBoardNodeProps, + BoardNodeProps, + BoardNodeType, + CardProps, + CollaborativeTextEditorElementProps, + ColumnBoardProps, + ColumnProps, + DrawingElementProps, + ExternalToolElementProps, + FileElementProps, + LinkElementProps, + MediaBoardProps, + MediaExternalToolElementProps, + MediaLineProps, + RichTextElementProps, + SubmissionContainerElementProps, + SubmissionItemProps, +} from '../../domain'; + +// omit all given keys from an object type +type StrictOmit = Omit; + +// make all value types of an object to be potentially undefined +// we cannot use Partial<> utility type because that makes the property keys optional +// and optional keys are not respected when implementing an interface like "class ... implements ..." +type MakeValuesPotentiallyUndefined = { + [P in keyof T]: T[P] | undefined; +}; + +// The properties that extend the base BoardNodeProps with specific BoardNodeType removed +type ExtraProps = StrictOmit; + +// We use Required<> utility type to make optional properties non-optional +type ComponentProps = MakeValuesPotentiallyUndefined>>; + +// re-add general BoardNodeType +type TypeProps = { type: BoardNodeType }; + +// The interface that acts as a skeleton for BoardNodeEntity +// This helps us to map the domain property interfaces to the entity +export interface BoardNodeEntityProps + extends BoardNodeProps, + TypeProps, + ComponentProps, + ComponentProps, + ComponentProps, + ComponentProps, + ComponentProps, + ComponentProps, + ComponentProps, + ComponentProps, + ComponentProps, + ComponentProps, + ComponentProps, + ComponentProps, + ComponentProps, + ComponentProps {} diff --git a/apps/server/src/modules/board/repo/types/index.ts b/apps/server/src/modules/board/repo/types/index.ts new file mode 100644 index 00000000000..05e69d4321e --- /dev/null +++ b/apps/server/src/modules/board/repo/types/index.ts @@ -0,0 +1 @@ +export * from './board-node-entity-props'; diff --git a/apps/server/src/modules/board/service/board-common-tool.service.spec.ts b/apps/server/src/modules/board/service/board-common-tool.service.spec.ts new file mode 100644 index 00000000000..5895d3eb0b9 --- /dev/null +++ b/apps/server/src/modules/board/service/board-common-tool.service.spec.ts @@ -0,0 +1,96 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { NotFoundException } from '@nestjs/common'; +import { contextExternalToolFactory } from '@modules/tool/context-external-tool/testing'; +import { BoardCommonToolService } from './board-common-tool.service'; +import { BoardNodeRepo } from '../repo'; +import { BoardNodeService } from './board-node.service'; +import { ColumnBoard, MediaBoard, AnyBoardNode } from '../domain'; + +import { columnBoardFactory, mediaBoardFactory } from '../testing'; + +describe('BoardCommonToolService', () => { + let module: TestingModule; + let service: BoardCommonToolService; + let boardNodeRepo: DeepMocked; + let boardNodeService: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + BoardCommonToolService, + { + provide: BoardNodeRepo, + useValue: createMock(), + }, + { + provide: BoardNodeService, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(BoardCommonToolService); + boardNodeRepo = module.get(BoardNodeRepo); + boardNodeService = module.get(BoardNodeService); + }); + + afterAll(async () => { + await module.close(); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('countBoardUsageForExternalTools', () => { + const setup = () => { + const contextExternalTools = [contextExternalToolFactory.build(), contextExternalToolFactory.build()]; + + const boardNodes: AnyBoardNode[] = [ + { rootId: '1' } as AnyBoardNode, + { rootId: '2' } as AnyBoardNode, + { rootId: '1' } as AnyBoardNode, + ]; + boardNodeRepo.findByContextExternalToolIds.mockResolvedValueOnce(boardNodes); + + return { contextExternalTools }; + }; + it('should count board usage for external tools', async () => { + const { contextExternalTools } = setup(); + const result = await service.countBoardUsageForExternalTools(contextExternalTools); + + expect(result).toBe(2); + }); + }); + + describe('findByDescendant', () => { + it('should return the root node when it is a ColumnBoard', async () => { + const boardNode: AnyBoardNode = { id: '1', rootId: '2' } as AnyBoardNode; + const rootNode: ColumnBoard = columnBoardFactory.build(); + boardNodeService.findRoot.mockResolvedValueOnce(rootNode); + + const result = await service.findByDescendant(boardNode); + + expect(result).toBe(rootNode); + }); + + it('should return the root node when it is a MediaBoard', async () => { + const boardNode: AnyBoardNode = { id: '1', rootId: '2' } as AnyBoardNode; + const rootNode: MediaBoard = mediaBoardFactory.build(); + boardNodeService.findRoot.mockResolvedValueOnce(rootNode); + + const result = await service.findByDescendant(boardNode); + + expect(result).toBe(rootNode); + }); + + it('should throw NotFoundException when root node is not a ColumnBoard or MediaBoard', async () => { + const boardNode: AnyBoardNode = { id: '1', rootId: '2' } as AnyBoardNode; + const rootNode: AnyBoardNode = { id: '2' } as AnyBoardNode; + boardNodeService.findRoot.mockResolvedValueOnce(rootNode); + + await expect(service.findByDescendant(boardNode)).rejects.toThrow(NotFoundException); + }); + }); +}); diff --git a/apps/server/src/modules/board/service/board-common-tool.service.ts b/apps/server/src/modules/board/service/board-common-tool.service.ts new file mode 100644 index 00000000000..684b7e39849 --- /dev/null +++ b/apps/server/src/modules/board/service/board-common-tool.service.ts @@ -0,0 +1,35 @@ +import type { ContextExternalTool } from '@modules/tool/context-external-tool/domain'; +import { Injectable, NotFoundException } from '@nestjs/common'; +import { EntityId } from '@shared/domain/types'; +import { AnyBoardNode, ColumnBoard, isColumnBoard, isMediaBoard, MediaBoard } from '../domain'; +import { BoardNodeRepo } from '../repo'; +import { BoardNodeService } from './board-node.service'; + +@Injectable() +export class BoardCommonToolService { + constructor(private readonly boardNodeRepo: BoardNodeRepo, private readonly boardNodeService: BoardNodeService) {} + + async countBoardUsageForExternalTools(contextExternalTools: ContextExternalTool[]): Promise { + // TODO check why this is done so complicated + const toolIds: EntityId[] = contextExternalTools + .map((tool: ContextExternalTool): EntityId | undefined => tool.id) + .filter((id: EntityId | undefined): id is EntityId => !!id); + + const boardNodes = await this.boardNodeRepo.findByContextExternalToolIds(toolIds, 0); + const rootIds = boardNodes.map((bn) => bn.rootId); + // TODO maybe we should check if these are ids of actual boards? + const boardCount = new Set(rootIds).size; + + return boardCount; + } + + async findByDescendant(boardNode: AnyBoardNode): Promise { + const rootNode = await this.boardNodeService.findRoot(boardNode); + + if (!isColumnBoard(rootNode) && !isMediaBoard(rootNode)) { + throw new NotFoundException(`There is no board with this id`); + } + + return rootNode; + } +} diff --git a/apps/server/src/modules/board/service/board-do-authorizable.service.spec.ts b/apps/server/src/modules/board/service/board-do-authorizable.service.spec.ts deleted file mode 100644 index 7712a4a1a13..00000000000 --- a/apps/server/src/modules/board/service/board-do-authorizable.service.spec.ts +++ /dev/null @@ -1,277 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { BoardDoAuthorizableProps, BoardExternalReferenceType, BoardRoles } from '@shared/domain/domainobject'; -import { CourseRepo } from '@shared/repo'; -import { - cardFactory, - columnBoardFactory, - columnFactory, - courseFactory, - mediaBoardFactory, - roleFactory, - setupEntities, - userFactory, -} from '@shared/testing'; -import { BoardDoRepo } from '../repo'; -import { BoardDoAuthorizableService } from './board-do-authorizable.service'; - -describe(BoardDoAuthorizableService.name, () => { - let module: TestingModule; - let service: BoardDoAuthorizableService; - let boardDoRepo: DeepMocked; - let courseRepo: DeepMocked; - - beforeAll(async () => { - module = await Test.createTestingModule({ - providers: [ - BoardDoAuthorizableService, - { - provide: BoardDoRepo, - useValue: createMock(), - }, - { - provide: CourseRepo, - useValue: createMock(), - }, - ], - }).compile(); - - service = module.get(BoardDoAuthorizableService); - boardDoRepo = module.get(BoardDoRepo); - courseRepo = module.get(CourseRepo); - await setupEntities(); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - afterAll(async () => { - await module.close(); - }); - - describe('findById', () => { - describe('when finding a board domainobject', () => { - const setup = () => { - const course = courseFactory.build(); - const columnBoard = columnBoardFactory.build(); - boardDoRepo.findById.mockResolvedValueOnce(columnBoard); - boardDoRepo.findById.mockResolvedValueOnce(columnBoard); - courseRepo.findById.mockResolvedValueOnce(course); - boardDoRepo.getAncestorIds.mockResolvedValueOnce([columnBoard.id]); - - return { columnBoardId: columnBoard.id }; - }; - - it('should call the repository', async () => { - const { columnBoardId } = setup(); - - await service.findById(columnBoardId); - - expect(boardDoRepo.findById).toHaveBeenCalledWith(columnBoardId, 1); - }); - - it('should return the column', async () => { - const { columnBoardId } = setup(); - - const result = await service.findById(columnBoardId); - - expect(result.id).toEqual(columnBoardId); - }); - }); - }); - - describe('getBoardAuthorizable', () => { - describe('when having an empty board', () => { - const setup = () => { - const course = courseFactory.build(); - const board = columnBoardFactory.build(); - return { board, course }; - }; - - it('should return an empty usergroup', async () => { - const { board, course } = setup(); - boardDoRepo.findById.mockResolvedValueOnce(board); - courseRepo.findById.mockResolvedValueOnce(course); - boardDoRepo.getAncestorIds.mockResolvedValueOnce([board.id]); - - const userGroup = await service.getBoardAuthorizable(board); - - expect(userGroup.id).toEqual(board.id); - expect(userGroup.users.length).toEqual(0); - }); - }); - - describe('when having a board with a teacher and some students', () => { - const setup = () => { - const roles = roleFactory.buildList(1, {}); - const teacher = userFactory.buildWithId({ roles }); - const substitutionTeacher = userFactory.buildWithId({ roles }); - const students = userFactory.buildListWithId(3); - const course = courseFactory.buildWithId({ - teachers: [teacher], - substitutionTeachers: [substitutionTeacher], - students, - }); - const board = columnBoardFactory.build({ context: { type: BoardExternalReferenceType.Course, id: course.id } }); - boardDoRepo.findById.mockResolvedValueOnce(board); - courseRepo.findById.mockResolvedValueOnce(course); - boardDoRepo.getAncestorIds.mockResolvedValueOnce([board.id]); - return { - board, - teacherId: teacher.id, - substitutionTeacherId: substitutionTeacher.id, - studentIds: students.map((s) => s.id), - teacher, - substitutionTeacher, - students, - }; - }; - - it('should return the teacher and the students with correct roles', async () => { - const { board, teacherId, substitutionTeacherId, studentIds } = setup(); - - const boardDoAuthorizable = await service.getBoardAuthorizable(board); - const userPermissions = boardDoAuthorizable.users.reduce((map, user) => { - map[user.userId] = user.roles; - return map; - }, {}); - - expect(boardDoAuthorizable.users).toHaveLength(5); - expect(userPermissions[teacherId]).toEqual([BoardRoles.EDITOR]); - expect(userPermissions[substitutionTeacherId]).toEqual([BoardRoles.EDITOR]); - expect(userPermissions[studentIds[0]]).toEqual([BoardRoles.READER]); - expect(userPermissions[studentIds[1]]).toEqual([BoardRoles.READER]); - expect(userPermissions[studentIds[2]]).toEqual([BoardRoles.READER]); - }); - - it('should return the users with their names', async () => { - const { board, teacher, substitutionTeacher, students } = setup(); - - const boardDoAuthorizable = await service.getBoardAuthorizable(board); - const firstNames = boardDoAuthorizable.users.reduce((map, user) => { - map[user.userId] = user.firstName; - return map; - }, {}); - - const lastNames = boardDoAuthorizable.users.reduce((map, user) => { - map[user.userId] = user.lastName; - return map; - }, {}); - - expect(boardDoAuthorizable.users).toHaveLength(5); - expect(firstNames[teacher.id]).toEqual(teacher.firstName); - expect(lastNames[teacher.id]).toEqual(teacher.lastName); - expect(firstNames[substitutionTeacher.id]).toEqual(substitutionTeacher.firstName); - expect(lastNames[substitutionTeacher.id]).toEqual(substitutionTeacher.lastName); - expect(firstNames[students[0].id]).toEqual(students[0].firstName); - expect(lastNames[students[0].id]).toEqual(students[0].lastName); - expect(firstNames[students[1].id]).toEqual(students[1].firstName); - expect(lastNames[students[1].id]).toEqual(students[1].lastName); - expect(firstNames[students[2].id]).toEqual(students[2].firstName); - expect(lastNames[students[2].id]).toEqual(students[2].lastName); - }); - - it('should return the boardDo', async () => { - const { board } = setup(); - - const boardDoAuthorizable = await service.getBoardAuthorizable(board); - - expect(boardDoAuthorizable.boardDo).toEqual(board); - }); - - it('should return the parentDo', async () => { - setup(); - const column = columnFactory.build(); - const card = cardFactory.build(); - - boardDoRepo.findParentOfId.mockResolvedValueOnce(column); - - const boardDoAuthorizable = await service.getBoardAuthorizable(card); - - expect(boardDoAuthorizable.parentDo).toEqual(column); - }); - - it('should return the rootDo', async () => { - const { board } = setup(); - const column = columnFactory.build(); - boardDoRepo.getAncestorIds.mockResolvedValueOnce([column.id, board.id]); - boardDoRepo.findById.mockResolvedValueOnce(board); - - const boardDoAuthorizable = await service.getBoardAuthorizable(board); - - expect(boardDoAuthorizable.rootDo).toEqual(board); - }); - }); - - describe('when trying to create a boardDoAuthorizable on a column without a columnboard as root', () => { - const setup = () => { - const roles = roleFactory.buildList(1, {}); - const teacher = userFactory.buildWithId({ roles }); - const students = userFactory.buildListWithId(3); - const column = columnFactory.build(); - - boardDoRepo.getAncestorIds.mockResolvedValueOnce([]); - boardDoRepo.findById.mockResolvedValueOnce(column); - - return { column, teacherId: teacher.id, studentIds: students.map((s) => s.id) }; - }; - - it('should throw an error', async () => { - const { column } = setup(); - - await expect(() => service.getBoardAuthorizable(column)).rejects.toThrowError(); - }); - }); - - describe('when having a board that does not reference a course', () => { - const setup = () => { - const teacher = userFactory.buildWithId(); - const board = columnBoardFactory.withoutContext().build(); - boardDoRepo.findById.mockResolvedValueOnce(board); - boardDoRepo.getAncestorIds.mockResolvedValueOnce([board.id]); - return { board, teacherId: teacher.id }; - }; - - it('should return an boardDoAuthorizable without user-entries', async () => { - const { board } = setup(); - - const boardDoAuthorizable = await service.getBoardAuthorizable(board); - expect(boardDoAuthorizable.users).toHaveLength(0); - }); - }); - }); - - describe('when having a media board bound to a user', () => { - const setup = () => { - const user = userFactory.buildWithId(); - const board = mediaBoardFactory.build({ context: { type: BoardExternalReferenceType.User, id: user.id } }); - - boardDoRepo.findById.mockResolvedValueOnce(board); - boardDoRepo.getAncestorIds.mockResolvedValueOnce([board.id]); - - return { - user, - board, - }; - }; - - it('should return the boardDoAuthorizable', async () => { - const { board, user } = setup(); - - const boardDoAuthorizable = await service.getBoardAuthorizable(board); - - expect(boardDoAuthorizable.getProps()).toEqual({ - id: board.id, - boardDo: board, - users: [ - { - userId: user.id, - roles: [BoardRoles.EDITOR], - }, - ], - rootDo: board, - }); - }); - }); -}); diff --git a/apps/server/src/modules/board/service/board-do-authorizable.service.ts b/apps/server/src/modules/board/service/board-do-authorizable.service.ts deleted file mode 100644 index f3f85edceaa..00000000000 --- a/apps/server/src/modules/board/service/board-do-authorizable.service.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { AuthorizationLoaderService } from '@modules/authorization'; -import { forwardRef, Inject, Injectable } from '@nestjs/common'; -import { - AnyBoardDo, - BoardDoAuthorizable, - BoardExternalReferenceType, - BoardRoles, - ColumnBoard, - MediaBoard, - UserWithBoardRoles, -} from '@shared/domain/domainobject'; -import { Course } from '@shared/domain/entity'; -import { EntityId } from '@shared/domain/types'; -import { CourseRepo } from '@shared/repo'; -import { BoardDoRepo } from '../repo'; - -@Injectable() -export class BoardDoAuthorizableService implements AuthorizationLoaderService { - constructor( - @Inject(forwardRef(() => BoardDoRepo)) private readonly boardDoRepo: BoardDoRepo, - private readonly courseRepo: CourseRepo - ) {} - - async findById(id: EntityId): Promise { - const boardDo = await this.boardDoRepo.findById(id, 1); - const boardDoAuthorizable = await this.getBoardAuthorizable(boardDo); - - return boardDoAuthorizable; - } - - async getBoardAuthorizable(boardDo: AnyBoardDo): Promise { - const rootDo = await this.getRootBoardDo(boardDo); - // TODO used only for SubmissionItem; for rest BoardDo avoid extra call to improve performance - const parentDo = await this.getParentDo(boardDo); - let users: UserWithBoardRoles[] = []; - - if (rootDo.context?.type === BoardExternalReferenceType.Course) { - const course = await this.courseRepo.findById(rootDo.context.id); - users = this.mapCourseUsersToUserBoardRoles(course); - } else if (rootDo.context?.type === BoardExternalReferenceType.User) { - users = [ - { - userId: rootDo.context.id, - roles: [BoardRoles.EDITOR], - }, - ]; - } - - const boardDoAuthorizable = new BoardDoAuthorizable({ users, id: boardDo.id, boardDo, rootDo, parentDo }); - - return boardDoAuthorizable; - } - - private mapCourseUsersToUserBoardRoles(course: Course): UserWithBoardRoles[] { - const users = [ - ...course.getTeachersList().map((user) => { - return { - userId: user.id, - firstName: user.firstName, - lastName: user.lastName, - roles: [BoardRoles.EDITOR], - }; - }), - ...course.getSubstitutionTeachersList().map((user) => { - return { - userId: user.id, - firstName: user.firstName, - lastName: user.lastName, - roles: [BoardRoles.EDITOR], - }; - }), - ...course.getStudentsList().map((user) => { - return { - userId: user.id, - firstName: user.firstName, - lastName: user.lastName, - roles: [BoardRoles.READER], - }; - }), - ]; - // TODO check unique - return users; - } - - private async getParentDo(boardDo: AnyBoardDo): Promise | undefined> { - const parentDo = await this.boardDoRepo.findParentOfId(boardDo.id); - return parentDo; - } - - // TODO there is a similar method in board-do.service.ts - private async getRootBoardDo(boardDo: AnyBoardDo): Promise { - const ancestorIds = await this.boardDoRepo.getAncestorIds(boardDo); - const ids = [...ancestorIds, boardDo.id]; - const rootId = ids[0]; - const rootBoardDo = await this.boardDoRepo.findById(rootId, 1); - - // TODO Use an abstract base class for root nodes. Multiple STI abstract base classes are blocked by MikroORM 6.1.2 (issue #3745) - if (!(rootBoardDo instanceof ColumnBoard || rootBoardDo instanceof MediaBoard)) { - throw new Error('root boardnode was expected to be a ColumnBoard or MediaBoard'); - } - - return rootBoardDo; - } -} diff --git a/apps/server/src/modules/board/service/board-do-copy-service/board-do-copy.service.spec.ts b/apps/server/src/modules/board/service/board-do-copy-service/board-do-copy.service.spec.ts deleted file mode 100644 index 3be8034cdde..00000000000 --- a/apps/server/src/modules/board/service/board-do-copy-service/board-do-copy.service.spec.ts +++ /dev/null @@ -1,990 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { FileRecordParentType } from '@infra/rabbitmq'; -import { ObjectId } from '@mikro-orm/mongodb'; -import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; -import { ContextExternalTool } from '@modules/tool/context-external-tool/domain'; -import { ContextExternalToolService } from '@modules/tool/context-external-tool/service'; -import { contextExternalToolFactory } from '@modules/tool/context-external-tool/testing'; -import { ToolFeatures } from '@modules/tool/tool-config'; -import { Test, TestingModule } from '@nestjs/testing'; -import { - Card, - Column, - ColumnBoard, - DrawingElement, - ExternalToolElement, - FileElement, - isCard, - isColumn, - isColumnBoard, - isDrawingElement, - isExternalToolElement, - isFileElement, - isLinkElement, - isRichTextElement, - isSubmissionContainerElement, - LinkElement, - RichTextElement, - SubmissionContainerElement, -} from '@shared/domain/domainobject'; -import { - cardFactory, - columnBoardFactory, - columnFactory, - drawingElementFactory, - externalToolElementFactory, - fileElementFactory, - linkElementFactory, - richTextElementFactory, - setupEntities, - submissionContainerElementFactory, - submissionItemFactory, -} from '@shared/testing'; -import { BoardDoCopyService } from './board-do-copy.service'; -import { SchoolSpecificFileCopyService } from './school-specific-file-copy.interface'; - -describe('recursive board copy visitor', () => { - let module: TestingModule; - let service: BoardDoCopyService; - - let contextExternalToolService: DeepMocked; - let copyHelperService: DeepMocked; - - beforeAll(async () => { - module = await Test.createTestingModule({ - providers: [ - BoardDoCopyService, - { - provide: ContextExternalToolService, - useValue: createMock(), - }, - { - provide: CopyHelperService, - useValue: createMock(), - }, - { - provide: ToolFeatures, - useValue: { - ctlToolsCopyEnabled: true, - }, - }, - ], - }).compile(); - - service = module.get(BoardDoCopyService); - contextExternalToolService = module.get(ContextExternalToolService); - copyHelperService = module.get(CopyHelperService); - - await setupEntities(); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - const setupfileCopyService = () => { - const fileCopyService = createMock(); - - return { fileCopyService }; - }; - - describe('copying column boards', () => { - const getColumnBoardCopyFromStatus = (status: CopyStatus) => { - const copy = status.copyEntity; - expect(isColumnBoard(copy)).toEqual(true); - return copy as ColumnBoard; - }; - - describe('when copying empty column board', () => { - const setup = () => { - const original = columnBoardFactory.build(); - copyHelperService.deriveStatusFromElements.mockReturnValueOnce(CopyStatusEnum.SUCCESS); - - return { original, ...setupfileCopyService() }; - }; - - it('should return a columnboard as copy', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(isColumnBoard(result.copyEntity)).toEqual(true); - }); - - it('should copy title', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getColumnBoardCopyFromStatus(result); - - expect(copy.title).toEqual(original.title); - }); - - it('should create new id', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getColumnBoardCopyFromStatus(result); - - expect(copy.id).not.toEqual(original.id); - }); - - it('should copy the context', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getColumnBoardCopyFromStatus(result); - - expect(copy.context).toEqual(original.context); - }); - - it('should show status successful', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(result.status).toEqual(CopyStatusEnum.SUCCESS); - }); - - it('should show type Columnboard', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(result.type).toEqual(CopyElementType.COLUMNBOARD); - }); - - it('should set the copy to unpublished', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getColumnBoardCopyFromStatus(result); - - expect(copy.isVisible).toEqual(false); - }); - }); - - describe('when copying a columnboard with children', () => { - const setup = () => { - const children = columnFactory.buildList(5); - - const original = columnBoardFactory.build({ children }); - - return { original, ...setupfileCopyService() }; - }; - - it('should add children to copy of columnboard', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getColumnBoardCopyFromStatus(result); - - expect(copy.children.length).toEqual(original.children.length); - }); - - it('should create copies of children', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getColumnBoardCopyFromStatus(result); - - const originalChildIds = original.children.map((child) => child.id); - const copyChildrenIds = copy.children.map((child) => child.id); - - originalChildIds.forEach((id) => { - const index = copyChildrenIds.findIndex((value) => value === id); - expect(index).toEqual(-1); - }); - }); - - it('should add status of child copies to copystatus', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(result.elements?.length).toEqual(original.children.length); - }); - }); - }); - - describe('copying a column', () => { - const getColumnCopyFromStatus = (status: CopyStatus) => { - const copy = status.copyEntity; - expect(isColumn(copy)).toEqual(true); - return copy as Column; - }; - - describe('when copying an empty column', () => { - const setup = () => { - const original = columnFactory.build(); - - return { original, ...setupfileCopyService() }; - }; - - it('should return a column as copy', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(isColumn(result.copyEntity)).toEqual(true); - }); - - it('should copy title', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getColumnCopyFromStatus(result); - - expect(copy.title).toEqual(original.title); - }); - - it('should create new id', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getColumnCopyFromStatus(result); - - expect(copy.id).not.toEqual(original.id); - }); - - it('should show status successful', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(result.status).toEqual(CopyStatusEnum.SUCCESS); - }); - - it('should show type Column', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(result.type).toEqual(CopyElementType.COLUMN); - }); - }); - - describe('when copying a column with children', () => { - const setup = () => { - const children = cardFactory.buildList(2); - const original = columnFactory.build({ children }); - - return { original, ...setupfileCopyService() }; - }; - - it('should add children to copy of columnboard', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getColumnCopyFromStatus(result); - - expect(copy.children.length).toEqual(original.children.length); - }); - - it('should create copies of children', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getColumnCopyFromStatus(result); - - const originalChildIds = original.children.map((child) => child.id); - const copyChildrenIds = copy.children.map((child) => child.id); - - originalChildIds.forEach((id) => { - const index = copyChildrenIds.findIndex((value) => value === id); - expect(index).toEqual(-1); - }); - }); - - it('should add status of child copies to copystatus', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(result.elements?.length).toEqual(original.children.length); - }); - }); - }); - - describe('copying cards', () => { - const getCardCopyFromStatus = (status: CopyStatus) => { - const copy = status.copyEntity; - expect(isCard(copy)).toEqual(true); - return copy as Card; - }; - - describe('when copying an empty card', () => { - const setup = () => { - const original = cardFactory.build(); - - return { original, ...setupfileCopyService() }; - }; - - it('should return a richtext element as copy', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(isCard(result.copyEntity)).toEqual(true); - }); - - it('should copy title', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getCardCopyFromStatus(result); - - expect(copy.title).toEqual(original.title); - }); - - it('should copy height', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getCardCopyFromStatus(result); - - expect(copy.height).toEqual(original.height); - }); - - it('should create new id', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getCardCopyFromStatus(result); - - expect(copy.id).not.toEqual(original.id); - }); - - it('should show status successful', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(result.status).toEqual(CopyStatusEnum.SUCCESS); - }); - - it('should show type Card', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(result.type).toEqual(CopyElementType.CARD); - }); - }); - - describe('when copying a card with children', () => { - const setup = () => { - const children = [...richTextElementFactory.buildList(4), ...submissionContainerElementFactory.buildList(3)]; - const original = cardFactory.build({ children }); - - return { original, ...setupfileCopyService() }; - }; - - it('should add children to copy of columnboard', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getCardCopyFromStatus(result); - - expect(copy.children.length).toEqual(original.children.length); - }); - - it('should create copies of children', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getCardCopyFromStatus(result); - - const originalChildIds = original.children.map((child) => child.id); - const copyChildrenIds = copy.children.map((child) => child.id); - - originalChildIds.forEach((id) => { - const index = copyChildrenIds.findIndex((value) => value === id); - expect(index).toEqual(-1); - }); - }); - - it('should add status of child copies to copystatus', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(result.elements?.length).toEqual(original.children.length); - }); - }); - }); - - describe('when copying a richtext element', () => { - const setup = () => { - const original = richTextElementFactory.build(); - - return { original, ...setupfileCopyService() }; - }; - - const getRichTextFromStatus = (status: CopyStatus) => { - const copy = status.copyEntity; - expect(isRichTextElement(copy)).toEqual(true); - return copy as RichTextElement; - }; - - it('should return a richtext element as copy', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(isRichTextElement(result.copyEntity)).toEqual(true); - }); - - it('should copy text', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getRichTextFromStatus(result); - - expect(copy.text).toEqual(original.text); - }); - - it('should copy text', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getRichTextFromStatus(result); - - expect(copy.inputFormat).toEqual(original.inputFormat); - }); - - it('should create new id', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getRichTextFromStatus(result); - - expect(copy.id).not.toEqual(original.id); - }); - - it('should show status successful', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(result.status).toEqual(CopyStatusEnum.SUCCESS); - }); - - it('should show type RichTextElement', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(result.type).toEqual(CopyElementType.RICHTEXT_ELEMENT); - }); - }); - - describe('when copying a drawing element', () => { - const setup = () => { - const original = drawingElementFactory.build(); - - return { original, ...setupfileCopyService() }; - }; - - const getDrawingElementFromStatus = (status: CopyStatus) => { - const copy = status.copyEntity; - expect(isDrawingElement(copy)).toEqual(true); - return copy as DrawingElement; - }; - - it('should return a drawing element as copy', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(isDrawingElement(result.copyEntity)).toEqual(true); - }); - - it('should copy description', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getDrawingElementFromStatus(result); - - expect(copy.description).toEqual(original.description); - }); - - it('should create new id', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getDrawingElementFromStatus(result); - - expect(copy.id).not.toEqual(original.id); - }); - - it('should show status partial', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(result.status).toEqual(CopyStatusEnum.PARTIAL); - }); - - it('should show type RichTextElement', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(result.type).toEqual(CopyElementType.DRAWING_ELEMENT); - }); - }); - - describe('when copying a file element', () => { - const setup = () => { - const original = fileElementFactory.build(); - - const visitorSetup = setupfileCopyService(); - - visitorSetup.fileCopyService.copyFilesOfParent.mockResolvedValueOnce([ - { id: new ObjectId().toHexString(), sourceId: original.id, name: original.caption }, - ]); - - return { original, ...visitorSetup }; - }; - - const getFileElementFromStatus = (status: CopyStatus) => { - const copy = status.copyEntity; - expect(isFileElement(copy)).toEqual(true); - return copy as FileElement; - }; - - it('should return a file element as copy', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(isFileElement(result.copyEntity)).toEqual(true); - }); - - it('should create new id', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getFileElementFromStatus(result); - - expect(copy.id).not.toEqual(original.id); - }); - - it('should copy caption', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getFileElementFromStatus(result); - - expect(copy.caption).toEqual(original.caption); - }); - - it('should copy alternative text', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getFileElementFromStatus(result); - - expect(copy.alternativeText).toEqual(original.alternativeText); - }); - - it('should call fileCopy Service', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getFileElementFromStatus(result); - - expect(fileCopyService.copyFilesOfParent).toHaveBeenCalledWith({ - sourceParentId: original.id, - targetParentId: copy.id, - parentType: FileRecordParentType.BoardNode, - }); - }); - - it('should show status successful', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(result.status).toEqual(CopyStatusEnum.SUCCESS); - }); - - it('should show type FILE_ELEMENT', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(result.type).toEqual(CopyElementType.FILE_ELEMENT); - }); - - it('should include file copy status', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - const fileCopyStatus = result.elements?.at(0); - - expect(fileCopyStatus).toEqual( - expect.objectContaining({ - status: CopyStatusEnum.SUCCESS, - type: CopyElementType.FILE, - }) - ); - }); - }); - - describe('copying submission container', () => { - const getSubmissionContainerFromStatus = (status: CopyStatus) => { - const copy = status.copyEntity; - expect(isSubmissionContainerElement(copy)).toEqual(true); - return copy as SubmissionContainerElement; - }; - - describe('when copying an empty submission container element', () => { - const setup = () => { - const original = submissionContainerElementFactory.build(); - - return { original, ...setupfileCopyService() }; - }; - - it('should return a submission container element as copy', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(isSubmissionContainerElement(result.copyEntity)).toEqual(true); - }); - - it('should copy dueDate', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getSubmissionContainerFromStatus(result); - - expect(copy.dueDate).toEqual(original.dueDate); - }); - - it('should create new id', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getSubmissionContainerFromStatus(result); - - expect(copy.id).not.toEqual(original.id); - }); - - it('should show status successful', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(result.status).toEqual(CopyStatusEnum.SUCCESS); - }); - - it('should show type SUBMISSION_CONTAINER', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(result.type).toEqual(CopyElementType.SUBMISSION_CONTAINER_ELEMENT); - }); - }); - - describe('when copying a card with children', () => { - const setup = () => { - const children = [...submissionItemFactory.buildList(4)]; - const original = submissionContainerElementFactory.build({ children }); - - return { original, ...setupfileCopyService() }; - }; - - it('should NOT add children to copy of columnboard', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getSubmissionContainerFromStatus(result); - - expect(copy.children.length).toEqual(0); - }); - - it('should add status of child copies to copystatus', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(result.elements?.length).toEqual(original.children.length); - }); - }); - }); - - describe('when copying a submission item', () => { - const setup = () => { - const original = submissionItemFactory.build(); - - return { original, ...setupfileCopyService() }; - }; - - it('should NOT make a copy', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(result.copyEntity).toBeUndefined(); - }); - - it('should show status not-doing', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(result.status).toEqual(CopyStatusEnum.NOT_DOING); - }); - - it('should show type SUBMISSION_ITEM', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(result.type).toEqual(CopyElementType.SUBMISSION_ITEM); - }); - }); - - describe('when copying an external tool element', () => { - describe('when the element has no linked tool', () => { - const setup = () => { - const original = externalToolElementFactory.build(); - - return { original, ...setupfileCopyService() }; - }; - - const getExternalToolElementFromStatus = (status: CopyStatus): ExternalToolElement => { - const copy = status.copyEntity; - - expect(isExternalToolElement(copy)).toEqual(true); - - return copy as ExternalToolElement; - }; - - it('should return a external tool element as copy', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(isExternalToolElement(result.copyEntity)).toEqual(true); - }); - - it('should not copy tool', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getExternalToolElementFromStatus(result); - - expect(copy.contextExternalToolId).toBeUndefined(); - }); - - it('should create new id', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getExternalToolElementFromStatus(result); - - expect(copy.id).not.toEqual(original.id); - }); - - it('should show status successful', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(result.status).toEqual(CopyStatusEnum.SUCCESS); - }); - - it('should show type ExternalToolElement', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(result.type).toEqual(CopyElementType.EXTERNAL_TOOL_ELEMENT); - }); - }); - - describe('when the element has a linked tool and the feature is active', () => { - describe('when the linked tool exists', () => { - const setup = () => { - const originalTool: ContextExternalTool = contextExternalToolFactory.buildWithId(); - const copiedTool: ContextExternalTool = contextExternalToolFactory.buildWithId(); - - const original: ExternalToolElement = externalToolElementFactory.build({ - contextExternalToolId: originalTool.id, - }); - - contextExternalToolService.findById.mockResolvedValueOnce(originalTool); - contextExternalToolService.copyContextExternalTool.mockResolvedValueOnce(copiedTool); - - return { original, ...setupfileCopyService(), copiedTool }; - }; - - const getExternalToolElementFromStatus = (status: CopyStatus): ExternalToolElement => { - const copy = status.copyEntity; - - expect(isExternalToolElement(copy)).toEqual(true); - - return copy as ExternalToolElement; - }; - - it('should return a external tool element as copy', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(isExternalToolElement(result.copyEntity)).toEqual(true); - }); - - it('should copy tool', async () => { - const { original, fileCopyService, copiedTool } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getExternalToolElementFromStatus(result); - - expect(copy.contextExternalToolId).toEqual(copiedTool.id); - }); - - it('should create new id', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getExternalToolElementFromStatus(result); - - expect(copy.id).not.toEqual(original.id); - }); - - it('should show status successful', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(result.status).toEqual(CopyStatusEnum.SUCCESS); - }); - - it('should show type ExternalToolElement', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(result.type).toEqual(CopyElementType.EXTERNAL_TOOL_ELEMENT); - }); - }); - - describe('when the linked tool does not exist anymore', () => { - const setup = () => { - const original: ExternalToolElement = externalToolElementFactory.build({ - contextExternalToolId: new ObjectId().toHexString(), - }); - - contextExternalToolService.findById.mockResolvedValueOnce(null); - - return { original, ...setupfileCopyService() }; - }; - - const getExternalToolElementFromStatus = (status: CopyStatus): ExternalToolElement => { - const copy = status.copyEntity; - - expect(isExternalToolElement(copy)).toEqual(true); - - return copy as ExternalToolElement; - }; - - it('should return a external tool element as copy', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(isExternalToolElement(result.copyEntity)).toEqual(true); - }); - - it('should not try to copy the tool', async () => { - const { original, fileCopyService } = setup(); - - await service.copy({ original, fileCopyService }); - - expect(contextExternalToolService.copyContextExternalTool).not.toHaveBeenCalled(); - }); - - it('should create new id', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getExternalToolElementFromStatus(result); - - expect(copy.id).not.toEqual(original.id); - }); - - it('should show status fail', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(result.status).toEqual(CopyStatusEnum.FAIL); - }); - - it('should show type ExternalToolElement', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(result.type).toEqual(CopyElementType.EXTERNAL_TOOL_ELEMENT); - }); - }); - }); - }); - - describe('when copying a link element', () => { - const setup = () => { - const original = linkElementFactory.build(); - - return { original, ...setupfileCopyService() }; - }; - - const getLinkElementFromStatus = (status: CopyStatus): LinkElement => { - const copy = status.copyEntity; - - expect(isLinkElement(copy)).toEqual(true); - - return copy as LinkElement; - }; - - it('should return a link element as copy', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(isLinkElement(result.copyEntity)).toEqual(true); - }); - - it('should create new id', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - const copy = getLinkElementFromStatus(result); - - expect(copy.id).not.toEqual(original.id); - }); - - it('should show status successful', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(result.status).toEqual(CopyStatusEnum.SUCCESS); - }); - - it('should be of type LinkElement', async () => { - const { original, fileCopyService } = setup(); - - const result = await service.copy({ original, fileCopyService }); - - expect(result.type).toEqual(CopyElementType.LINK_ELEMENT); - }); - }); -}); diff --git a/apps/server/src/modules/board/service/board-do-copy-service/board-do-copy.service.ts b/apps/server/src/modules/board/service/board-do-copy-service/board-do-copy.service.ts deleted file mode 100644 index a288bd73d12..00000000000 --- a/apps/server/src/modules/board/service/board-do-copy-service/board-do-copy.service.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { CopyHelperService, CopyStatus } from '@modules/copy-helper'; -import { ContextExternalToolService } from '@modules/tool/context-external-tool/service'; -import { IToolFeatures, ToolFeatures } from '@modules/tool/tool-config'; -import { Inject, Injectable } from '@nestjs/common'; -import { AnyBoardDo } from '@shared/domain/domainobject'; -import { RecursiveCopyVisitor } from './recursive-copy.visitor'; -import { SchoolSpecificFileCopyService } from './school-specific-file-copy.interface'; - -export type BoardDoCopyParams = { - original: AnyBoardDo; - fileCopyService: SchoolSpecificFileCopyService; -}; - -@Injectable() -export class BoardDoCopyService { - constructor( - @Inject(ToolFeatures) private readonly toolFeatures: IToolFeatures, - private readonly contextExternalToolService: ContextExternalToolService, - private readonly copyHelperService: CopyHelperService - ) {} - - public async copy(params: BoardDoCopyParams): Promise { - const visitor = new RecursiveCopyVisitor( - params.fileCopyService, - this.contextExternalToolService, - this.toolFeatures, - this.copyHelperService - ); - - const result = await visitor.copy(params.original); - - return result; - } -} diff --git a/apps/server/src/modules/board/service/board-do-copy-service/index.ts b/apps/server/src/modules/board/service/board-do-copy-service/index.ts deleted file mode 100644 index 6abe888af24..00000000000 --- a/apps/server/src/modules/board/service/board-do-copy-service/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './board-do-copy.service'; -export * from './school-specific-file-copy-service.factory'; -export * from './school-specific-file-copy.interface'; 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 deleted file mode 100644 index c53f5327513..00000000000 --- a/apps/server/src/modules/board/service/board-do-copy-service/recursive-copy.visitor.spec.ts +++ /dev/null @@ -1,351 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ContextExternalToolService } from '@modules/tool/context-external-tool/service'; -import { IToolFeatures, ToolFeatures } from '@modules/tool/tool-config'; -import { Test, TestingModule } from '@nestjs/testing'; -import { Card, CollaborativeTextEditorElement, Column, LinkElement } from '@shared/domain/domainobject'; -import { - cardFactory, - collaborativeTextEditorElementFactory, - columnBoardFactory, - columnFactory, - linkElementFactory, - mediaBoardFactory, - mediaExternalToolElementFactory, - mediaLineFactory, - setupEntities, -} from '@shared/testing'; -import { CopyFileDto } from '@src/modules/files-storage-client/dto'; -import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '../../../copy-helper'; -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; - let contextExternalToolService: DeepMocked; - let copyHelperService: DeepMocked; - - let toolFeatures: IToolFeatures; - - beforeAll(async () => { - module = await Test.createTestingModule({ - providers: [ - RecursiveCopyVisitor, - { - provide: SchoolSpecificFileCopyServiceFactory, - useValue: createMock(), - }, - { - provide: ContextExternalToolService, - useValue: createMock(), - }, - { - provide: CopyHelperService, - useValue: createMock(), - }, - { - provide: ToolFeatures, - useValue: { - ctlToolsCopyEnabled: true, - }, - }, - ], - }).compile(); - - fileCopyServiceFactory = module.get(SchoolSpecificFileCopyServiceFactory); - contextExternalToolService = module.get(ContextExternalToolService); - copyHelperService = module.get(CopyHelperService); - toolFeatures = module.get(ToolFeatures); - - await setupEntities(); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - 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, - contextExternalToolService, - toolFeatures, - copyHelperService - ); - - 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, - contextExternalToolService, - toolFeatures, - copyHelperService - ); - - 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, - contextExternalToolService, - toolFeatures, - copyHelperService - ); - - 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, - contextExternalToolService, - toolFeatures, - copyHelperService - ); - - await visitor.visitLinkElementAsync(linkElement); - const copy = visitor.copyMap.get(linkElement.id) as LinkElement; - - expect(copy.imageUrl).toEqual(''); - }); - }); - }); - - describe('when copying a media board', () => { - const setup = () => { - const element = mediaExternalToolElementFactory.build(); - const line = mediaLineFactory.build({ children: [element] }); - const board = mediaBoardFactory.build({ children: [line] }); - - const visitor = new RecursiveCopyVisitor( - createMock(), - contextExternalToolService, - toolFeatures, - copyHelperService - ); - - return { - board, - visitor, - }; - }; - - it('should fail', async () => { - const { visitor, board } = setup(); - - await visitor.visitMediaBoardAsync(board); - - expect(visitor.resultMap.get(board.id)).toEqual({ - type: CopyElementType.MEDIA_BOARD, - status: CopyStatusEnum.NOT_DOING, - elements: [ - { - type: CopyElementType.MEDIA_LINE, - status: CopyStatusEnum.NOT_DOING, - elements: [ - { - type: CopyElementType.MEDIA_EXTERNAL_TOOL_ELEMENT, - status: CopyStatusEnum.NOT_DOING, - }, - ], - }, - ], - }); - }); - }); - - describe('visitCollaborativeTextEditorElementAsync', () => { - const setup = () => { - const collaborativeTextEditorElement = collaborativeTextEditorElementFactory.build(); - const visitor = new RecursiveCopyVisitor( - createMock(), - contextExternalToolService, - toolFeatures, - copyHelperService - ); - const nowMock = new Date(2020, 1, 1, 0, 0, 0); - - jest.useFakeTimers(); - jest.setSystemTime(nowMock); - - return { - collaborativeTextEditorElement, - visitor, - nowMock, - }; - }; - - it('should add status to the resultMap', async () => { - const { visitor, collaborativeTextEditorElement, nowMock } = setup(); - - await visitor.visitCollaborativeTextEditorElementAsync(collaborativeTextEditorElement); - - const status = visitor.resultMap.get(collaborativeTextEditorElement.id); - const expectedCopyEntity = expect.objectContaining({ - id: expect.any(String), - createdAt: nowMock, - updatedAt: nowMock, - }) as CollaborativeTextEditorElement; - - expect(status).toEqual({ - copyEntity: expectedCopyEntity, - type: CopyElementType.COLLABORATIVE_TEXT_EDITOR_ELEMENT, - status: CopyStatusEnum.PARTIAL, - }); - }); - - it('should add the element to the copyMap', async () => { - const { visitor, collaborativeTextEditorElement, nowMock } = setup(); - - await visitor.visitCollaborativeTextEditorElementAsync(collaborativeTextEditorElement); - - const copyMapElement = visitor.copyMap.get(collaborativeTextEditorElement.id); - expect(copyMapElement?.id).toBeDefined(); - expect(copyMapElement?.createdAt).toEqual(nowMock); - expect(copyMapElement?.updatedAt).toEqual(nowMock); - }); - }); - - describe('visitColumnBoardAsync', () => { - const setup = () => { - const recursiveCopyVisitor = new RecursiveCopyVisitor( - createMock(), - contextExternalToolService, - toolFeatures, - copyHelperService - ); - - const collaborativeTextEditorElement = collaborativeTextEditorElementFactory.build(); - const card = cardFactory.build({ children: [collaborativeTextEditorElement] }); - const boardDo = columnFactory.build({ children: [card] }); - const columnBoard = columnBoardFactory.build({ children: [boardDo] }); - - copyHelperService.deriveStatusFromElements.mockReturnValueOnce(CopyStatusEnum.PARTIAL); - - return { - columnBoard, - recursiveCopyVisitor, - collaborativeTextEditorElement, - }; - }; - - it('should set result map', async () => { - const { columnBoard, recursiveCopyVisitor } = setup(); - - await recursiveCopyVisitor.visitColumnBoardAsync(columnBoard); - - const expectedElements = [ - { - copyEntity: expect.any(Column), - elements: [ - { - copyEntity: expect.any(Card), - elements: [ - { - copyEntity: expect.any(CollaborativeTextEditorElement), - status: CopyStatusEnum.PARTIAL, - type: CopyElementType.COLLABORATIVE_TEXT_EDITOR_ELEMENT, - }, - ], - status: CopyStatusEnum.SUCCESS, - type: CopyElementType.CARD, - }, - ], - status: CopyStatusEnum.SUCCESS, - type: CopyElementType.COLUMN, - }, - ]; - const expectedEntity = { - id: expect.any(String), - title: columnBoard.title, - context: columnBoard.context, - createdAt: expect.any(Date), - updatedAt: expect.any(Date), - isVisible: false, - layout: columnBoard.layout, - }; - const expectedResult = { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - copyEntity: expect.objectContaining(expectedEntity), - type: CopyElementType.COLUMNBOARD, - status: CopyStatusEnum.PARTIAL, - elements: expectedElements, - }; - - expect(recursiveCopyVisitor.resultMap.get(columnBoard.id)).toEqual(expectedResult); - }); - - it('should set copyMap', async () => { - const { columnBoard, recursiveCopyVisitor } = setup(); - - await recursiveCopyVisitor.visitColumnBoardAsync(columnBoard); - - const expectedEntity = { - id: expect.any(String), - title: columnBoard.title, - context: columnBoard.context, - createdAt: expect.any(Date), - updatedAt: expect.any(Date), - isVisible: false, - layout: columnBoard.layout, - }; - - const copyMapElement = recursiveCopyVisitor.copyMap.get(columnBoard.id); - expect(copyMapElement).toEqual(expect.objectContaining(expectedEntity)); - }); - }); -}); 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 deleted file mode 100644 index bfab5b0cf94..00000000000 --- a/apps/server/src/modules/board/service/board-do-copy-service/recursive-copy.visitor.ts +++ /dev/null @@ -1,369 +0,0 @@ -import { FileRecordParentType } from '@infra/rabbitmq'; -import { ObjectId } from '@mikro-orm/mongodb'; -import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; -import { ContextExternalTool } from '@modules/tool/context-external-tool/domain'; -import { ContextExternalToolService } from '@modules/tool/context-external-tool/service'; -import { IToolFeatures } from '@modules/tool/tool-config'; -import { - AnyBoardDo, - BoardCompositeVisitorAsync, - Card, - CollaborativeTextEditorElement, - Column, - ColumnBoard, - DrawingElement, - ExternalToolElement, - FileElement, - MediaBoard, - MediaExternalToolElement, - MediaLine, - RichTextElement, - SubmissionContainerElement, - SubmissionItem, -} from '@shared/domain/domainobject'; -import { LinkElement } from '@shared/domain/domainobject/board/link-element.do'; -import { EntityId } from '@shared/domain/types'; -import { SchoolSpecificFileCopyService } from './school-specific-file-copy.interface'; - -export class RecursiveCopyVisitor implements BoardCompositeVisitorAsync { - resultMap = new Map(); - - copyMap = new Map(); - - constructor( - private readonly fileCopyService: SchoolSpecificFileCopyService, - private readonly contextExternalToolService: ContextExternalToolService, - private readonly toolFeatures: IToolFeatures, - private readonly copyHelperService: CopyHelperService - ) {} - - public async copy(original: AnyBoardDo): Promise { - await original.acceptAsync(this); - - const result = this.resultMap.get(original.id); - /* istanbul ignore next */ - if (result === undefined) { - throw new Error('nothing copied'); - } - return result; - } - - public async visitColumnBoardAsync(original: ColumnBoard): Promise { - await this.visitChildrenOf(original); - - const copy = new ColumnBoard({ - id: new ObjectId().toHexString(), - title: original.title, - context: original.context, - createdAt: new Date(), - updatedAt: new Date(), - children: this.getCopiesForChildrenOf(original), - isVisible: false, - layout: original.layout, - }); - - const copyStatusOfChildren = this.getCopyStatusesForChildrenOf(original); - const status = this.copyHelperService.deriveStatusFromElements(copyStatusOfChildren); - - this.resultMap.set(original.id, { - copyEntity: copy, - type: CopyElementType.COLUMNBOARD, - status, - elements: copyStatusOfChildren, - }); - this.copyMap.set(original.id, copy); - } - - public async visitColumnAsync(original: Column): Promise { - await this.visitChildrenOf(original); - const copy = new Column({ - id: new ObjectId().toHexString(), - title: original.title, - children: this.getCopiesForChildrenOf(original), - createdAt: new Date(), - updatedAt: new Date(), - }); - this.resultMap.set(original.id, { - copyEntity: copy, - type: CopyElementType.COLUMN, - status: CopyStatusEnum.SUCCESS, - elements: this.getCopyStatusesForChildrenOf(original), - }); - this.copyMap.set(original.id, copy); - } - - public async visitCardAsync(original: Card): Promise { - await this.visitChildrenOf(original); - const copy = new Card({ - id: new ObjectId().toHexString(), - title: original.title, - height: original.height, - children: this.getCopiesForChildrenOf(original), - createdAt: new Date(), - updatedAt: new Date(), - }); - this.resultMap.set(original.id, { - copyEntity: copy, - type: CopyElementType.CARD, - status: CopyStatusEnum.SUCCESS, - elements: this.getCopyStatusesForChildrenOf(original), - }); - this.copyMap.set(original.id, copy); - } - - public async visitFileElementAsync(original: FileElement): Promise { - const copy = new FileElement({ - id: new ObjectId().toHexString(), - caption: original.caption, - alternativeText: original.alternativeText, - children: [], - createdAt: new Date(), - updatedAt: new Date(), - }); - const fileCopy = await this.fileCopyService.copyFilesOfParent({ - sourceParentId: original.id, - targetParentId: copy.id, - parentType: FileRecordParentType.BoardNode, - }); - const fileCopyStatus = fileCopy.map((copyFileDto) => { - return { - type: CopyElementType.FILE, - status: copyFileDto.id ? CopyStatusEnum.SUCCESS : CopyStatusEnum.FAIL, - title: copyFileDto.name ?? `(old fileid: ${copyFileDto.sourceId})`, - }; - }); - this.resultMap.set(original.id, { - copyEntity: copy, - type: CopyElementType.FILE_ELEMENT, - status: CopyStatusEnum.SUCCESS, - elements: fileCopyStatus, - }); - this.copyMap.set(original.id, copy); - } - - public async visitDrawingElementAsync(original: DrawingElement): Promise { - const copy = new DrawingElement({ - id: new ObjectId().toHexString(), - description: original.description, - children: [], - createdAt: new Date(), - updatedAt: new Date(), - }); - this.resultMap.set(original.id, { - copyEntity: copy, - type: CopyElementType.DRAWING_ELEMENT, - status: CopyStatusEnum.PARTIAL, - }); - this.copyMap.set(original.id, copy); - - return Promise.resolve(); - } - - public async visitLinkElementAsync(original: LinkElement): Promise { - const copy = new LinkElement({ - id: new ObjectId().toHexString(), - url: original.url, - title: original.title, - imageUrl: original.imageUrl, - children: [], - createdAt: new Date(), - updatedAt: new Date(), - }); - - 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(); - } - - public async visitRichTextElementAsync(original: RichTextElement): Promise { - const copy = new RichTextElement({ - id: new ObjectId().toHexString(), - text: original.text, - inputFormat: original.inputFormat, - children: [], - createdAt: new Date(), - updatedAt: new Date(), - }); - this.resultMap.set(original.id, { - copyEntity: copy, - type: CopyElementType.RICHTEXT_ELEMENT, - status: CopyStatusEnum.SUCCESS, - }); - this.copyMap.set(original.id, copy); - - return Promise.resolve(); - } - - public async visitSubmissionContainerElementAsync(original: SubmissionContainerElement): Promise { - await this.visitChildrenOf(original); - const copy = new SubmissionContainerElement({ - id: new ObjectId().toHexString(), - dueDate: original.dueDate, - children: [], - createdAt: new Date(), - updatedAt: new Date(), - }); - this.resultMap.set(original.id, { - copyEntity: copy, - type: CopyElementType.SUBMISSION_CONTAINER_ELEMENT, - status: CopyStatusEnum.SUCCESS, - elements: this.getCopyStatusesForChildrenOf(original), - }); - this.copyMap.set(original.id, copy); - } - - public async visitSubmissionItemAsync(original: SubmissionItem): Promise { - this.resultMap.set(original.id, { - type: CopyElementType.SUBMISSION_ITEM, - status: CopyStatusEnum.NOT_DOING, - }); - - return Promise.resolve(); - } - - public async visitExternalToolElementAsync(original: ExternalToolElement): Promise { - const boardElementCopy: ExternalToolElement = new ExternalToolElement({ - id: new ObjectId().toHexString(), - contextExternalToolId: undefined, - children: [], - createdAt: new Date(), - updatedAt: new Date(), - }); - - let status: CopyStatusEnum; - if (this.toolFeatures.ctlToolsCopyEnabled && original.contextExternalToolId) { - const linkedTool: ContextExternalTool | null = await this.contextExternalToolService.findById( - original.contextExternalToolId - ); - - if (linkedTool) { - const contextExternalToolCopy: ContextExternalTool = - await this.contextExternalToolService.copyContextExternalTool(linkedTool, boardElementCopy.id); - - boardElementCopy.contextExternalToolId = contextExternalToolCopy.id; - - status = CopyStatusEnum.SUCCESS; - } else { - status = CopyStatusEnum.FAIL; - } - } else { - status = CopyStatusEnum.SUCCESS; - } - - this.resultMap.set(original.id, { - copyEntity: boardElementCopy, - type: CopyElementType.EXTERNAL_TOOL_ELEMENT, - status, - }); - this.copyMap.set(original.id, boardElementCopy); - - return Promise.resolve(); - } - - public async visitCollaborativeTextEditorElementAsync(original: CollaborativeTextEditorElement): Promise { - const now = new Date(); - const copy = new CollaborativeTextEditorElement({ - id: new ObjectId().toHexString(), - createdAt: now, - updatedAt: now, - }); - - this.resultMap.set(original.id, { - copyEntity: copy, - type: CopyElementType.COLLABORATIVE_TEXT_EDITOR_ELEMENT, - status: CopyStatusEnum.PARTIAL, - }); - this.copyMap.set(original.id, copy); - - return Promise.resolve(); - } - - public async visitMediaBoardAsync(original: MediaBoard): Promise { - await this.visitChildrenOf(original); - - this.resultMap.set(original.id, { - type: CopyElementType.MEDIA_BOARD, - status: CopyStatusEnum.NOT_DOING, - elements: this.getCopyStatusesForChildrenOf(original), - }); - } - - public async visitMediaLineAsync(original: MediaLine): Promise { - await this.visitChildrenOf(original); - - this.resultMap.set(original.id, { - type: CopyElementType.MEDIA_LINE, - status: CopyStatusEnum.NOT_DOING, - elements: this.getCopyStatusesForChildrenOf(original), - }); - } - - public visitMediaExternalToolElementAsync(original: MediaExternalToolElement): Promise { - this.resultMap.set(original.id, { - type: CopyElementType.MEDIA_EXTERNAL_TOOL_ELEMENT, - status: CopyStatusEnum.NOT_DOING, - }); - - return Promise.resolve(); - } - - private async visitChildrenOf(boardDo: AnyBoardDo): Promise[]> { - return Promise.allSettled(boardDo.children.map((child) => child.acceptAsync(this))); - } - - private getCopyStatusesForChildrenOf(original: AnyBoardDo) { - const childstatusses: CopyStatus[] = []; - - original.children.forEach((child) => { - const childStatus = this.resultMap.get(child.id); - if (childStatus) { - childstatusses.push(childStatus); - } - }); - - return childstatusses; - } - - private getCopiesForChildrenOf(original: AnyBoardDo) { - const copies: AnyBoardDo[] = []; - original.children.forEach((child) => { - const childCopy = this.copyMap.get(child.id); - if (childCopy) { - copies.push(childCopy); - } - }); - - return copies; - } -} diff --git a/apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy-service.factory.spec.ts b/apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy-service.factory.spec.ts deleted file mode 100644 index 4271949bd16..00000000000 --- a/apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy-service.factory.spec.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { setupEntities } from '@shared/testing'; -import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; -import { FileRecordParentType } from '@modules/files-storage/entity'; -import { ObjectId } from '@mikro-orm/mongodb'; -import { SchoolSpecificFileCopyServiceFactory } from './school-specific-file-copy-service.factory'; -import { SchoolSpecificFileCopyServiceImpl } from './school-specific-file-copy.service'; - -describe('school specific file copy service factory', () => { - let module: TestingModule; - let factory: SchoolSpecificFileCopyServiceFactory; - let adapter: DeepMocked; - - afterAll(async () => { - await module.close(); - }); - - beforeAll(async () => { - await setupEntities(); - module = await Test.createTestingModule({ - providers: [ - SchoolSpecificFileCopyServiceFactory, - { - provide: FilesStorageClientAdapterService, - useValue: createMock(), - }, - ], - }).compile(); - factory = module.get(SchoolSpecificFileCopyServiceFactory); - adapter = module.get(FilesStorageClientAdapterService); - }); - - it('should build a school specific file copy service', () => { - const service = factory.build({ - targetSchoolId: new ObjectId().toHexString(), - sourceSchoolId: new ObjectId().toHexString(), - userId: new ObjectId().toHexString(), - }); - - expect(service).toBeInstanceOf(SchoolSpecificFileCopyServiceImpl); - }); - - describe('using created service', () => { - const setup = () => { - const userId = new ObjectId().toHexString(); - const sourceParentId = new ObjectId().toHexString(); - const targetParentId = new ObjectId().toHexString(); - const parentType = FileRecordParentType.BoardNode; - const sourceSchoolId = new ObjectId().toHexString(); - const targetSchoolId = new ObjectId().toHexString(); - - const service = factory.build({ - targetSchoolId, - sourceSchoolId, - userId, - }); - - const mockResult = [ - { id: new ObjectId().toHexString(), sourceId: new ObjectId().toHexString(), name: 'filename' }, - ]; - adapter.copyFilesOfParent.mockResolvedValue(mockResult); - - return { - service, - mockResult, - userId, - sourceParentId, - targetParentId, - parentType, - sourceSchoolId, - targetSchoolId, - }; - }; - - it('should call FilesStorageClientAdapterService with user and schoolIds', async () => { - const { service, userId, sourceParentId, targetParentId, parentType, sourceSchoolId, targetSchoolId } = setup(); - - await service.copyFilesOfParent({ - sourceParentId, - targetParentId, - parentType, - }); - - expect(adapter.copyFilesOfParent).toHaveBeenCalledWith({ - source: { - parentId: sourceParentId, - parentType, - schoolId: sourceSchoolId, - }, - target: { - parentId: targetParentId, - parentType, - schoolId: targetSchoolId, - }, - userId, - }); - }); - - it('should return result of adapter operation', async () => { - const { service, sourceParentId, targetParentId, parentType, mockResult } = setup(); - - const result = await service.copyFilesOfParent({ - sourceParentId, - targetParentId, - parentType, - }); - - expect(result).toEqual(mockResult); - }); - }); -}); diff --git a/apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy-service.factory.ts b/apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy-service.factory.ts deleted file mode 100644 index 424033fa974..00000000000 --- a/apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy-service.factory.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; -import { - SchoolSpecificFileCopyService, - SchoolSpecificFileCopyServiceProps, -} from './school-specific-file-copy.interface'; -import { SchoolSpecificFileCopyServiceImpl } from './school-specific-file-copy.service'; - -@Injectable() -export class SchoolSpecificFileCopyServiceFactory { - constructor(private readonly filesStorageClientAdapterService: FilesStorageClientAdapterService) {} - - build(props: SchoolSpecificFileCopyServiceProps): SchoolSpecificFileCopyService { - return new SchoolSpecificFileCopyServiceImpl(this.filesStorageClientAdapterService, props); - } -} diff --git a/apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy.interface.ts b/apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy.interface.ts deleted file mode 100644 index 2a3b769f580..00000000000 --- a/apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy.interface.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { CopyFileDto } from '@modules/files-storage-client/dto'; -import { FileRecordParentType } from '@modules/files-storage/entity'; -import { EntityId } from '@shared/domain/types'; - -export type SchoolSpecificFileCopyServiceCopyParams = { - sourceParentId: EntityId; - targetParentId: EntityId; - parentType: FileRecordParentType; -}; - -export type SchoolSpecificFileCopyServiceProps = { - sourceSchoolId: EntityId; - targetSchoolId: EntityId; - userId: EntityId; -}; - -export interface SchoolSpecificFileCopyService { - copyFilesOfParent(params: SchoolSpecificFileCopyServiceCopyParams): Promise; -} diff --git a/apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy.service.ts b/apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy.service.ts deleted file mode 100644 index 1f3fa5f5193..00000000000 --- a/apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy.service.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; -import { CopyFileDto } from '@modules/files-storage-client/dto'; -import { - SchoolSpecificFileCopyService, - SchoolSpecificFileCopyServiceCopyParams, - SchoolSpecificFileCopyServiceProps, -} from './school-specific-file-copy.interface'; - -export class SchoolSpecificFileCopyServiceImpl implements SchoolSpecificFileCopyService { - constructor( - private readonly filesStorageClientAdapterService: FilesStorageClientAdapterService, - private readonly props: SchoolSpecificFileCopyServiceProps - ) {} - - public async copyFilesOfParent(params: SchoolSpecificFileCopyServiceCopyParams): Promise { - return this.filesStorageClientAdapterService.copyFilesOfParent({ - source: { - parentId: params.sourceParentId, - parentType: params.parentType, - schoolId: this.props.sourceSchoolId, - }, - target: { - parentId: params.targetParentId, - parentType: params.parentType, - schoolId: this.props.targetSchoolId, - }, - userId: this.props.userId, - }); - } -} diff --git a/apps/server/src/modules/board/service/board-do-copy-service/swap-internal-links.visitor.spec.ts b/apps/server/src/modules/board/service/board-do-copy-service/swap-internal-links.visitor.spec.ts deleted file mode 100644 index c831f97595f..00000000000 --- a/apps/server/src/modules/board/service/board-do-copy-service/swap-internal-links.visitor.spec.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { ObjectId } from '@mikro-orm/mongodb'; -import { LinkElement, MediaBoard, MediaExternalToolElement, MediaLine } from '@shared/domain/domainobject'; -import { EntityId } from '@shared/domain/types'; -import { - cardFactory, - collaborativeTextEditorElementFactory, - columnBoardFactory, - columnFactory, - drawingElementFactory, - externalToolElementFactory, - fileElementFactory, - linkElementFactory, - mediaBoardFactory, - mediaExternalToolElementFactory, - mediaLineFactory, - richTextElementFactory, - submissionContainerElementFactory, - submissionItemFactory, -} from '@shared/testing'; -import { SwapInternalLinksVisitor } from './swap-internal-links.visitor'; - -describe('swap internal links visitor', () => { - it('should keep link unchanged', () => { - const map = new Map(); - const linkElement = linkElementFactory.build({ url: 'testurl.dev' }); - const visitor = new SwapInternalLinksVisitor(map); - - linkElement.accept(visitor); - - expect(linkElement.url).toEqual('testurl.dev'); - }); - - const setupIdPair = () => { - const originalId = new ObjectId().toString(); - const newId = new ObjectId().toString(); - const value = { - originalId, - newId, - originalUrl: `testurl.dev/${originalId}`, - expectedUrl: `testurl.dev/${newId}`, - }; - return value; - }; - - const buildIdMap = (pairs: { originalId: string; newId: string }[]) => { - const map = new Map(); - pairs.forEach((pair) => { - map.set(pair.originalId, pair.newId); - }); - return map; - }; - - const buildBoardContaining = (linkelelements: LinkElement[]) => { - const cardContainingLinks = cardFactory.build({ children: linkelelements }); - const submissionContainer = submissionContainerElementFactory.build({ - children: [ - submissionItemFactory.build({ - children: [richTextElementFactory.build()], - }), - ], - }); - const cardContainingOthers = cardFactory.build({ - children: [ - richTextElementFactory.build(), - fileElementFactory.build(), - submissionContainer, - drawingElementFactory.build(), - externalToolElementFactory.build(), - collaborativeTextEditorElementFactory.build(), - ], - }); - const column = columnFactory.build({ - children: [cardContainingLinks, cardContainingOthers], - }); - const columnBoard = columnBoardFactory.build({ - children: [column], - }); - return columnBoard; - }; - - describe('when a single id is matched', () => { - const setupWithIdPair = () => { - const pair = setupIdPair(); - const map = buildIdMap([pair]); - const visitor = new SwapInternalLinksVisitor(map); - - return { pair, visitor }; - }; - - it('should change ids in link', () => { - const { pair, visitor } = setupWithIdPair(); - const linkElement = linkElementFactory.build({ url: pair.originalUrl }); - - linkElement.accept(visitor); - - expect(linkElement.url).toEqual(pair.expectedUrl); - }); - - it('should change ids in multiple matching links', () => { - const { pair, visitor } = setupWithIdPair(); - const firstLinkElement = linkElementFactory.build({ url: pair.originalUrl }); - const secondLinkElement = linkElementFactory.build({ url: pair.originalUrl }); - const root = buildBoardContaining([firstLinkElement, secondLinkElement]); - - root.accept(visitor); - - expect(firstLinkElement.url).toEqual(pair.expectedUrl); - expect(secondLinkElement.url).toEqual(pair.expectedUrl); - }); - }); - - describe('when multiple different ids are matched', () => { - const setupWithMultipleIds = () => { - const pairs = [setupIdPair(), setupIdPair()]; - - const idMap = buildIdMap(pairs); - const visitor = new SwapInternalLinksVisitor(idMap); - - return { visitor, pairs }; - }; - - const buildLinkElementsWithUrls = (urls: string[]) => urls.map((url) => linkElementFactory.build({ url })); - - it('should change multiple ids in different links', () => { - const { visitor, pairs } = setupWithMultipleIds(); - const linkElements = buildLinkElementsWithUrls(pairs.map((pair) => pair.originalUrl)); - const root = buildBoardContaining(linkElements); - - root.accept(visitor); - - expect(linkElements[0].url).toEqual(pairs[0].expectedUrl); - expect(linkElements[1].url).toEqual(pairs[1].expectedUrl); - }); - }); - - describe('when it is a media board', () => { - const setup = () => { - const element = mediaExternalToolElementFactory.build(); - const elementCopy = new MediaExternalToolElement(element.getProps()); - const line = mediaLineFactory.build({ children: [element] }); - const lineCopy = new MediaLine(line.getProps()); - const board = mediaBoardFactory.build({ children: [line] }); - const boardCopy = new MediaBoard(board.getProps()); - - const visitor = new SwapInternalLinksVisitor(new Map()); - - return { - element, - elementCopy, - line, - lineCopy, - board, - boardCopy, - visitor, - }; - }; - - it('should do nothing', () => { - const { visitor, element, elementCopy, line, lineCopy, board, boardCopy } = setup(); - - visitor.visitMediaBoard(board); - - expect(element).toEqual(elementCopy); - expect(line).toEqual(lineCopy); - expect(board).toEqual(boardCopy); - }); - }); -}); diff --git a/apps/server/src/modules/board/service/board-do-copy-service/swap-internal-links.visitor.ts b/apps/server/src/modules/board/service/board-do-copy-service/swap-internal-links.visitor.ts deleted file mode 100644 index 3b65325211a..00000000000 --- a/apps/server/src/modules/board/service/board-do-copy-service/swap-internal-links.visitor.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { - AnyBoardDo, - BoardCompositeVisitor, - Card, - Column, - ColumnBoard, - DrawingElement, - LinkElement, - MediaBoard, - MediaLine, - SubmissionContainerElement, - SubmissionItem, -} from '@shared/domain/domainobject'; -import { EntityId } from '@shared/domain/types'; - -export class SwapInternalLinksVisitor implements BoardCompositeVisitor { - constructor(private readonly idMap: Map) {} - - visitDrawingElement(drawingElement: DrawingElement): void { - this.visitChildrenOf(drawingElement); - } - - visitCard(card: Card): void { - this.visitChildrenOf(card); - } - - visitColumn(column: Column): void { - this.visitChildrenOf(column); - } - - visitColumnBoard(columnBoard: ColumnBoard): void { - this.visitChildrenOf(columnBoard); - } - - visitExternalToolElement(): void { - this.doNothing(); - } - - visitFileElement(): void { - this.doNothing(); - } - - visitLinkElement(linkElement: LinkElement): void { - this.idMap.forEach((value, key) => { - linkElement.url = linkElement.url.replace(key, value); - }); - } - - visitRichTextElement(): void { - this.doNothing(); - } - - visitSubmissionContainerElement(submissionContainerElement: SubmissionContainerElement): void { - this.visitChildrenOf(submissionContainerElement); - } - - visitSubmissionItem(submissionItem: SubmissionItem): void { - this.visitChildrenOf(submissionItem); - } - - visitCollaborativeTextEditorElement(): void { - this.doNothing(); - } - - private visitChildrenOf(boardDo: AnyBoardDo) { - boardDo.children.forEach((child) => child.accept(this)); - } - - private doNothing() {} - - visitMediaBoard(mediaBoard: MediaBoard): void { - this.visitChildrenOf(mediaBoard); - } - - visitMediaLine(mediaLine: MediaLine): void { - this.visitChildrenOf(mediaLine); - } - - visitMediaExternalToolElement(): void { - this.doNothing(); - } -} diff --git a/apps/server/src/modules/board/service/board-do.service.spec.ts b/apps/server/src/modules/board/service/board-do.service.spec.ts deleted file mode 100644 index 399f4e49e88..00000000000 --- a/apps/server/src/modules/board/service/board-do.service.spec.ts +++ /dev/null @@ -1,260 +0,0 @@ -import { DeepMocked, createMock } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { - cardFactory, - columnBoardFactory, - columnFactory, - richTextElementFactory, -} from '@shared/testing/factory/domainobject'; -import { ColumnBoard } from '@shared/domain/domainobject'; -import { NotFoundLoggableException } from '@shared/common/loggable-exception'; -import { BoardDoRepo } from '../repo'; -import { BoardDoService } from './board-do.service'; - -describe(BoardDoService.name, () => { - let module: TestingModule; - let service: BoardDoService; - let boardDoRepo: DeepMocked; - - beforeAll(async () => { - module = await Test.createTestingModule({ - providers: [ - BoardDoService, - { - provide: BoardDoRepo, - useValue: createMock(), - }, - ], - }).compile(); - - service = module.get(BoardDoService); - boardDoRepo = module.get(BoardDoRepo); - }); - - afterAll(async () => { - await module.close(); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - describe('move', () => { - describe('when moving a card from one column to another', () => { - const setup = () => { - const sourceCards = cardFactory.buildList(3); - const sourceColumn = columnFactory.build({ children: sourceCards }); - - const targetCards = cardFactory.buildList(2); - const targetColumn = columnFactory.build({ children: targetCards }); - - return { sourceColumn, targetColumn, sourceCards, targetCards }; - }; - - it('should remove it from the source column', async () => { - const { sourceCards, sourceColumn, targetColumn } = setup(); - const card = sourceCards[0]; - boardDoRepo.findParentOfId.mockResolvedValueOnce(sourceColumn); - jest.spyOn(sourceColumn, 'removeChild'); - - await service.move(card, targetColumn, 0); - - expect(sourceColumn.removeChild).toBeCalledWith(card); - }); - - it('should add it to the target column at specified position', async () => { - const { sourceCards, sourceColumn, targetColumn } = setup(); - const card = sourceCards[0]; - boardDoRepo.findParentOfId.mockResolvedValueOnce(sourceColumn); - jest.spyOn(targetColumn, 'addChild'); - - await service.move(card, targetColumn, 1); - - expect(targetColumn.addChild).toBeCalledWith(card, 1); - }); - }); - - describe('when moving a card within the same column', () => { - const setup = () => { - const cards = cardFactory.buildList(3); - const column = columnFactory.build({ children: cards }); - boardDoRepo.findParentOfId.mockResolvedValueOnce(column); - - return { column, cards }; - }; - - it('should remove it from the column', async () => { - const { cards, column } = setup(); - const card = cards[0]; - jest.spyOn(column, 'removeChild'); - - await service.move(card, column, 2); - - expect(column.removeChild).toBeCalledWith(card); - }); - - it('should add it to the column at specified position', async () => { - const { cards, column } = setup(); - const card = cards[0]; - jest.spyOn(column, 'addChild'); - - await service.move(card, column, 1); - - expect(column.addChild).toBeCalledWith(card, 1); - }); - }); - - describe('when repo does not return the same DO instance', () => { - describe('when moving a card within the same column', () => { - // Note: We don not have (yet) an identity map for our domain objects. - // That's why each call to the repo finders yields a new instance! - // This test is for that situation and can be removed later. - const setup = () => { - const cards = cardFactory.buildList(3); - const column = columnFactory.build({ children: cards }); - const columnClone = columnFactory.build({ id: column.id, children: column.children }); - boardDoRepo.findParentOfId.mockResolvedValueOnce(columnClone); - - return { column, cards, columnClone }; - }; - - it('should remove it from the column', async () => { - const { cards, column } = setup(); - const card = cards[0]; - jest.spyOn(column, 'removeChild'); - - await service.move(card, column, 2); - - expect(column.removeChild).toBeCalledWith(card); - }); - - it('should add it to the column at specified position', async () => { - const { cards, column } = setup(); - const card = cards[0]; - jest.spyOn(column, 'addChild'); - - await service.move(card, column, 1); - - expect(column.addChild).toBeCalledWith(card, 1); - }); - }); - }); - - describe('when moving a root card to a column', () => { - const setup = () => { - const card = cardFactory.build(); - const targetColumn = columnFactory.build(); - boardDoRepo.findParentOfId.mockResolvedValueOnce(undefined); - - return { card, targetColumn }; - }; - - it('should add it to the column at specified position', async () => { - const { card, targetColumn } = setup(); - jest.spyOn(targetColumn, 'addChild'); - - await service.move(card, targetColumn, 0); - - expect(targetColumn.addChild).toBeCalledWith(card, 0); - }); - }); - }); - - describe('deleteWithDescendants', () => { - describe('when deleting an element', () => { - const setup = () => { - const elements = richTextElementFactory.buildList(3); - const card = cardFactory.build({ children: elements }); - boardDoRepo.findParentOfId.mockResolvedValueOnce(card); - - return { card, elements }; - }; - - it('should remove the element from the parent', async () => { - const { elements, card } = setup(); - const element = elements[0]; - jest.spyOn(card, 'removeChild'); - - await service.deleteWithDescendants(element); - - expect(card.removeChild).toHaveBeenCalledWith(element); - }); - - it('should update the siblings', async () => { - const { elements, card } = setup(); - const element = elements[0]; - jest.spyOn(card, 'removeChild'); - const expectedElements = [elements[1], elements[2]]; - - await service.deleteWithDescendants(element); - - expect(boardDoRepo.save).toHaveBeenCalledWith(expectedElements, card); - }); - }); - }); - - describe('getRootBoardDo', () => { - describe('when searching a board for an element', () => { - const setup2 = () => { - const element = richTextElementFactory.build(); - const board = columnBoardFactory.build({ children: [element] }); - - boardDoRepo.getAncestorIds.mockResolvedValue([board.id]); - boardDoRepo.findById.mockResolvedValue(board); - - return { - element, - board, - }; - }; - - it('should return the board', async () => { - const { element, board } = setup2(); - - const result = await service.getRootBoardDo(element); - - expect(result).toEqual(board); - }); - }); - - describe('when searching a board by itself', () => { - const setup2 = () => { - const board: ColumnBoard = columnBoardFactory.build({ children: [] }); - - boardDoRepo.getAncestorIds.mockResolvedValue([]); - boardDoRepo.findById.mockResolvedValue(board); - - return { - board, - }; - }; - - it('should return the board', async () => { - const { board } = setup2(); - - const result = await service.getRootBoardDo(board); - - expect(result).toEqual(board); - }); - }); - - describe('when the root node is not a board', () => { - const setup2 = () => { - const element = richTextElementFactory.build(); - - boardDoRepo.getAncestorIds.mockResolvedValue([]); - boardDoRepo.findById.mockResolvedValue(element); - - return { - element, - }; - }; - - it('should throw a NotFoundLoggableException', async () => { - const { element } = setup2(); - - await expect(service.getRootBoardDo(element)).rejects.toThrow(NotFoundLoggableException); - }); - }); - }); -}); diff --git a/apps/server/src/modules/board/service/board-do.service.ts b/apps/server/src/modules/board/service/board-do.service.ts deleted file mode 100644 index b312b1fc093..00000000000 --- a/apps/server/src/modules/board/service/board-do.service.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { NotFoundLoggableException } from '@shared/common/loggable-exception'; -import { AnyBoardDo, ColumnBoard, MediaBoard } from '@shared/domain/domainobject'; -import { EntityId } from '@shared/domain/types'; -import { BoardDoRepo } from '../repo'; - -@Injectable() -export class BoardDoService { - constructor(private readonly boardDoRepo: BoardDoRepo) {} - - async deleteWithDescendants(domainObject: AnyBoardDo): Promise { - const parent = await this.boardDoRepo.findParentOfId(domainObject.id); - - if (parent) { - parent.removeChild(domainObject); - await this.boardDoRepo.save(parent.children, parent); - } - - await this.boardDoRepo.delete(domainObject); - } - - async move(child: AnyBoardDo, targetParent: AnyBoardDo, targetPosition?: number): Promise { - if (targetParent.hasChild(child)) { - targetParent.removeChild(child); - } else { - const sourceParent = await this.boardDoRepo.findParentOfId(child.id); - if (sourceParent) { - sourceParent.removeChild(child); - await this.boardDoRepo.save(sourceParent.children, sourceParent); - } - } - targetParent.addChild(child, targetPosition); - await this.boardDoRepo.save(targetParent.children, targetParent); - } - - // TODO there is a similar method in board-do-authorizable.service.ts - async getRootBoardDo(boardDo: AnyBoardDo): Promise { - const ancestorIds: EntityId[] = await this.boardDoRepo.getAncestorIds(boardDo); - const idHierarchy: EntityId[] = [...ancestorIds, boardDo.id]; - const rootId: EntityId = idHierarchy[0]; - const rootBoardDo: AnyBoardDo = await this.boardDoRepo.findById(rootId, 1); - - if (rootBoardDo instanceof ColumnBoard || rootBoardDo instanceof MediaBoard) { - return rootBoardDo; - } - - throw new NotFoundLoggableException(ColumnBoard.name, { id: rootId }); - } -} diff --git a/apps/server/src/modules/board/service/board-node-authorizable.service.spec.ts b/apps/server/src/modules/board/service/board-node-authorizable.service.spec.ts new file mode 100644 index 00000000000..86242decfd5 --- /dev/null +++ b/apps/server/src/modules/board/service/board-node-authorizable.service.spec.ts @@ -0,0 +1,140 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { setupEntities } from '@shared/testing'; +import { columnBoardFactory, columnFactory } from '../testing'; +import { BoardNodeAuthorizable, BoardRoles, UserWithBoardRoles } from '../domain'; +import { BoardNodeRepo } from '../repo'; +import { BoardContextService } from './internal/board-context.service'; +import { BoardNodeAuthorizableService } from './board-node-authorizable.service'; +import { BoardNodeService } from './board-node.service'; + +describe(BoardNodeAuthorizableService.name, () => { + let module: TestingModule; + let service: BoardNodeAuthorizableService; + let boardNodeRepo: DeepMocked; + let boardNodeService: DeepMocked; + let boardContextService: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + BoardNodeAuthorizableService, + { + provide: BoardNodeRepo, + useValue: createMock(), + }, + { + provide: BoardNodeService, + useValue: createMock(), + }, + { + provide: BoardContextService, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(BoardNodeAuthorizableService); + boardNodeRepo = module.get(BoardNodeRepo); + boardNodeService = module.get(BoardNodeService); + boardContextService = module.get(BoardContextService); + + await setupEntities(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('findById', () => { + describe('when finding a board domainobject', () => { + const setup = () => { + const column = columnFactory.build(); + const columnBoard = columnBoardFactory.build({ children: [column] }); + boardNodeRepo.findById.mockResolvedValueOnce(column); + + const authorizable: BoardNodeAuthorizable = new BoardNodeAuthorizable({ + id: column.id, + boardNode: column, + rootNode: columnBoard, + users: [], + }); + + return { columnBoard, column, authorizable }; + }; + + it('should call the repository', async () => { + const { column } = setup(); + + await service.findById(column.id); + + expect(boardNodeRepo.findById).toHaveBeenCalledWith(column.id, 1); + }); + + it('should return the result from getBoardAuthorizable', async () => { + const { column, authorizable } = setup(); + const spy = jest.spyOn(service, 'getBoardAuthorizable').mockResolvedValueOnce(authorizable); + + const result = await service.findById(column.id); + + expect(result).toEqual(authorizable); + + spy.mockRestore(); + }); + }); + }); + + describe('getBoardAuthorizable', () => { + const setup = () => { + const column = columnFactory.build(); + const columnBoard = columnBoardFactory.build({ children: [column] }); + + boardNodeService.findParent.mockResolvedValueOnce(columnBoard); + boardNodeService.findRoot.mockResolvedValueOnce(columnBoard); + const usersWithRoles: UserWithBoardRoles[] = [ + { + userId: columnBoard.context.id, + roles: [BoardRoles.EDITOR], + }, + ]; + boardContextService.getUsersWithBoardRoles.mockResolvedValue(usersWithRoles); + + return { columnBoard, column, usersWithRoles }; + }; + + it('should call the service to get the parent node', async () => { + const { column } = setup(); + + await service.getBoardAuthorizable(column); + + expect(boardNodeService.findParent).toHaveBeenCalledWith(column, 1); + }); + + it('should call the service to get the root node', async () => { + const { column } = setup(); + + await service.getBoardAuthorizable(column); + + expect(boardNodeService.findRoot).toHaveBeenCalledWith(column, 1); + }); + + it('should return an authorizable of the root context', async () => { + const { column, columnBoard, usersWithRoles } = setup(); + + const result = await service.getBoardAuthorizable(column); + const expected = new BoardNodeAuthorizable({ + users: usersWithRoles, + id: column.id, + boardNode: column, + rootNode: columnBoard, + parentNode: columnBoard, + }); + + expect(result).toEqual(expected); + }); + }); +}); diff --git a/apps/server/src/modules/board/service/board-node-authorizable.service.ts b/apps/server/src/modules/board/service/board-node-authorizable.service.ts new file mode 100644 index 00000000000..bc9a846311f --- /dev/null +++ b/apps/server/src/modules/board/service/board-node-authorizable.service.ts @@ -0,0 +1,43 @@ +import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { type EntityId } from '@shared/domain/types'; +import { type AuthorizationLoaderService } from '@modules/authorization'; +import { AnyBoardNode, BoardNodeAuthorizable } from '../domain'; +import { BoardNodeRepo } from '../repo'; +import { BoardContextService } from './internal/board-context.service'; +import { BoardNodeService } from './board-node.service'; + +@Injectable() +export class BoardNodeAuthorizableService implements AuthorizationLoaderService { + constructor( + @Inject(forwardRef(() => BoardNodeRepo)) private readonly boardNodeRepo: BoardNodeRepo, + private readonly boardNodeService: BoardNodeService, + private readonly boardContextService: BoardContextService + ) {} + + /** + * @deprecated + */ + async findById(id: EntityId): Promise { + const boardNode = await this.boardNodeRepo.findById(id, 1); + + const boardNodeAuthorizable = this.getBoardAuthorizable(boardNode); + + return boardNodeAuthorizable; + } + + async getBoardAuthorizable(boardNode: AnyBoardNode): Promise { + const rootNode = await this.boardNodeService.findRoot(boardNode, 1); + const parentNode = await this.boardNodeService.findParent(boardNode, 1); + const users = await this.boardContextService.getUsersWithBoardRoles(rootNode); + + const boardNodeAuthorizable = new BoardNodeAuthorizable({ + users, + id: boardNode.id, + boardNode, + rootNode, + parentNode, + }); + + return boardNodeAuthorizable; + } +} diff --git a/apps/server/src/modules/board/service/board-node-permission.service.spec.ts b/apps/server/src/modules/board/service/board-node-permission.service.spec.ts new file mode 100644 index 00000000000..3d636d1561c --- /dev/null +++ b/apps/server/src/modules/board/service/board-node-permission.service.spec.ts @@ -0,0 +1,166 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Action, AuthorizationContext, AuthorizationService } from '@modules/authorization'; +import { Test, TestingModule } from '@nestjs/testing'; +import { setupEntities, userFactory } from '@shared/testing'; +import { BoardNodeAuthorizable, BoardRoles, UserWithBoardRoles } from '../domain'; +import { BoardNodeAuthorizableService } from './board-node-authorizable.service'; +import { BoardNodePermissionService } from './board-node-permission.service'; +import { columnBoardFactory } from '../testing'; + +describe(BoardNodePermissionService.name, () => { + let module: TestingModule; + let service: BoardNodePermissionService; + let authorizationService: DeepMocked; + let boardNodeAuthorizableService: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + BoardNodePermissionService, + { + provide: AuthorizationService, + useValue: createMock(), + }, + { + provide: BoardNodeAuthorizableService, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(BoardNodePermissionService); + authorizationService = module.get(AuthorizationService); + boardNodeAuthorizableService = module.get(BoardNodeAuthorizableService); + + await setupEntities(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('checkPermission', () => { + const setup = () => { + const user = userFactory.build(); + const anyBoardDo = columnBoardFactory.build(); + + const boardNodeAuthorizable = new BoardNodeAuthorizable({ + users: [{ userId: user.id, roles: [BoardRoles.READER] }], + id: anyBoardDo.id, + boardNode: anyBoardDo, + rootNode: columnBoardFactory.build(), + }); + + return { anyBoardDo, boardNodeAuthorizable, user }; + }; + + it('should call authorization service to getUserWithPermission', async () => { + const { anyBoardDo, user } = setup(); + await service.checkPermission(user.id, anyBoardDo, Action.write); + + expect(authorizationService.getUserWithPermissions).toBeCalledWith(user.id); + }); + + it('should call boardDNodeuthorizableService to getBoardAuthorizable', async () => { + const { anyBoardDo, user } = setup(); + await service.checkPermission(user.id, anyBoardDo, Action.write); + + expect(boardNodeAuthorizableService.getBoardAuthorizable).toBeCalledWith(anyBoardDo); + }); + + it('should call authorization service to checkPermission', async () => { + const { anyBoardDo, boardNodeAuthorizable, user } = setup(); + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + + boardNodeAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce(boardNodeAuthorizable); + + await service.checkPermission(user.id, anyBoardDo, Action.write); + + const permissionContext: AuthorizationContext = { + action: Action.write, + requiredPermissions: [], + }; + + expect(authorizationService.checkPermission).toBeCalledWith(user, boardNodeAuthorizable, permissionContext); + }); + }); + + describe('isUserBoardEditor', () => { + const setup = () => { + const user = userFactory.build(); + const anyBoardDo = columnBoardFactory.build(); + return { anyBoardDo, user }; + }; + it('should return true if user is board editor', () => { + const { anyBoardDo, user } = setup(); + const boardDoAuthorizable = new BoardNodeAuthorizable({ + users: [{ userId: user.id, roles: [BoardRoles.EDITOR] }], + id: anyBoardDo.id, + boardNode: anyBoardDo, + rootNode: anyBoardDo, + }); + const result = service.isUserBoardEditor(user.id, boardDoAuthorizable.users); + expect(result).toBe(true); + }); + + it('should return false if user is not board editor', () => { + const { anyBoardDo, user } = setup(); + const boardDoAuthorizable = new BoardNodeAuthorizable({ + users: [{ userId: user.id, roles: [BoardRoles.READER] }], + id: anyBoardDo.id, + boardNode: anyBoardDo, + rootNode: anyBoardDo, + }); + const result = service.isUserBoardEditor(user.id, boardDoAuthorizable.users); + expect(result).toBe(false); + }); + + it('should return false if user is not part of board', () => { + const { anyBoardDo, user } = setup(); + const boardDoAuthorizable = new BoardNodeAuthorizable({ + users: [], + id: anyBoardDo.id, + boardNode: anyBoardDo, + rootNode: anyBoardDo, + }); + const result = service.isUserBoardEditor(user.id, boardDoAuthorizable.users); + expect(result).toBe(false); + }); + }); + + describe('isUserBoardReader', () => { + const setup = () => { + const user = userFactory.build(); + return { user }; + }; + + it('should return false if user is board editor', () => { + const { user } = setup(); + const users: UserWithBoardRoles[] = [{ userId: user.id, roles: [BoardRoles.EDITOR] }]; + const result = service.isUserBoardReader(user.id, users); + expect(result).toBe(false); + }); + + it('should return false if user is both bord editor and reader', () => { + const { user } = setup(); + + const users: UserWithBoardRoles[] = [{ userId: user.id, roles: [BoardRoles.EDITOR, BoardRoles.READER] }]; + + const result = service.isUserBoardReader(user.id, users); + expect(result).toBe(false); + }); + + it('should return true if user is board reader', () => { + const { user } = setup(); + + const users: UserWithBoardRoles[] = [{ userId: user.id, roles: [BoardRoles.READER] }]; + + const result = service.isUserBoardReader(user.id, users); + expect(result).toBe(true); + }); + }); +}); diff --git a/apps/server/src/modules/board/service/board-node-permission.service.ts b/apps/server/src/modules/board/service/board-node-permission.service.ts new file mode 100644 index 00000000000..60d2670be42 --- /dev/null +++ b/apps/server/src/modules/board/service/board-node-permission.service.ts @@ -0,0 +1,46 @@ +import { Action, AuthorizationService } from '@modules/authorization'; +import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { Permission } from '@shared/domain/interface'; +import { EntityId } from '@shared/domain/types'; +import { AnyBoardNode, BoardRoles, UserWithBoardRoles } from '../domain'; +import { BoardNodeAuthorizableService } from './board-node-authorizable.service'; + +@Injectable() +export class BoardNodePermissionService { + constructor( + @Inject(forwardRef(() => AuthorizationService)) + private readonly authorizationService: AuthorizationService, + private readonly boardNodeAuthorizableService: BoardNodeAuthorizableService + ) {} + + async checkPermission(userId: EntityId, boardNode: AnyBoardNode, action: Action): Promise { + const requiredPermissions: Permission[] = []; + const user = await this.authorizationService.getUserWithPermissions(userId); + const boardNodeAuthorizable = await this.boardNodeAuthorizableService.getBoardAuthorizable(boardNode); + + this.authorizationService.checkPermission(user, boardNodeAuthorizable, { action, requiredPermissions }); + } + + isUserBoardEditor(userId: EntityId, userBoardRoles: UserWithBoardRoles[]): boolean { + const boardDoAuthorisedUser = userBoardRoles.find((user) => user.userId === userId); + + if (boardDoAuthorisedUser) { + return boardDoAuthorisedUser?.roles.includes(BoardRoles.EDITOR); + } + + return false; + } + + isUserBoardReader(userId: EntityId, userBoardRoles: UserWithBoardRoles[]): boolean { + const boardDoAuthorisedUser = userBoardRoles.find((user) => user.userId === userId); + + if (boardDoAuthorisedUser) { + return ( + boardDoAuthorisedUser.roles.includes(BoardRoles.READER) && + !boardDoAuthorisedUser.roles.includes(BoardRoles.EDITOR) + ); + } + + return false; + } +} diff --git a/apps/server/src/modules/board/service/board-node.service.spec.ts b/apps/server/src/modules/board/service/board-node.service.spec.ts new file mode 100644 index 00000000000..4564dcb699f --- /dev/null +++ b/apps/server/src/modules/board/service/board-node.service.spec.ts @@ -0,0 +1,150 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { setupEntities } from '@shared/testing'; +import { Card, ColumnBoard } from '../domain'; +import { BoardNodeRepo } from '../repo'; +import { columnBoardFactory, richTextElementFactory } from '../testing'; +import { BoardNodeService } from './board-node.service'; +import { BoardNodeDeleteHooksService, ContentElementUpdateService } from './internal'; + +describe(BoardNodeService.name, () => { + let module: TestingModule; + let service: BoardNodeService; + + let boardNodeRepo: DeepMocked; + // let contentElementUpdateService: DeepMocked; + // let boardNodeDeleteHooksService: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + BoardNodeService, + { + provide: BoardNodeRepo, + useValue: createMock(), + }, + { + provide: ContentElementUpdateService, + useValue: createMock(), + }, + { + provide: BoardNodeDeleteHooksService, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(BoardNodeService); + boardNodeRepo = module.get(BoardNodeRepo); + // contentElementUpdateService = module.get(ContentElementUpdateService); + // boardNodeDeleteHooksService = module.get(BoardNodeDeleteHooksService); + + await setupEntities(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('findById', () => { + const setup = () => { + const boardNode = columnBoardFactory.build(); + boardNodeRepo.findById.mockResolvedValueOnce(boardNode); + + return { boardNode }; + }; + + it('should use the repo', async () => { + const { boardNode } = setup(); + + await service.findById(boardNode.id, 2); + + expect(boardNodeRepo.findById).toHaveBeenCalledWith(boardNode.id, 2); + }); + + it('should return the repo result', async () => { + const { boardNode } = setup(); + + const result = await service.findById(boardNode.id, 2); + + expect(result).toBe(boardNode); + }); + }); + + describe('findByClassAndId', () => { + const setup = () => { + const boardNode = columnBoardFactory.build(); + boardNodeRepo.findById.mockResolvedValueOnce(boardNode); + + return { boardNode }; + }; + + it('should use the repo', async () => { + const { boardNode } = setup(); + + await service.findByClassAndId(ColumnBoard, boardNode.id); + + expect(boardNodeRepo.findById).toHaveBeenCalledWith(boardNode.id, undefined); + }); + + it('should return the repo result', async () => { + const { boardNode } = setup(); + + const result = await service.findByClassAndId(ColumnBoard, boardNode.id); + + expect(result).toBe(boardNode); + }); + + describe('when class doesnt match', () => { + it('should throw error', async () => { + const { boardNode } = setup(); + + await expect(service.findByClassAndId(Card, boardNode.id)).rejects.toThrowError(); + }); + }); + }); + + describe('findContentElementById', () => { + const setup = () => { + const element = richTextElementFactory.build(); + boardNodeRepo.findById.mockResolvedValueOnce(element); + + return { element }; + }; + + it('should use the repo', async () => { + const { element } = setup(); + + await service.findContentElementById(element.id); + + expect(boardNodeRepo.findById).toHaveBeenCalledWith(element.id, undefined); + }); + + it('should return the repo result', async () => { + const { element } = setup(); + + const result = await service.findContentElementById(element.id); + + expect(result).toBe(element); + }); + + describe('when node is not a content element', () => { + const setupNoneElement = () => { + const boardNode = columnBoardFactory.build(); + boardNodeRepo.findById.mockResolvedValueOnce(boardNode); + + return { boardNode }; + }; + + it('should throw error', async () => { + const { boardNode } = setupNoneElement(); + + await expect(service.findContentElementById(boardNode.id)).rejects.toThrowError(); + }); + }); + }); +}); diff --git a/apps/server/src/modules/board/service/board-node.service.ts b/apps/server/src/modules/board/service/board-node.service.ts new file mode 100644 index 00000000000..bd8092a2e78 --- /dev/null +++ b/apps/server/src/modules/board/service/board-node.service.ts @@ -0,0 +1,133 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { EntityId } from '@shared/domain/types'; +import { AnyElementContentBody } from '../controller/dto'; +import { AnyBoardNode, AnyContentElement, ColumnBoard, isContentElement, MediaBoard } from '../domain'; +import { BoardNodeRepo } from '../repo/board-node.repo'; +import { BoardNodeDeleteHooksService } from './internal/board-node-delete-hooks.service'; +import { ContentElementUpdateService } from './internal/content-element-update.service'; + +type WithTitle = Extract; +type WithVisibility = Extract; +type WithHeight = Extract; +type WithCompleted = Extract; + +@Injectable() +export class BoardNodeService { + constructor( + private readonly boardNodeRepo: BoardNodeRepo, + private readonly contentElementUpdateService: ContentElementUpdateService, + private readonly boardNodeDeleteHooksService: BoardNodeDeleteHooksService + ) {} + + async addRoot(boardNode: ColumnBoard | MediaBoard): Promise { + await this.boardNodeRepo.save(boardNode); + } + + async addToParent(parent: AnyBoardNode, child: AnyBoardNode, position?: number): Promise { + parent.addChild(child, position); + await this.boardNodeRepo.save(parent); + } + + async updateTitle>(node: T, title: T['title']) { + node.title = title; + await this.boardNodeRepo.save(node); + } + + async updateVisibility>(node: T, isVisible: T['isVisible']) { + node.isVisible = isVisible; + await this.boardNodeRepo.save(node); + } + + async updateHeight>(node: T, height: T['height']) { + node.height = height; + await this.boardNodeRepo.save(node); + } + + async updateCompleted>(node: T, completed: T['completed']) { + node.completed = completed; + await this.boardNodeRepo.save(node); + } + + async updateContent(element: AnyContentElement, content: AnyElementContentBody): Promise { + await this.contentElementUpdateService.updateContent(element, content); + } + + async move(child: AnyBoardNode, targetParent: AnyBoardNode, targetPosition?: number): Promise { + const saveList: AnyBoardNode[] = []; + + if (targetParent.hasChild(child)) { + targetParent.removeChild(child); + } else { + const sourceParent = await this.findParent(child); + if (sourceParent) { + sourceParent.removeChild(child); + saveList.concat(sourceParent.children); + } + } + targetParent.addChild(child, targetPosition); + saveList.concat(targetParent.children); + + await this.boardNodeRepo.save(saveList); + } + + async findById(id: EntityId, depth?: number): Promise { + const boardNode = this.boardNodeRepo.findById(id, depth); + + return boardNode; + } + + async findByClassAndId( + Constructor: { new (props: S): T }, + id: EntityId, + depth?: number + ): Promise { + const boardNode = await this.boardNodeRepo.findById(id, depth); + if (!(boardNode instanceof Constructor)) { + throw new NotFoundException(`There is no '${Constructor.name}' with this id`); + } + + return boardNode; + } + + async findByClassAndIds( + Constructor: { new (props: S): T }, + ids: EntityId[], + depth?: number + ): Promise { + const boardNodes = await this.boardNodeRepo.findByIds(ids, depth); + const filteredNodes = boardNodes.filter((node) => node instanceof Constructor); + + return filteredNodes as T[]; + } + + async findContentElementById(id: EntityId, depth?: number): Promise { + const boardNode = await this.boardNodeRepo.findById(id, depth); + + if (!isContentElement(boardNode)) { + throw new NotFoundException(`There is no content element with this id`); + } + + return boardNode; + } + + async findParent(child: AnyBoardNode, depth?: number): Promise { + const parentNode = child.parentId ? await this.boardNodeRepo.findById(child.parentId, depth) : undefined; + + return parentNode; + } + + async findRoot(child: AnyBoardNode, depth?: number): Promise { + const rootNode = await this.boardNodeRepo.findById(child.rootId, depth); + + return rootNode; + } + + async delete(boardNode: AnyBoardNode): Promise { + const parent = await this.findParent(boardNode); + if (parent) { + parent.removeChild(boardNode); + } + await this.boardNodeRepo.delete(boardNode); + await this.boardNodeDeleteHooksService.afterDelete(boardNode); + } +} diff --git a/apps/server/src/modules/board/service/card.service.spec.ts b/apps/server/src/modules/board/service/card.service.spec.ts deleted file mode 100644 index fb47aa65044..00000000000 --- a/apps/server/src/modules/board/service/card.service.spec.ts +++ /dev/null @@ -1,299 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { Card, CardInitProps, ContentElementType } from '@shared/domain/domainobject'; -import { setupEntities } from '@shared/testing'; -import { - cardFactory, - columnBoardFactory, - columnFactory, - richTextElementFactory, -} from '@shared/testing/factory/domainobject'; -import { BoardDoRepo } from '../repo'; -import { BoardDoService } from './board-do.service'; -import { CardService } from './card.service'; -import { ContentElementService } from './content-element.service'; - -describe(CardService.name, () => { - let module: TestingModule; - let service: CardService; - let boardDoRepo: DeepMocked; - let boardDoService: DeepMocked; - let contentElementService: DeepMocked; - - beforeAll(async () => { - module = await Test.createTestingModule({ - providers: [ - CardService, - { - provide: BoardDoRepo, - useValue: createMock(), - }, - { - provide: BoardDoService, - useValue: createMock(), - }, - { - provide: ContentElementService, - useValue: createMock(), - }, - ], - }).compile(); - - service = module.get(CardService); - boardDoRepo = module.get(BoardDoRepo); - boardDoService = module.get(BoardDoService); - contentElementService = module.get(ContentElementService); - - await setupEntities(); - }); - - afterAll(async () => { - await module.close(); - }); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('findById', () => { - describe('when finding one specific card', () => { - const setup = () => { - const card = cardFactory.build(); - return { card, cardId: card.id }; - }; - - it('should call the board do repository', async () => { - const { card, cardId } = setup(); - boardDoRepo.findByClassAndId.mockResolvedValueOnce(card); - - await service.findById(cardId); - - expect(boardDoRepo.findByClassAndId).toHaveBeenCalledWith(Card, cardId); - }); - - it('should return the domain objects from the board do repository', async () => { - const { card, cardId } = setup(); - boardDoRepo.findByClassAndId.mockResolvedValueOnce(card); - - const result = await service.findById(cardId); - - expect(result).toEqual(card); - }); - }); - }); - - describe('findByIds', () => { - describe('when finding many cards', () => { - const setup = () => { - const cards = cardFactory.buildList(3); - const cardIds = cards.map((c) => c.id); - - return { cards, cardIds }; - }; - - it('should call the card repository', async () => { - const { cards, cardIds } = setup(); - boardDoRepo.findByIds.mockResolvedValueOnce(cards); - - await service.findByIds(cardIds); - - expect(boardDoRepo.findByIds).toHaveBeenCalledWith(cardIds); - }); - - it('should return the domain objects from the card repository', async () => { - const { cards, cardIds } = setup(); - boardDoRepo.findByIds.mockResolvedValueOnce(cards); - - const result = await service.findByIds(cardIds); - - expect(result).toEqual(cards); - }); - - it('should throw an error if some DOs are not cards', async () => { - const richTextElements = richTextElementFactory.buildList(2); - const richTextElementIds = richTextElements.map((t) => t.id); - boardDoRepo.findByIds.mockResolvedValue(richTextElements); - - await expect(service.findByIds(richTextElementIds)).rejects.toThrow(); - }); - }); - }); - - describe('create', () => { - describe('when creating a card', () => { - const setup = () => { - const column = columnFactory.build(); - const columnId = column.id; - const createCardBodyParams = { - requiredEmptyElements: [ContentElementType.FILE, ContentElementType.RICH_TEXT], - }; - - return { column, columnId, createCardBodyParams }; - }; - - it('should save a list of cards using the boardDo repo', async () => { - const { column } = setup(); - - await service.create(column); - - expect(boardDoRepo.save).toHaveBeenCalledWith( - [ - expect.objectContaining({ - id: expect.any(String), - title: '', - createdAt: expect.any(Date), - updatedAt: expect.any(Date), - }), - ], - column - ); - }); - - it('contentElementService.create should be called with given parameters', async () => { - const { column, createCardBodyParams } = setup(); - - const { requiredEmptyElements } = createCardBodyParams; - - await service.create(column, requiredEmptyElements); - - expect(contentElementService.create).toHaveBeenCalledTimes(2); - expect(contentElementService.create).toHaveBeenNthCalledWith(1, expect.anything(), ContentElementType.FILE); - expect(contentElementService.create).toHaveBeenNthCalledWith( - 2, - expect.anything(), - ContentElementType.RICH_TEXT - ); - }); - }); - - describe('when creating a card with initial props', () => { - const setup = () => { - const column = columnFactory.build(); - const cardInitProps: CardInitProps = { - title: 'card #1', - height: 42, - }; - - return { column, cardInitProps }; - }; - - it('should set and save card with initial props', async () => { - const { column, cardInitProps } = setup(); - - await service.create(column, undefined, cardInitProps); - - expect(boardDoRepo.save).toHaveBeenCalledWith( - [ - expect.objectContaining({ - id: expect.any(String), - title: cardInitProps.title, - height: cardInitProps.height, - createdAt: expect.any(Date), - updatedAt: expect.any(Date), - }), - ], - column - ); - }); - }); - }); - - describe('createMany', () => { - describe('when creating many cards', () => { - const setup = () => { - const column = columnFactory.build(); - const cardInitProps = cardFactory.buildList(3); - - return { column, cardInitProps }; - }; - - it('should save a list of cards using the boardDo repo', async () => { - const { column, cardInitProps } = setup(); - - const result = await service.createMany(column, cardInitProps); - - expect(result).toHaveLength(3); - expect(boardDoRepo.save).toHaveBeenCalledTimes(1); - }); - }); - }); - - describe('delete', () => { - describe('when deleting a card', () => { - it('should call the service', async () => { - const card = cardFactory.build(); - - await service.delete(card); - - expect(boardDoService.deleteWithDescendants).toHaveBeenCalledWith(card); - }); - }); - }); - - describe('move', () => { - describe('when moving a card', () => { - it('should call the service', async () => { - const targetParent = columnFactory.build(); - const card = cardFactory.build(); - - await service.move(card, targetParent, 3); - - expect(boardDoService.move).toHaveBeenCalledWith(card, targetParent, 3); - }); - }); - }); - - describe('updateTitle', () => { - describe('when updating the title', () => { - it('should call the repo to save the updated card', async () => { - const card = cardFactory.build({ title: 'card #1' }); - const column = columnFactory.build({ children: [card] }); - const columnBoard = columnBoardFactory.build({ children: [column] }); - boardDoRepo.findParentOfId.mockResolvedValueOnce(columnBoard); - - const newTitle = 'new title'; - - await service.updateTitle(card, newTitle); - - expect(boardDoRepo.save).toHaveBeenCalledWith( - expect.objectContaining({ - id: expect.any(String), - title: newTitle, - height: expect.any(Number), - children: [], - createdAt: expect.any(Date), - updatedAt: expect.any(Date), - }), - columnBoard - ); - }); - }); - }); - - describe('setHeight', () => { - describe('when updating the height', () => { - it('should call the repo to save the updated card', async () => { - const card = cardFactory.build({ height: 10 }); - const column = columnFactory.build({ children: [card] }); - const columnBoard = columnBoardFactory.build({ children: [column] }); - boardDoRepo.findParentOfId.mockResolvedValueOnce(columnBoard); - - const newHeight = 42; - - await service.updateHeight(card, newHeight); - - expect(boardDoRepo.save).toHaveBeenCalledWith( - expect.objectContaining({ - id: expect.any(String), - title: expect.any(String), - height: newHeight, - children: [], - createdAt: expect.any(Date), - updatedAt: expect.any(Date), - }), - columnBoard - ); - }); - }); - }); -}); diff --git a/apps/server/src/modules/board/service/card.service.ts b/apps/server/src/modules/board/service/card.service.ts deleted file mode 100644 index 4b1ae92a115..00000000000 --- a/apps/server/src/modules/board/service/card.service.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { ObjectId } from '@mikro-orm/mongodb'; -import { Injectable, NotFoundException } from '@nestjs/common'; -import { Card, CardInitProps, Column, ContentElementType } from '@shared/domain/domainobject'; -import { EntityId } from '@shared/domain/types'; -import { BoardDoRepo } from '../repo'; -import { BoardDoService } from './board-do.service'; -import { ContentElementService } from './content-element.service'; - -@Injectable() -export class CardService { - constructor( - private readonly boardDoRepo: BoardDoRepo, - private readonly boardDoService: BoardDoService, - private readonly contentElementService: ContentElementService - ) {} - - async findById(cardId: EntityId): Promise { - return this.boardDoRepo.findByClassAndId(Card, cardId); - } - - async findByIds(cardIds: EntityId[]): Promise { - const cards = await this.boardDoRepo.findByIds(cardIds); - if (cards.some((card) => !(card instanceof Card))) { - throw new NotFoundException('some ids do not belong to a card'); - } - - return cards as Card[]; - } - - async create(parent: Column, requiredEmptyElements?: ContentElementType[], props?: CardInitProps): Promise { - const card = new Card({ - id: new ObjectId().toHexString(), - title: props?.title || '', - height: props?.height || 150, - children: [], - createdAt: new Date(), - updatedAt: new Date(), - }); - - parent.addChild(card); - - await this.boardDoRepo.save(parent.children, parent); - - if (requiredEmptyElements) { - await this.createEmptyElements(card, requiredEmptyElements); - } - - return card; - } - - async createMany(parent: Column, props: CardInitProps[]): Promise { - const cards = props.map((prop) => { - const card = new Card({ - id: new ObjectId().toHexString(), - title: prop.title, - height: prop.height, - children: [], - createdAt: new Date(), - updatedAt: new Date(), - }); - - parent.addChild(card); - - return card; - }); - - await this.boardDoRepo.save(parent.children, parent); - - return cards; - } - - async delete(card: Card): Promise { - await this.boardDoService.deleteWithDescendants(card); - } - - async move(card: Card, targetColumn: Column, targetPosition?: number): Promise { - await this.boardDoService.move(card, targetColumn, targetPosition); - } - - async updateHeight(card: Card, height: number): Promise { - const parent = await this.boardDoRepo.findParentOfId(card.id); - card.height = height; - await this.boardDoRepo.save(card, parent); - } - - async updateTitle(card: Card, title: string): Promise { - const parent = await this.boardDoRepo.findParentOfId(card.id); - card.title = title; - await this.boardDoRepo.save(card, parent); - } - - private async createEmptyElements(card: Card, requiredEmptyElements: ContentElementType[]): Promise { - for await (const requiredEmptyElement of requiredEmptyElements) { - await this.contentElementService.create(card, requiredEmptyElement); - } - } -} diff --git a/apps/server/src/modules/board/service/column-board-copy.service.spec.ts b/apps/server/src/modules/board/service/column-board-copy.service.spec.ts deleted file mode 100644 index e53df6797bc..00000000000 --- a/apps/server/src/modules/board/service/column-board-copy.service.spec.ts +++ /dev/null @@ -1,341 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; -import { UserService } from '@modules/user'; -import { Test, TestingModule } from '@nestjs/testing'; -import { BoardExternalReferenceType, ColumnBoard, UserDO } from '@shared/domain/domainobject'; -import { EntityId } from '@shared/domain/types'; -import { CourseRepo } from '@shared/repo'; -import { - cardFactory, - columnBoardFactory, - columnFactory, - courseFactory, - linkElementFactory, - schoolEntityFactory, - setupEntities, - userFactory, -} from '@shared/testing'; -import { ObjectId } from '@mikro-orm/mongodb'; -import { BoardDoRepo } from '../repo'; -import { - BoardDoCopyService, - SchoolSpecificFileCopyService, - SchoolSpecificFileCopyServiceFactory, -} from './board-do-copy-service'; -import { ColumnBoardCopyService } from './column-board-copy.service'; - -describe('column board copy service', () => { - let module: TestingModule; - let service: ColumnBoardCopyService; - let doCopyService: DeepMocked; - let boardRepo: DeepMocked; - let userService: DeepMocked; - let courseRepo: DeepMocked; - let fileCopyServiceFactory: DeepMocked; - let copyHelperService: DeepMocked; - - beforeAll(async () => { - module = await Test.createTestingModule({ - providers: [ - { - provide: BoardDoCopyService, - useValue: createMock(), - }, - { - provide: BoardDoRepo, - useValue: createMock(), - }, - { - provide: UserService, - useValue: createMock(), - }, - { - provide: CourseRepo, - useValue: createMock(), - }, - { - provide: SchoolSpecificFileCopyServiceFactory, - useValue: createMock(), - }, - { - provide: CopyHelperService, - useValue: createMock(), - }, - ColumnBoardCopyService, - ], - }).compile(); - - service = module.get(ColumnBoardCopyService); - doCopyService = module.get(BoardDoCopyService); - boardRepo = module.get(BoardDoRepo); - userService = module.get(UserService); - courseRepo = module.get(CourseRepo); - fileCopyServiceFactory = module.get(SchoolSpecificFileCopyServiceFactory); - copyHelperService = module.get(CopyHelperService); - - await setupEntities(); - }); - - beforeEach(() => { - jest.clearAllMocks(); - jest.resetAllMocks(); - }); - - describe('when copying a column board', () => { - const setup = () => { - const originalSchool = schoolEntityFactory.buildWithId(); - const targetSchool = schoolEntityFactory.buildWithId(); - const course = courseFactory.buildWithId({ school: originalSchool }); - const originalExternalReference = { - id: course.id, - type: BoardExternalReferenceType.Course, - }; - const originalBoard = columnBoardFactory.build({ - context: originalExternalReference, - }); - - const targetCourse = courseFactory.buildWithId(); - const destinationExternalReference = { - id: targetCourse.id, - type: BoardExternalReferenceType.Course, - }; - - const boardCopy = columnBoardFactory.build({ - context: originalExternalReference, - }); - - const expectedBoardCopy = { - ...boardCopy, - props: { ...boardCopy.getProps(), context: destinationExternalReference }, - }; - - const resultCopyStatus: CopyStatus = { - type: CopyElementType.COLUMNBOARD, - status: CopyStatusEnum.SUCCESS, - copyEntity: boardCopy, - }; - - const expectedCopyStatus = { - ...resultCopyStatus, - copyEntity: expectedBoardCopy, - }; - - const user = userFactory.buildWithId({ school: targetSchool }); - - const fileCopyServiceMock = createMock(); - fileCopyServiceFactory.build.mockReturnValueOnce(fileCopyServiceMock); - - boardRepo.findByClassAndId.mockResolvedValueOnce(originalBoard); - courseRepo.findById.mockResolvedValueOnce(course); - userService.findById.mockResolvedValueOnce({ schoolId: user.school.id } as UserDO); - doCopyService.copy.mockResolvedValueOnce(resultCopyStatus); - - const existingBoardIds = [new ObjectId().toHexString()]; - boardRepo.findIdsByExternalReference.mockResolvedValueOnce(existingBoardIds); - - const existingTitle = 'existingTitle'; - boardRepo.getTitlesByIds.mockResolvedValueOnce({ [existingBoardIds[0]]: existingTitle }); - - const derivedCopyTitle = 'derivedCopyTitle (1)'; - copyHelperService.deriveCopyName.mockReturnValueOnce(derivedCopyTitle); - - return { - course, - originalBoard, - destinationExternalReference, - user, - resultCopyStatus, - boardCopy, - expectedBoardCopy, - expectedCopyStatus, - fileCopyServiceMock, - existingBoardIds, - existingTitle, - derivedCopyTitle, - }; - }; - - it('should get column board do', async () => { - const { originalBoard, destinationExternalReference, user } = setup(); - await service.copyColumnBoard({ - originalColumnBoardId: originalBoard.id, - destinationExternalReference, - userId: user.id, - }); - - expect(boardRepo.findByClassAndId).toHaveBeenCalledWith(ColumnBoard, originalBoard.id); - }); - - it('should call copyService with column board do', async () => { - const { fileCopyServiceMock, originalBoard, destinationExternalReference, user } = setup(); - await service.copyColumnBoard({ - originalColumnBoardId: originalBoard.id, - destinationExternalReference, - userId: user.id, - copyTitle: 'newTitle', - }); - - expect(doCopyService.copy).toHaveBeenCalledWith({ - fileCopyService: fileCopyServiceMock, - original: originalBoard, - }); - }); - - it('should persist copy of board, with replaced externalReference', async () => { - const { originalBoard, destinationExternalReference, user, expectedBoardCopy } = setup(); - await service.copyColumnBoard({ - originalColumnBoardId: originalBoard.id, - destinationExternalReference, - userId: user.id, - copyTitle: 'newTitle', - }); - - expect(boardRepo.save).toHaveBeenCalledWith(expectedBoardCopy); - }); - - it('should return copyStatus', async () => { - const { originalBoard, destinationExternalReference, user, expectedCopyStatus } = setup(); - const result = await service.copyColumnBoard({ - originalColumnBoardId: originalBoard.id, - destinationExternalReference, - userId: user.id, - copyTitle: 'newTitle', - }); - - expect(result).toEqual(expectedCopyStatus); - }); - - describe('when copyTitle is not provided', () => { - it('should get board ids for reference', async () => { - const { originalBoard, destinationExternalReference, user } = setup(); - await service.copyColumnBoard({ - originalColumnBoardId: originalBoard.id, - destinationExternalReference, - userId: user.id, - }); - - expect(boardRepo.findIdsByExternalReference).toHaveBeenCalledWith(destinationExternalReference); - }); - - it('should get board titles for reference', async () => { - const { existingBoardIds, originalBoard, destinationExternalReference, user } = setup(); - - await service.copyColumnBoard({ - originalColumnBoardId: originalBoard.id, - destinationExternalReference, - userId: user.id, - }); - - expect(boardRepo.getTitlesByIds).toHaveBeenCalledWith([existingBoardIds[0]]); - }); - - it('should call helper to obtain copy name', async () => { - const { originalBoard, destinationExternalReference, user, existingTitle } = setup(); - const originalTitle = originalBoard.title; - await service.copyColumnBoard({ - originalColumnBoardId: originalBoard.id, - destinationExternalReference, - userId: user.id, - }); - - expect(copyHelperService.deriveCopyName).toHaveBeenCalledWith(originalTitle, [existingTitle]); - }); - - it('should call copyService with the derived title', async () => { - const { derivedCopyTitle, originalBoard, destinationExternalReference, user } = setup(); - - const copyBoard = originalBoard; - copyBoard.title = derivedCopyTitle; - - await service.copyColumnBoard({ - originalColumnBoardId: originalBoard.id, - destinationExternalReference, - userId: user.id, - }); - - expect(doCopyService.copy).toHaveBeenCalledWith(expect.objectContaining({ original: copyBoard })); - }); - }); - - describe('when copyTitle is provided', () => { - it('should not call deriveCopyName if copyTitle is provided', async () => { - const { originalBoard, destinationExternalReference, user } = setup(); - const copyTitle = 'copyTitle'; - - await service.copyColumnBoard({ - originalColumnBoardId: originalBoard.id, - destinationExternalReference, - userId: user.id, - copyTitle, - }); - expect(copyHelperService.deriveCopyName).not.toHaveBeenCalled(); - }); - it('should call copyService with given copyTitle', async () => { - const { fileCopyServiceMock, originalBoard, destinationExternalReference, user } = setup(); - const copyTitle = 'copyTitle'; - - await service.copyColumnBoard({ - originalColumnBoardId: originalBoard.id, - destinationExternalReference, - userId: user.id, - copyTitle, - }); - const copyBoard = originalBoard; - copyBoard.title = copyTitle; - expect(doCopyService.copy).toHaveBeenCalledWith({ - fileCopyService: fileCopyServiceMock, - original: copyBoard, - }); - }); - }); - }); - - describe('when changing linked ids', () => { - const setup = () => { - const linkedIdBefore = new ObjectId().toString(); - const linkElement = linkElementFactory.build({ - url: `someurl/${linkedIdBefore}`, - }); - const board = columnBoardFactory.build({ - children: [ - columnFactory.build({ - children: [ - cardFactory.build({ - children: [linkElement], - }), - ], - }), - ], - }); - boardRepo.findById.mockResolvedValue(board); - - return { board, linkElement, linkedIdBefore }; - }; - - it('should get board', async () => { - const { board } = setup(); - - await service.swapLinkedIds(board.id, new Map()); - - expect(boardRepo.findById).toHaveBeenCalledWith(board.id); - }); - - it('should update links in board', async () => { - const { board, linkElement, linkedIdBefore } = setup(); - const expectedId = new ObjectId().toString(); - const map = new Map().set(linkedIdBefore, expectedId); - - await service.swapLinkedIds(board.id, map); - - expect(linkElement.url).toEqual(`someurl/${expectedId}`); - }); - - it('should persist updates', async () => { - const { board } = setup(); - - await service.swapLinkedIds(board.id, new Map()); - - expect(boardRepo.save).toHaveBeenCalledWith(board); - }); - }); -}); diff --git a/apps/server/src/modules/board/service/column-board-copy.service.ts b/apps/server/src/modules/board/service/column-board-copy.service.ts deleted file mode 100644 index 540b65b0406..00000000000 --- a/apps/server/src/modules/board/service/column-board-copy.service.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { CopyHelperService, CopyStatus } from '@modules/copy-helper'; -import { UserService } from '@modules/user'; -import { Injectable, InternalServerErrorException, NotImplementedException } from '@nestjs/common'; -import { - BoardExternalReference, - BoardExternalReferenceType, - ColumnBoard, - isColumnBoard, -} from '@shared/domain/domainobject'; -import { EntityId } from '@shared/domain/types'; -import { CourseRepo } from '@shared/repo'; -import { BoardDoRepo } from '../repo'; -import { BoardDoCopyService, SchoolSpecificFileCopyServiceFactory } from './board-do-copy-service'; -import { SwapInternalLinksVisitor } from './board-do-copy-service/swap-internal-links.visitor'; - -@Injectable() -export class ColumnBoardCopyService { - constructor( - private readonly boardDoRepo: BoardDoRepo, - private readonly copyHelperService: CopyHelperService, - private readonly courseRepo: CourseRepo, - private readonly userService: UserService, - private readonly boardDoCopyService: BoardDoCopyService, - private readonly fileCopyServiceFactory: SchoolSpecificFileCopyServiceFactory - ) {} - - async copyColumnBoard(props: { - originalColumnBoardId: EntityId; - destinationExternalReference: BoardExternalReference; - userId: EntityId; - copyTitle?: string; - }): Promise { - const originalBoard: ColumnBoard = await this.boardDoRepo.findByClassAndId( - ColumnBoard, - props.originalColumnBoardId - ); - - if (props.copyTitle) { - originalBoard.title = props.copyTitle; - } else { - originalBoard.title = await this.deriveColumnBoardTitle(originalBoard.title, props.destinationExternalReference); - } - - const user = await this.userService.findById(props.userId); - /* istanbul ignore next */ - if (originalBoard.context.type !== BoardExternalReferenceType.Course) { - throw new NotImplementedException('only courses are supported as board parents'); - } - const course = await this.courseRepo.findById(originalBoard.context.id); // TODO: get rid of this - - const fileCopyService = this.fileCopyServiceFactory.build({ - sourceSchoolId: course.school.id, - targetSchoolId: user.schoolId, - userId: props.userId, - }); - - const copyStatus = await this.boardDoCopyService.copy({ original: originalBoard, fileCopyService }); - - /* istanbul ignore next */ - if (!isColumnBoard(copyStatus.copyEntity)) { - throw new InternalServerErrorException('expected copy of columnboard to be a columnboard'); - } - - copyStatus.copyEntity.context = props.destinationExternalReference; - await this.boardDoRepo.save(copyStatus.copyEntity); - - return copyStatus; - } - - private async deriveColumnBoardTitle( - originalTitle: string, - destinationExternalReference: BoardExternalReference - ): Promise { - const existingBoardIds = await this.boardDoRepo.findIdsByExternalReference(destinationExternalReference); - const existingTitles = await this.boardDoRepo.getTitlesByIds(existingBoardIds); - const copyName = this.copyHelperService.deriveCopyName(originalTitle, Object.values(existingTitles)); - return copyName; - } - - public async swapLinkedIds(boardId: EntityId, idMap: Map) { - const board = await this.boardDoRepo.findById(boardId); - - const visitor = new SwapInternalLinksVisitor(idMap); - - board.accept(visitor); - - await this.boardDoRepo.save(board); - - return board; - } -} diff --git a/apps/server/src/modules/board/service/column-board.service.spec.ts b/apps/server/src/modules/board/service/column-board.service.spec.ts index 353f053967a..a514242ea97 100644 --- a/apps/server/src/modules/board/service/column-board.service.spec.ts +++ b/apps/server/src/modules/board/service/column-board.service.spec.ts @@ -1,245 +1,130 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { Configuration } from '@hpi-schul-cloud/commons/lib'; -import { IConfig } from '@hpi-schul-cloud/commons/lib/interfaces/IConfig'; -import { ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; -import { - BoardExternalReference, - BoardExternalReferenceType, - BoardLayout, - ColumnBoard, - ContentElementFactory, -} from '@shared/domain/domainobject'; -import { columnBoardNodeFactory, setupEntities } from '@shared/testing'; -import { columnBoardFactory, columnFactory, richTextElementFactory } from '@shared/testing/factory/domainobject'; -import { BoardDoRepo } from '../repo'; -import { BoardDoService } from './board-do.service'; +import { EntityId } from '@shared/domain/types'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ColumnBoardService } from './column-board.service'; +import { BoardNodeRepo } from '../repo'; +import { BoardNodeService } from './board-node.service'; +import { ColumnBoardCopyService, ColumnBoardLinkService } from './internal'; +import { ColumnBoard, BoardExternalReference, BoardExternalReferenceType } from '../domain'; -describe(ColumnBoardService.name, () => { +import { columnBoardFactory } from '../testing'; +import { CopyElementType, CopyStatus, CopyStatusEnum } from '../../copy-helper'; + +describe('ColumnBoardService', () => { let module: TestingModule; let service: ColumnBoardService; - let boardDoRepo: DeepMocked; - let boardDoService: DeepMocked; - let configBefore: IConfig; + let repo: jest.Mocked; + let boardNodeService: jest.Mocked; + let columnBoardCopyService: DeepMocked; + let columnBoardLinkService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ providers: [ ColumnBoardService, { - provide: BoardDoRepo, - useValue: createMock(), + provide: BoardNodeRepo, + useValue: createMock(), }, { - provide: BoardDoService, - useValue: createMock(), + provide: BoardNodeService, + useValue: createMock(), }, { - provide: ContentElementFactory, - useValue: createMock(), + provide: ColumnBoardCopyService, + useValue: createMock(), + }, + { + provide: ColumnBoardLinkService, + useValue: createMock(), }, ], }).compile(); - service = module.get(ColumnBoardService); - boardDoRepo = module.get(BoardDoRepo); - boardDoService = module.get(BoardDoService); - configBefore = Configuration.toObject({ plainSecrets: true }); - await setupEntities(); - }); - - afterEach(() => { - jest.clearAllMocks(); - Configuration.reset(configBefore); + service = module.get(ColumnBoardService); + repo = module.get(BoardNodeRepo); + boardNodeService = module.get(BoardNodeService); + columnBoardCopyService = module.get(ColumnBoardCopyService); + columnBoardLinkService = module.get(ColumnBoardLinkService); }); afterAll(async () => { await module.close(); }); - const setup = () => { - const board = columnBoardFactory.build(); - const boardId = board.id; - const column = columnFactory.build(); - const courseId = new ObjectId().toHexString(); - const externalReference: BoardExternalReference = { - id: courseId, - type: BoardExternalReferenceType.Course, - }; - - return { board, boardId, column, courseId, externalReference }; - }; - - describe('findById', () => { - it('should call the board do repository', async () => { - const { boardId, board } = setup(); - boardDoRepo.findByClassAndId.mockResolvedValueOnce(board); - - await service.findById(boardId); - - expect(boardDoRepo.findByClassAndId).toHaveBeenCalledWith(ColumnBoard, boardId); - }); - - it('should return the columnBoard object of the given', async () => { - const { board } = setup(); - boardDoRepo.findByClassAndId.mockResolvedValueOnce(board); - - const result = await service.findById(board.id); - - expect(result).toEqual(board); - }); + beforeEach(() => { + jest.resetAllMocks(); }); - describe('findIdsByExternalReference', () => { - it('should call the board do repository', async () => { - const { boardId, externalReference } = setup(); - - boardDoRepo.findIdsByExternalReference.mockResolvedValue([boardId]); + it('should find ColumnBoard by id', async () => { + const columnBoard = columnBoardFactory.build(); + boardNodeService.findByClassAndId.mockResolvedValue(columnBoard); - await service.findIdsByExternalReference(externalReference); + const result = await service.findById('1'); - expect(boardDoRepo.findIdsByExternalReference).toHaveBeenCalledWith(externalReference); - }); + expect(result).toBe(columnBoard); + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(ColumnBoard, '1', undefined); }); - describe('findByDescendant', () => { - const setup2 = () => { - const element = richTextElementFactory.build(); - const board = columnBoardFactory.build({ children: [element] }); - boardDoService.getRootBoardDo.mockResolvedValue(board); - return { - element, - board, - }; + it('should find ColumnBoards by external reference', async () => { + const columnBoard = columnBoardFactory.build(); + repo.findByExternalReference.mockResolvedValueOnce([columnBoard]); + const reference: BoardExternalReference = { + type: BoardExternalReferenceType.Course, + id: '1', }; - it('should call board-do service to get the rootDo', async () => { - const { element } = setup2(); - - await service.findByDescendant(element); - - expect(boardDoService.getRootBoardDo).toHaveBeenCalledWith(element); - }); - - it('should return the root boardDo', async () => { - const { element, board } = setup2(); + const result = await service.findByExternalReference(reference); - const result = await service.findByDescendant(element); - - expect(result).toEqual(board); - }); + expect(result).toEqual([columnBoard]); + expect(repo.findByExternalReference).toHaveBeenCalledWith(reference, undefined); }); - describe('getBoardObjectTitlesById', () => { - describe('when asking for a list of boardObject-ids', () => { - const setupBoards = () => { - return { - boardNodes: columnBoardNodeFactory.buildListWithId(3), - }; - }; - - it('should call the boardDoRepo.getTitleById with the same parameters', async () => { - const { boardNodes } = setupBoards(); - const ids = boardNodes.map((n) => n.id); + it('should update ColumnBoard visibility', async () => { + const columnBoard = columnBoardFactory.build(); - await service.getBoardObjectTitlesById(ids); + await service.updateVisibility(columnBoard, true); - expect(boardDoRepo.getTitlesByIds).toHaveBeenCalledWith(ids); - }); - }); + expect(boardNodeService.updateVisibility).toHaveBeenCalledWith(columnBoard, true); }); - describe('create', () => { - const setupBoards = () => { - const context: BoardExternalReference = { - type: BoardExternalReferenceType.Course, - id: new ObjectId().toHexString(), - }; - - return { context }; + it('should delete ColumnBoards by course id', async () => { + const columnBoard = columnBoardFactory.build(); + repo.findByExternalReference.mockResolvedValueOnce([columnBoard]); + const reference: BoardExternalReference = { + type: BoardExternalReferenceType.Course, + id: '1', }; - describe('when creating a fresh column board', () => { - it('should return a columnBoardInfo of that board', async () => { - const { context } = setupBoards(); - const title = `My brand new Mainboard`; - - const columnBoardInfo = await service.create(context, BoardLayout.COLUMNS, title); + await service.deleteByCourseId('1'); - expect(columnBoardInfo).toEqual(expect.objectContaining({ title })); - }); - }); + expect(repo.findByExternalReference).toHaveBeenCalledWith(reference, undefined); + expect(repo.delete).toHaveBeenCalledWith([columnBoard]); }); - describe('delete', () => { - it('should call the service to delete the board', async () => { - const { board } = setup(); - - await service.delete(board); - - expect(boardDoService.deleteWithDescendants).toHaveBeenCalledWith(board); + it('should copy ColumnBoard', async () => { + const copyStatus: CopyStatus = { status: CopyStatusEnum.SUCCESS, type: CopyElementType.COLUMNBOARD }; + columnBoardCopyService.copyColumnBoard.mockResolvedValueOnce(copyStatus); + const result = await service.copyColumnBoard({ + originalColumnBoardId: '1', + destinationExternalReference: { + type: BoardExternalReferenceType.Course, + id: '1', + }, + userId: '1', }); - }); - - describe('deleteByCourseId', () => { - describe('when deleting by courseId', () => { - it('should call boardDoRepo.findIdsByExternalReference to find the board ids', async () => { - const { boardId, courseId, externalReference } = setup(); - - boardDoRepo.findIdsByExternalReference.mockResolvedValue([boardId]); - - await service.deleteByCourseId(courseId); - - expect(boardDoRepo.findIdsByExternalReference).toHaveBeenCalledWith(externalReference); - }); - - it('should call boardDoService.deleteWithDescendants to delete the board', async () => { - const { board, courseId } = setup(); - await service.deleteByCourseId(courseId); - - expect(boardDoService.deleteWithDescendants).toHaveBeenCalledWith(board); - }); - }); + expect(result).toEqual(copyStatus); }); - describe('updateTitle', () => { - describe('when updating the title', () => { - it('should call the service', async () => { - const board = columnBoardFactory.build(); - const newTitle = 'new title'; - - await service.updateTitle(board, newTitle); - - expect(boardDoRepo.save).toHaveBeenCalledWith( - expect.objectContaining({ - id: expect.any(String), - title: newTitle, - children: [], - createdAt: expect.any(Date), - updatedAt: expect.any(Date), - }) - ); - }); - }); - }); + it('should swap Linked Ids', async () => { + const idMap = new Map(); + idMap.set('1', '2'); + const columnBoard = columnBoardFactory.build(); + columnBoardLinkService.swapLinkedIds.mockResolvedValueOnce(columnBoard); - describe('updateBoardVisibility', () => { - it('should call the boardDoRepo.save with the updated board', async () => { - const board = columnBoardFactory.build(); - const isVisible = true; - - await service.updateBoardVisibility(board, isVisible); - - expect(boardDoRepo.save).toHaveBeenCalledWith( - expect.objectContaining({ - id: board.id, - isVisible, - createdAt: expect.any(Date), - updatedAt: expect.any(Date), - }) - ); - }); + const result = await service.swapLinkedIds('1', idMap); + + expect(result).toEqual(columnBoard); }); }); diff --git a/apps/server/src/modules/board/service/column-board.service.ts b/apps/server/src/modules/board/service/column-board.service.ts index 755373050c9..7230ff3cb8c 100644 --- a/apps/server/src/modules/board/service/column-board.service.ts +++ b/apps/server/src/modules/board/service/column-board.service.ts @@ -1,91 +1,64 @@ -import { ObjectId } from '@mikro-orm/mongodb'; import { Injectable } from '@nestjs/common'; -import { - AnyBoardDo, - BoardExternalReference, - BoardExternalReferenceType, - BoardLayout, - ColumnBoard, - MediaBoard, -} from '@shared/domain/domainobject'; import { EntityId } from '@shared/domain/types'; -import { BoardDoRepo } from '../repo'; -import { BoardDoService } from './board-do.service'; +import { CopyStatus } from '@modules/copy-helper'; +import { BoardExternalReference, BoardExternalReferenceType, ColumnBoard, isColumnBoard } from '../domain'; +import { BoardNodeRepo } from '../repo'; +import { BoardNodeService } from './board-node.service'; +import { ColumnBoardCopyService } from './internal/column-board-copy.service'; +import { ColumnBoardLinkService } from './internal/column-board-link.service'; @Injectable() export class ColumnBoardService { - constructor(private readonly boardDoRepo: BoardDoRepo, private readonly boardDoService: BoardDoService) {} + constructor( + private readonly boardNodeRepo: BoardNodeRepo, + private readonly boardNodeService: BoardNodeService, + private readonly columnBoardCopyService: ColumnBoardCopyService, + private readonly clumnBoardLinkService: ColumnBoardLinkService + ) {} - async findById(boardId: EntityId): Promise { - const board = await this.boardDoRepo.findByClassAndId(ColumnBoard, boardId); + async findById(id: EntityId, depth?: number): Promise { + const columnBoard = this.boardNodeService.findByClassAndId(ColumnBoard, id, depth); - return board; - } - - async findIdsByExternalReference(reference: BoardExternalReference): Promise { - const ids = this.boardDoRepo.findIdsByExternalReference(reference); - - return ids; - } - - async findByDescendant(boardDo: AnyBoardDo): Promise { - const rootboardDo = this.boardDoService.getRootBoardDo(boardDo); - - return rootboardDo; - } - - async getBoardObjectTitlesById(boardIds: EntityId[]): Promise> { - const titleMap = this.boardDoRepo.getTitlesByIds(boardIds); - return titleMap; + return columnBoard; } - async create(context: BoardExternalReference, layout: BoardLayout, title: string): Promise { - const columnBoard = new ColumnBoard({ - id: new ObjectId().toHexString(), - title, - children: [], - createdAt: new Date(), - updatedAt: new Date(), - context, - isVisible: false, - layout, - }); + async findByExternalReference(reference: BoardExternalReference, depth?: number): Promise { + const boardNodes = await this.boardNodeRepo.findByExternalReference(reference, depth); - await this.boardDoRepo.save(columnBoard); + const boards = boardNodes.filter((bn) => isColumnBoard(bn)); - return columnBoard; + return boards as ColumnBoard[]; } - async delete(board: ColumnBoard): Promise { - await this.boardDoService.deleteWithDescendants(board); + async updateVisibility(columbBoard: ColumnBoard, visibility: boolean): Promise { + await this.boardNodeService.updateVisibility(columbBoard, visibility); } + // called from feathers + // TODO remove when not needed anymore async deleteByCourseId(courseId: EntityId): Promise { - const columnBoardsId = await this.findIdsByExternalReference({ + const boardNodes = await this.findByExternalReference({ type: BoardExternalReferenceType.Course, id: courseId, }); - const deletePromises = columnBoardsId.map((columnBoardId) => this.deleteColumnBoardById(columnBoardId)); - - await Promise.all(deletePromises); + await this.boardNodeRepo.delete(boardNodes); } - private async deleteColumnBoardById(id: EntityId): Promise { - const columnBoardToDeletion = await this.boardDoRepo.findByClassAndId(ColumnBoard, id); + async copyColumnBoard(props: { + originalColumnBoardId: EntityId; + destinationExternalReference: BoardExternalReference; + userId: EntityId; + copyTitle?: string; + }): Promise { + const copyStatus = await this.columnBoardCopyService.copyColumnBoard(props); - if (columnBoardToDeletion) { - await this.boardDoService.deleteWithDescendants(columnBoardToDeletion); - } + return copyStatus; } - async updateTitle(board: ColumnBoard, title: string): Promise { - board.title = title; - await this.boardDoRepo.save(board); - } + async swapLinkedIds(boardId: EntityId, idMap: Map): Promise { + const board = await this.clumnBoardLinkService.swapLinkedIds(boardId, idMap); - async updateBoardVisibility(board: ColumnBoard, isVisible: boolean): Promise { - board.isVisible = isVisible; - await this.boardDoRepo.save(board); + return board; } } diff --git a/apps/server/src/modules/board/service/column.service.spec.ts b/apps/server/src/modules/board/service/column.service.spec.ts deleted file mode 100644 index 2411e6650ad..00000000000 --- a/apps/server/src/modules/board/service/column.service.spec.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { Column } from '@shared/domain/domainobject'; -import { setupEntities } from '@shared/testing'; -import { columnBoardFactory, columnFactory } from '@shared/testing/factory/domainobject'; -import { BoardDoRepo } from '../repo'; -import { BoardDoService } from './board-do.service'; -import { ColumnService } from './column.service'; - -describe(ColumnService.name, () => { - let module: TestingModule; - let service: ColumnService; - let boardDoRepo: DeepMocked; - let boardDoService: DeepMocked; - - beforeAll(async () => { - module = await Test.createTestingModule({ - providers: [ - ColumnService, - { - provide: BoardDoRepo, - useValue: createMock(), - }, - { - provide: BoardDoService, - useValue: createMock(), - }, - ], - }).compile(); - - service = module.get(ColumnService); - boardDoRepo = module.get(BoardDoRepo); - boardDoService = module.get(BoardDoService); - await setupEntities(); - }); - - afterAll(async () => { - await module.close(); - }); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('findById', () => { - describe('when finding a column', () => { - const setup = () => { - const column = columnFactory.build(); - return { column, columnId: column.id }; - }; - - it('should call the repository', async () => { - const { column, columnId } = setup(); - boardDoRepo.findByClassAndId.mockResolvedValueOnce(column); - - await service.findById(columnId); - - expect(boardDoRepo.findByClassAndId).toHaveBeenCalledWith(Column, columnId); - }); - - it('should return the column', async () => { - const { column, columnId } = setup(); - boardDoRepo.findByClassAndId.mockResolvedValueOnce(column); - - const result = await service.findById(columnId); - - expect(result).toEqual(column); - }); - }); - }); - - describe('create', () => { - describe('when creating a column', () => { - const setup = () => { - const board = columnBoardFactory.build(); - const boardId = board.id; - - return { board, boardId }; - }; - - it('should save a list of columns using the repo', async () => { - const { board } = setup(); - - await service.create(board); - - expect(boardDoRepo.save).toHaveBeenCalledWith( - [ - expect.objectContaining({ - id: expect.any(String), - title: '', - children: [], - createdAt: expect.any(Date), - updatedAt: expect.any(Date), - }), - ], - board - ); - }); - }); - }); - - describe('createMany', () => { - describe('when creating multiple columns', () => { - const setup = () => { - const board = columnBoardFactory.build(); - const props = [{ title: 'title-1' }, { title: 'title-2' }]; - - return { board, props }; - }; - - it('should save a list of columns using the repo in a batch', async () => { - const { board, props } = setup(); - - await service.createMany(board, props); - - expect(boardDoRepo.save).toHaveBeenCalledTimes(1); - expect(boardDoRepo.save).toHaveBeenCalledWith( - [ - expect.objectContaining({ - id: expect.any(String), - title: 'title-1', - children: [], - createdAt: expect.any(Date), - updatedAt: expect.any(Date), - }), - expect.objectContaining({ - id: expect.any(String), - title: 'title-2', - children: [], - createdAt: expect.any(Date), - updatedAt: expect.any(Date), - }), - ], - board - ); - }); - }); - }); - - describe('delete', () => { - describe('when deleting a column', () => { - it('should call the service', async () => { - const column = columnFactory.build(); - - await service.delete(column); - - expect(boardDoService.deleteWithDescendants).toHaveBeenCalledWith(column); - }); - }); - }); - - describe('move', () => { - describe('when moving a column', () => { - it('should call the service', async () => { - const board = columnBoardFactory.build(); - const column = columnFactory.build(); - - await service.move(column, board, 3); - - expect(boardDoService.move).toHaveBeenCalledWith(column, board, 3); - }); - }); - }); - - describe('updateTitle', () => { - describe('when updating the title', () => { - it('should call the service', async () => { - const column = columnFactory.build(); - const columnBoard = columnBoardFactory.build({ children: [column] }); - boardDoRepo.findParentOfId.mockResolvedValueOnce(columnBoard); - - const newTitle = 'new title'; - - await service.updateTitle(column, newTitle); - - expect(boardDoRepo.save).toHaveBeenCalledWith( - expect.objectContaining({ - id: expect.any(String), - title: newTitle, - children: [], - createdAt: expect.any(Date), - updatedAt: expect.any(Date), - }), - columnBoard - ); - }); - }); - }); -}); diff --git a/apps/server/src/modules/board/service/column.service.ts b/apps/server/src/modules/board/service/column.service.ts deleted file mode 100644 index 1b452012609..00000000000 --- a/apps/server/src/modules/board/service/column.service.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { ObjectId } from '@mikro-orm/mongodb'; -import { Injectable } from '@nestjs/common'; -import { Column, ColumnBoard, ColumnInitProps } from '@shared/domain/domainobject'; -import { EntityId } from '@shared/domain/types'; -import { BoardDoRepo } from '../repo'; -import { BoardDoService } from './board-do.service'; - -@Injectable() -export class ColumnService { - constructor(private readonly boardDoRepo: BoardDoRepo, private readonly boardDoService: BoardDoService) {} - - async findById(columnId: EntityId): Promise { - const column = await this.boardDoRepo.findByClassAndId(Column, columnId); - return column; - } - - async create(parent: ColumnBoard, props?: ColumnInitProps): Promise { - const column = new Column({ - id: new ObjectId().toHexString(), - title: props?.title || '', - children: [], - createdAt: new Date(), - updatedAt: new Date(), - }); - - parent.addChild(column); - - await this.boardDoRepo.save(parent.children, parent); - - return column; - } - - async createMany(parent: ColumnBoard, props: ColumnInitProps[]): Promise { - const columns = props.map((prop) => { - const column = new Column({ - id: new ObjectId().toHexString(), - title: prop.title, - children: [], - createdAt: new Date(), - updatedAt: new Date(), - }); - - parent.addChild(column); - - return column; - }); - - await this.boardDoRepo.save(parent.children, parent); - - return columns; - } - - async delete(column: Column): Promise { - await this.boardDoService.deleteWithDescendants(column); - } - - async move(column: Column, targetBoard: ColumnBoard, targetPosition?: number): Promise { - await this.boardDoService.move(column, targetBoard, targetPosition); - } - - async updateTitle(column: Column, title: string): Promise { - const parent = await this.boardDoRepo.findParentOfId(column.id); - column.title = title; - await this.boardDoRepo.save(column, parent); - } -} diff --git a/apps/server/src/modules/board/service/content-element-update.visitor.spec.ts b/apps/server/src/modules/board/service/content-element-update.visitor.spec.ts deleted file mode 100644 index bac4a981971..00000000000 --- a/apps/server/src/modules/board/service/content-element-update.visitor.spec.ts +++ /dev/null @@ -1,291 +0,0 @@ -import { ObjectId } from '@mikro-orm/mongodb'; -import { InputFormat } from '@shared/domain/types'; -import { - cardFactory, - collaborativeTextEditorElementFactory, - columnBoardFactory, - columnFactory, - drawingElementFactory, - externalToolElementFactory, - fileElementFactory, - linkElementFactory, - mediaBoardFactory, - mediaExternalToolElementFactory, - mediaLineFactory, - richTextElementFactory, - submissionContainerElementFactory, - submissionItemFactory, -} from '@shared/testing'; -import { ExternalToolContentBody, FileContentBody, LinkContentBody, RichTextContentBody } from '../controller/dto'; -import { ContentElementUpdateVisitor } from './content-element-update.visitor'; - -describe(ContentElementUpdateVisitor.name, () => { - describe('when visiting an unsupported component', () => { - const setup = () => { - const board = columnBoardFactory.build(); - const column = columnFactory.build(); - const card = cardFactory.build(); - const content = new RichTextContentBody(); - content.text = 'a text'; - content.inputFormat = InputFormat.RICH_TEXT_CK5; - const submissionItem = submissionItemFactory.build(); - const collaborativeTextEditorElement = collaborativeTextEditorElementFactory.build(); - const updater = new ContentElementUpdateVisitor(content); - - return { board, column, card, submissionItem, collaborativeTextEditorElement, updater }; - }; - - describe('when component is a column board', () => { - it('should throw an error', async () => { - const { board, updater } = setup(); - await expect(updater.visitColumnBoardAsync(board)).rejects.toThrow(); - }); - }); - - describe('when component is a column', () => { - it('should throw an error', async () => { - const { column, updater } = setup(); - await expect(() => updater.visitColumnAsync(column)).rejects.toThrow(); - }); - }); - - describe('when component is a card', () => { - it('should throw an error', async () => { - const { card, updater } = setup(); - await expect(() => updater.visitCardAsync(card)).rejects.toThrow(); - }); - }); - - describe('when component is a submission-item', () => { - it('should throw an error', async () => { - const { submissionItem, updater } = setup(); - await expect(() => updater.visitSubmissionItemAsync(submissionItem)).rejects.toThrow(); - }); - }); - - describe('when component is a collaborative text editor element', () => { - it('should throw an error', async () => { - const { updater, collaborativeTextEditorElement } = setup(); - await expect(() => - updater.visitCollaborativeTextEditorElementAsync(collaborativeTextEditorElement) - ).rejects.toThrow(); - }); - }); - - describe('when component is a media board', () => { - it('should throw an error', async () => { - const { updater } = setup(); - const board = mediaBoardFactory.build(); - - await expect(updater.visitMediaBoardAsync(board)).rejects.toThrow(); - }); - }); - - describe('when component is a media line', () => { - it('should throw an error', async () => { - const { updater } = setup(); - const line = mediaLineFactory.build(); - - await expect(() => updater.visitMediaLineAsync(line)).rejects.toThrow(); - }); - }); - - describe('when component is a media external tool element', () => { - it('should throw an error', async () => { - const { updater } = setup(); - - const element = mediaExternalToolElementFactory.build(); - - await expect(() => updater.visitMediaExternalToolElementAsync(element)).rejects.toThrow(); - }); - }); - }); - - describe('when visiting a file element using the wrong content', () => { - const setup = () => { - const fileElement = fileElementFactory.build(); - const content = new RichTextContentBody(); - content.text = 'a text'; - content.inputFormat = InputFormat.RICH_TEXT_CK5; - const updater = new ContentElementUpdateVisitor(content); - - return { fileElement, updater }; - }; - - it('should throw an error', async () => { - const { fileElement, updater } = setup(); - - await expect(() => updater.visitFileElementAsync(fileElement)).rejects.toThrow(); - }); - }); - - describe('when visiting a rich text element using the wrong content', () => { - const setup = () => { - const richTextElement = richTextElementFactory.build(); - const content = new FileContentBody(); - content.caption = 'a caption'; - const updater = new ContentElementUpdateVisitor(content); - - return { richTextElement, updater }; - }; - - it('should throw an error', async () => { - const { richTextElement, updater } = setup(); - - await expect(() => updater.visitRichTextElementAsync(richTextElement)).rejects.toThrow(); - }); - }); - - describe('when visiting a drawing element using the wrong content', () => { - const setup = () => { - const drawingElement = drawingElementFactory.build(); - const content = new FileContentBody(); - content.caption = 'a caption'; - const updater = new ContentElementUpdateVisitor(content); - - return { drawingElement, updater }; - }; - - it('should throw an error', async () => { - const { drawingElement, updater } = setup(); - - await expect(() => updater.visitDrawingElementAsync(drawingElement)).rejects.toThrow(); - }); - }); - - describe('when visiting a submission container element using the wrong content', () => { - const setup = () => { - const submissionContainerElement = submissionContainerElementFactory.build(); - const content = new RichTextContentBody(); - content.text = 'a text'; - content.inputFormat = InputFormat.RICH_TEXT_CK5; - const updater = new ContentElementUpdateVisitor(content); - - return { submissionContainerElement, updater }; - }; - - it('should throw an error', async () => { - const { submissionContainerElement, updater } = setup(); - - await expect(() => updater.visitSubmissionContainerElementAsync(submissionContainerElement)).rejects.toThrow(); - }); - }); - - describe('when visiting a link element', () => { - describe('when content is valid', () => { - const setup = () => { - const linkElement = linkElementFactory.build(); - const content = new LinkContentBody(); - content.url = 'https://super-example.com/'; - content.title = 'SuperExample - the best examples in the web'; - content.imageUrl = '/preview/image.jpg'; - const updater = new ContentElementUpdateVisitor(content); - - return { linkElement, content, updater }; - }; - - it('should update the content', async () => { - const { linkElement, content, updater } = setup(); - - await updater.visitLinkElementAsync(linkElement); - - expect(linkElement.url).toEqual(content.url); - expect(linkElement.title).toEqual(content.title); - expect(linkElement.imageUrl).toEqual(content.imageUrl); - }); - }); - - describe('when content is not a link element', () => { - const setup = () => { - const linkElement = linkElementFactory.build(); - const content = new FileContentBody(); - content.caption = 'a caption'; - const updater = new ContentElementUpdateVisitor(content); - - return { linkElement, updater }; - }; - - it('should throw an error', async () => { - const { linkElement, updater } = setup(); - - await expect(() => updater.visitLinkElementAsync(linkElement)).rejects.toThrow(); - }); - }); - - describe('when imageUrl for preview image is not a relative url', () => { - const setup = () => { - const linkElement = linkElementFactory.build(); - const content = new LinkContentBody(); - content.url = 'https://super-example.com/'; - content.title = 'SuperExample - the best examples in the web'; - content.imageUrl = 'https://www.external.de/fake-preview-image.jpg'; - const updater = new ContentElementUpdateVisitor(content); - - return { linkElement, content, updater }; - }; - - it('should ignore the image url', async () => { - const { linkElement, updater } = setup(); - - await updater.visitLinkElementAsync(linkElement); - - expect(linkElement.imageUrl).toBe(''); - }); - }); - }); - - describe('when visiting a external tool element', () => { - describe('when visiting a external tool element with valid content', () => { - const setup = () => { - const externalToolElement = externalToolElementFactory.build({ contextExternalToolId: undefined }); - const content = new ExternalToolContentBody(); - content.contextExternalToolId = new ObjectId().toHexString(); - const updater = new ContentElementUpdateVisitor(content); - - return { externalToolElement, updater, content }; - }; - - it('should update the content', async () => { - const { externalToolElement, updater, content } = setup(); - - await updater.visitExternalToolElementAsync(externalToolElement); - - expect(externalToolElement.contextExternalToolId).toEqual(content.contextExternalToolId); - }); - }); - - describe('when visiting a external tool element using the wrong content', () => { - const setup = () => { - const externalToolElement = externalToolElementFactory.build(); - const content = new RichTextContentBody(); - content.text = 'a text'; - content.inputFormat = InputFormat.RICH_TEXT_CK5; - const updater = new ContentElementUpdateVisitor(content); - - return { externalToolElement, updater }; - }; - - it('should throw an error', async () => { - const { externalToolElement, updater } = setup(); - - await expect(() => updater.visitExternalToolElementAsync(externalToolElement)).rejects.toThrow(); - }); - }); - - describe('when visiting a external tool element without setting a contextExternalId', () => { - const setup = () => { - const externalToolElement = externalToolElementFactory.build(); - const content = new ExternalToolContentBody(); - const updater = new ContentElementUpdateVisitor(content); - - return { externalToolElement, updater }; - }; - - it('should throw an error', async () => { - const { externalToolElement, updater } = setup(); - - await expect(() => updater.visitExternalToolElementAsync(externalToolElement)).rejects.toThrow(); - }); - }); - }); -}); diff --git a/apps/server/src/modules/board/service/content-element-update.visitor.ts b/apps/server/src/modules/board/service/content-element-update.visitor.ts deleted file mode 100644 index fe1b979e2cd..00000000000 --- a/apps/server/src/modules/board/service/content-element-update.visitor.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { sanitizeRichText } from '@shared/controller'; -import type { - AnyBoardDo, - BoardCompositeVisitorAsync, - Card, - Column, - ColumnBoard, - ExternalToolElement, - FileElement, - MediaBoard, - MediaExternalToolElement, - MediaLine, - RichTextElement, - SubmissionContainerElement, - SubmissionItem, -} from '@shared/domain/domainobject'; -import { CollaborativeTextEditorElement } from '@shared/domain/domainobject/board/collaborative-text-editor-element.do'; -import { DrawingElement } from '@shared/domain/domainobject/board/drawing-element.do'; -import { LinkElement } from '@shared/domain/domainobject/board/link-element.do'; -import { InputFormat } from '@shared/domain/types'; -import { - AnyElementContentBody, - DrawingContentBody, - ExternalToolContentBody, - FileContentBody, - LinkContentBody, - RichTextContentBody, - SubmissionContainerContentBody, -} from '../controller/dto'; - -@Injectable() -export class ContentElementUpdateVisitor implements BoardCompositeVisitorAsync { - private readonly content: AnyElementContentBody; - - constructor(content: AnyElementContentBody) { - this.content = content; - } - - async visitColumnBoardAsync(columnBoard: ColumnBoard): Promise { - return this.rejectNotHandled(columnBoard); - } - - async visitColumnAsync(column: Column): Promise { - return this.rejectNotHandled(column); - } - - async visitCardAsync(card: Card): Promise { - return this.rejectNotHandled(card); - } - - async visitFileElementAsync(fileElement: FileElement): Promise { - if (this.content instanceof FileContentBody) { - fileElement.caption = sanitizeRichText(this.content.caption, InputFormat.PLAIN_TEXT); - fileElement.alternativeText = sanitizeRichText(this.content.alternativeText, InputFormat.PLAIN_TEXT); - return Promise.resolve(); - } - return this.rejectNotHandled(fileElement); - } - - async visitLinkElementAsync(linkElement: LinkElement): Promise { - if (this.content instanceof LinkContentBody) { - linkElement.url = new URL(this.content.url).toString(); - linkElement.title = this.content.title ?? ''; - linkElement.description = this.content.description ?? ''; - if (this.content.imageUrl) { - const isRelativeUrl = (url: string) => { - const fallbackHostname = 'https://www.fallback-url-if-url-is-relative.org'; - const imageUrlObject = new URL(url, fallbackHostname); - return imageUrlObject.origin === fallbackHostname; - }; - - if (isRelativeUrl(this.content.imageUrl)) { - linkElement.imageUrl = this.content.imageUrl; - } - } - return Promise.resolve(); - } - return this.rejectNotHandled(linkElement); - } - - async visitRichTextElementAsync(richTextElement: RichTextElement): Promise { - if (this.content instanceof RichTextContentBody) { - richTextElement.text = sanitizeRichText(this.content.text, this.content.inputFormat); - richTextElement.inputFormat = this.content.inputFormat; - return Promise.resolve(); - } - return this.rejectNotHandled(richTextElement); - } - - async visitDrawingElementAsync(drawingElement: DrawingElement): Promise { - if (this.content instanceof DrawingContentBody) { - drawingElement.description = this.content.description; - return Promise.resolve(); - } - return this.rejectNotHandled(drawingElement); - } - - async visitSubmissionContainerElementAsync(submissionContainerElement: SubmissionContainerElement): Promise { - if (this.content instanceof SubmissionContainerContentBody) { - if (this.content.dueDate !== undefined) { - submissionContainerElement.dueDate = this.content.dueDate; - } - return Promise.resolve(); - } - return this.rejectNotHandled(submissionContainerElement); - } - - async visitSubmissionItemAsync(submission: SubmissionItem): Promise { - return this.rejectNotHandled(submission); - } - - async visitExternalToolElementAsync(externalToolElement: ExternalToolElement): Promise { - if (this.content instanceof ExternalToolContentBody && this.content.contextExternalToolId !== undefined) { - // Updates should not remove an existing reference to a tool, to prevent orphan tool instances - externalToolElement.contextExternalToolId = this.content.contextExternalToolId; - return Promise.resolve(); - } - return this.rejectNotHandled(externalToolElement); - } - - async visitCollaborativeTextEditorElementAsync( - collaborativeTextEditorElement: CollaborativeTextEditorElement - ): Promise { - return this.rejectNotHandled(collaborativeTextEditorElement); - } - - private rejectNotHandled(component: AnyBoardDo): Promise { - return Promise.reject(new Error(`Cannot update element of type: '${component.constructor.name}'`)); - } - - visitMediaBoardAsync(mediaBoard: MediaBoard): Promise { - return this.rejectNotHandled(mediaBoard); - } - - visitMediaLineAsync(mediaLine: MediaLine): Promise { - return this.rejectNotHandled(mediaLine); - } - - visitMediaExternalToolElementAsync(mediaElement: MediaExternalToolElement): Promise { - return this.rejectNotHandled(mediaElement); - } -} diff --git a/apps/server/src/modules/board/service/content-element.service.spec.ts b/apps/server/src/modules/board/service/content-element.service.spec.ts deleted file mode 100644 index cacd9a9a80d..00000000000 --- a/apps/server/src/modules/board/service/content-element.service.spec.ts +++ /dev/null @@ -1,429 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ContextExternalTool } from '@modules/tool/context-external-tool/domain'; -import { BadRequestException, NotFoundException } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import { - ContentElementFactory, - ContentElementType, - FileElement, - RichTextElement, - SubmissionContainerElement, -} from '@shared/domain/domainobject'; -import { InputFormat } from '@shared/domain/types'; -import { - cardFactory, - drawingElementFactory, - fileElementFactory, - linkElementFactory, - richTextElementFactory, - setupEntities, - submissionContainerElementFactory, -} from '@shared/testing'; -import { contextExternalToolFactory } from '@src/modules/tool/context-external-tool/testing'; -import { - DrawingContentBody, - FileContentBody, - LinkContentBody, - RichTextContentBody, - SubmissionContainerContentBody, -} from '../controller/dto'; -import { BoardDoRepo } from '../repo'; -import { BoardDoService } from './board-do.service'; -import { ContentElementService } from './content-element.service'; - -describe(ContentElementService.name, () => { - let module: TestingModule; - let service: ContentElementService; - let boardDoRepo: DeepMocked; - let boardDoService: DeepMocked; - let contentElementFactory: DeepMocked; - - beforeAll(async () => { - module = await Test.createTestingModule({ - providers: [ - ContentElementService, - { - provide: BoardDoRepo, - useValue: createMock(), - }, - { - provide: BoardDoService, - useValue: createMock(), - }, - { - provide: ContentElementFactory, - useValue: createMock(), - }, - ], - }).compile(); - - service = module.get(ContentElementService); - boardDoRepo = module.get(BoardDoRepo); - boardDoService = module.get(BoardDoService); - contentElementFactory = module.get(ContentElementFactory); - - await setupEntities(); - }); - - afterAll(async () => { - await module.close(); - }); - - describe('findById', () => { - describe('when trying get RichTextElement by id', () => { - const setup = () => { - const richTextElement = richTextElementFactory.build(); - boardDoRepo.findById.mockResolvedValue(richTextElement); - - return { richTextElement }; - }; - - it('should return instance of RichTextElement', async () => { - const { richTextElement } = setup(); - - const result = await service.findById(richTextElement.id); - - expect(result).toBeInstanceOf(RichTextElement); - }); - }); - - describe('when trying get FileElement by id', () => { - const setup = () => { - const fileElement = fileElementFactory.build(); - boardDoRepo.findById.mockResolvedValue(fileElement); - - return { fileElement }; - }; - - it('should return a FileElement', async () => { - const { fileElement } = setup(); - - const result = await service.findById(fileElement.id); - - expect(result).toBeInstanceOf(FileElement); - }); - }); - - describe('when trying get an wrong element by id', () => { - const setup = () => { - const cardElement = cardFactory.build(); - boardDoRepo.findById.mockResolvedValue(cardElement); - - return { cardElement }; - }; - - it('should throw NotFoundException', async () => { - const { cardElement } = setup(); - - await expect(service.findById(cardElement.id)).rejects.toThrowError(NotFoundException); - }); - }); - }); - - describe('findParentOfId', () => { - describe('when parent is a valid node', () => { - const setup = () => { - const card = cardFactory.build(); - const element = richTextElementFactory.build(); - - return { element, card }; - }; - - it('should call the repo', async () => { - const { element, card } = setup(); - boardDoRepo.findParentOfId.mockResolvedValueOnce(card); - - await service.findParentOfId(element.id); - - expect(boardDoRepo.findParentOfId).toHaveBeenCalledWith(element.id); - }); - - it('should throw NotFoundException', async () => { - const { element } = setup(); - - boardDoRepo.findParentOfId.mockResolvedValue(undefined); - - await expect(service.findParentOfId(element.id)).rejects.toThrowError(NotFoundException); - }); - - it('should return the parent', async () => { - const { element, card } = setup(); - boardDoRepo.findParentOfId.mockResolvedValueOnce(card); - - const result = await service.findParentOfId(element.id); - - expect(result).toEqual(card); - }); - }); - }); - - describe('countBoardUsageForExternalTools', () => { - describe('when counting the amount of boards used by tools', () => { - const setup = () => { - const contextExternalTools: ContextExternalTool[] = contextExternalToolFactory.buildListWithId(3); - - boardDoRepo.countBoardUsageForExternalTools.mockResolvedValueOnce(3); - - return { - contextExternalTools, - }; - }; - - it('should count the usages', async () => { - const { contextExternalTools } = setup(); - - await service.countBoardUsageForExternalTools(contextExternalTools); - - expect(boardDoRepo.countBoardUsageForExternalTools).toHaveBeenCalledWith(contextExternalTools); - }); - - it('should return the amount of boards', async () => { - const { contextExternalTools } = setup(); - - const result: number = await service.countBoardUsageForExternalTools(contextExternalTools); - - expect(result).toEqual(3); - }); - }); - }); - - describe('create', () => { - describe('when creating a content element of type', () => { - const setup = () => { - const card = cardFactory.build(); - const cardId = card.id; - const richTextElement = richTextElementFactory.build(); - - contentElementFactory.build.mockReturnValue(richTextElement); - - return { card, cardId, richTextElement }; - }; - - it('should call getElement method of ContentElementProvider', async () => { - const { card } = setup(); - - await service.create(card, ContentElementType.RICH_TEXT); - - expect(contentElementFactory.build).toHaveBeenCalledWith(ContentElementType.RICH_TEXT); - }); - - it('should call addChild method of parent element', async () => { - const { card, richTextElement } = setup(); - const spy = jest.spyOn(card, 'addChild'); - - await service.create(card, ContentElementType.RICH_TEXT); - - expect(spy).toHaveBeenCalledWith(richTextElement); - }); - - it('should call save method of boardDo repo', async () => { - const { card, richTextElement } = setup(); - - await service.create(card, ContentElementType.RICH_TEXT); - - expect(boardDoRepo.save).toHaveBeenCalledWith([richTextElement], card); - }); - }); - - describe('when creating a drawing element multiple times', () => { - const setup = () => { - const card = cardFactory.build(); - const drawingElement = drawingElementFactory.build(); - - contentElementFactory.build.mockReturnValue(drawingElement); - - return { card, drawingElement }; - }; - - it('should return error for second creation', async () => { - const { card } = setup(); - - await service.create(card, ContentElementType.DRAWING); - - await expect(service.create(card, ContentElementType.DRAWING)).rejects.toThrow(BadRequestException); - }); - }); - }); - - describe('delete', () => { - describe('when deleting an element', () => { - it('should call the service', async () => { - const element = richTextElementFactory.build(); - - await service.delete(element); - - expect(boardDoService.deleteWithDescendants).toHaveBeenCalledWith(element); - }); - }); - }); - - describe('move', () => { - describe('when moving an element', () => { - it('should call the service', async () => { - const targetParent = cardFactory.build(); - const element = richTextElementFactory.build(); - - await service.move(element, targetParent, 3); - - expect(boardDoService.move).toHaveBeenCalledWith(element, targetParent, 3); - }); - }); - }); - - describe('update', () => { - describe('when element is a rich text element', () => { - const setup = () => { - const richTextElement = richTextElementFactory.build(); - const content = new RichTextContentBody(); - content.text = '

this has been updated

'; - content.inputFormat = InputFormat.RICH_TEXT_CK5; - const card = cardFactory.build(); - boardDoRepo.findParentOfId.mockResolvedValue(card); - - return { richTextElement, content, card }; - }; - - it('should update the element', async () => { - const { richTextElement, content } = setup(); - - await service.update(richTextElement, content); - - expect(richTextElement.text).toEqual(content.text); - expect(richTextElement.inputFormat).toEqual(InputFormat.RICH_TEXT_CK5); - }); - - it('should persist the element', async () => { - const { richTextElement, content, card } = setup(); - - await service.update(richTextElement, content); - - expect(boardDoRepo.save).toHaveBeenCalledWith(richTextElement, card); - }); - }); - - describe('when element is a drawing element', () => { - const setup = () => { - const drawingElement = drawingElementFactory.build(); - const content = new DrawingContentBody(); - content.description = 'test-description'; - const card = cardFactory.build(); - boardDoRepo.findParentOfId.mockResolvedValue(card); - - return { drawingElement, content, card }; - }; - - it('should update the element', async () => { - const { drawingElement, content } = setup(); - - await service.update(drawingElement, content); - - expect(drawingElement.description).toEqual(content.description); - }); - - it('should persist the element', async () => { - const { drawingElement, content, card } = setup(); - - await service.update(drawingElement, content); - - expect(boardDoRepo.save).toHaveBeenCalledWith(drawingElement, card); - }); - }); - - describe('when element is a file element', () => { - const setup = () => { - const fileElement = fileElementFactory.build(); - - const content = new FileContentBody(); - content.caption = 'this has been updated'; - content.alternativeText = 'this altText has been updated'; - const card = cardFactory.build(); - boardDoRepo.findParentOfId.mockResolvedValue(card); - - return { fileElement, content, card }; - }; - - it('should update the element', async () => { - const { fileElement, content } = setup(); - - await service.update(fileElement, content); - - expect(fileElement.caption).toEqual(content.caption); - expect(fileElement.alternativeText).toEqual(content.alternativeText); - }); - - it('should persist the element', async () => { - const { fileElement, content, card } = setup(); - - await service.update(fileElement, content); - - expect(boardDoRepo.save).toHaveBeenCalledWith(fileElement, card); - }); - }); - - describe('when element is a link element', () => { - const setup = () => { - const linkElement = linkElementFactory.build(); - - const content = new LinkContentBody(); - content.url = 'https://www.medium.com/great-article'; - const card = cardFactory.build(); - boardDoRepo.findParentOfId.mockResolvedValue(card); - - const imageResponse = { - title: 'Webpage-title', - description: '', - url: linkElement.url, - image: { url: 'https://my-open-graph-proxy.scvs.de/image/adefcb12ed3a' }, - }; - - return { linkElement, content, card, imageResponse }; - }; - - it('should persist the element', async () => { - const { linkElement, content, card } = setup(); - - await service.update(linkElement, content); - - expect(boardDoRepo.save).toHaveBeenCalledWith(linkElement, card); - }); - - it('should call open graph service', async () => { - const { linkElement, content, card } = setup(); - - await service.update(linkElement, content); - - expect(boardDoRepo.save).toHaveBeenCalledWith(linkElement, card); - }); - }); - - describe('when element is a submission container element', () => { - const setup = () => { - const submissionContainerElement = submissionContainerElementFactory.build(); - - const content = new SubmissionContainerContentBody(); - content.dueDate = new Date(); - - const card = cardFactory.build(); - boardDoRepo.findParentOfId.mockResolvedValue(card); - - return { submissionContainerElement, content, card }; - }; - - it('should update the element', async () => { - const { submissionContainerElement, content } = setup(); - - const element = (await service.update(submissionContainerElement, content)) as SubmissionContainerElement; - - expect(element.dueDate).toEqual(content.dueDate); - }); - - it('should persist the element', async () => { - const { submissionContainerElement, content, card } = setup(); - - const element = await service.update(submissionContainerElement, content); - - expect(boardDoRepo.save).toHaveBeenCalledWith(element, card); - }); - }); - }); -}); diff --git a/apps/server/src/modules/board/service/content-element.service.ts b/apps/server/src/modules/board/service/content-element.service.ts deleted file mode 100644 index 9c949e43240..00000000000 --- a/apps/server/src/modules/board/service/content-element.service.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { ContextExternalTool } from '@modules/tool/context-external-tool/domain'; -import { Injectable, NotFoundException } from '@nestjs/common'; -import { - AnyBoardDo, - AnyContentElementDo, - Card, - ContentElementFactory, - ContentElementType, - SubmissionItem, - isAnyContentElement, -} from '@shared/domain/domainobject'; -import { EntityId } from '@shared/domain/types'; -import { AnyElementContentBody } from '../controller/dto'; -import { BoardDoRepo } from '../repo'; -import { BoardDoService } from './board-do.service'; -import { ContentElementUpdateVisitor } from './content-element-update.visitor'; - -@Injectable() -export class ContentElementService { - constructor( - private readonly boardDoRepo: BoardDoRepo, - private readonly boardDoService: BoardDoService, - private readonly contentElementFactory: ContentElementFactory - ) {} - - async findById(elementId: EntityId): Promise { - const element = await this.boardDoRepo.findById(elementId); - - if (!isAnyContentElement(element)) { - throw new NotFoundException(`There is no '${element.constructor.name}' with this id`); - } - - return element; - } - - async findParentOfId(elementId: EntityId): Promise { - const parent = await this.boardDoRepo.findParentOfId(elementId); - if (!parent) { - throw new NotFoundException('There is no node with this id'); - } - return parent; - } - - async countBoardUsageForExternalTools(contextExternalTools: ContextExternalTool[]): Promise { - const count: number = await this.boardDoRepo.countBoardUsageForExternalTools(contextExternalTools); - - return count; - } - - async create(parent: Card | SubmissionItem, type: ContentElementType): Promise { - const element = this.contentElementFactory.build(type); - parent.addChild(element); - - await this.boardDoRepo.save(parent.children, parent); - - return element; - } - - async delete(element: AnyContentElementDo): Promise { - await this.boardDoService.deleteWithDescendants(element); - } - - async move(element: AnyContentElementDo, targetCard: Card, targetPosition: number): Promise { - await this.boardDoService.move(element, targetCard, targetPosition); - } - - async update(element: AnyContentElementDo, content: AnyElementContentBody): Promise { - const updater = new ContentElementUpdateVisitor(content); - await element.acceptAsync(updater); - - const parent = await this.boardDoRepo.findParentOfId(element.id); - - await this.boardDoRepo.save(element, parent); - - return element; - } -} diff --git a/apps/server/src/modules/board/service/event/user-deleted-event-handler.service.spec.ts b/apps/server/src/modules/board/service/event/user-deleted-event-handler.service.spec.ts index 942162218b6..d8482bcad0e 100644 --- a/apps/server/src/modules/board/service/event/user-deleted-event-handler.service.spec.ts +++ b/apps/server/src/modules/board/service/event/user-deleted-event-handler.service.spec.ts @@ -1,11 +1,6 @@ import { createMock, type DeepMocked } from '@golevelup/ts-jest'; import { MikroORM } from '@mikro-orm/core'; import { ObjectId } from '@mikro-orm/mongodb'; -import { EventBus } from '@nestjs/cqrs'; -import { Test, TestingModule } from '@nestjs/testing'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; -import { mediaBoardFactory, setupEntities } from '@shared/testing'; -import { Logger } from '@src/core/logger'; import { DataDeletedEvent, DomainDeletionReportBuilder, @@ -13,7 +8,13 @@ import { DomainOperationReportBuilder, OperationType, UserDeletedEvent, -} from '../../../deletion'; +} from '@modules/deletion'; +import { EventBus } from '@nestjs/cqrs'; +import { Test, TestingModule } from '@nestjs/testing'; +import { setupEntities } from '@shared/testing'; +import { Logger } from '@src/core/logger'; +import { mediaBoardFactory } from '../../testing'; +import { BoardNodeService } from '../board-node.service'; import { MediaBoardService } from '../media-board'; import { UserDeletedEventHandlerService } from './user-deleted-event-handler.service'; @@ -21,6 +22,7 @@ describe(UserDeletedEventHandlerService.name, () => { let module: TestingModule; let service: UserDeletedEventHandlerService; + let boardNodeService: DeepMocked; let mediaBoardService: DeepMocked; let eventBus: DeepMocked; @@ -28,6 +30,10 @@ describe(UserDeletedEventHandlerService.name, () => { module = await Test.createTestingModule({ providers: [ UserDeletedEventHandlerService, + { + provide: BoardNodeService, + useValue: createMock(), + }, { provide: MediaBoardService, useValue: createMock(), @@ -48,6 +54,7 @@ describe(UserDeletedEventHandlerService.name, () => { }).compile(); service = module.get(UserDeletedEventHandlerService); + boardNodeService = module.get(BoardNodeService); mediaBoardService = module.get(MediaBoardService); eventBus = module.get(EventBus); }); @@ -66,8 +73,7 @@ describe(UserDeletedEventHandlerService.name, () => { const board = mediaBoardFactory.build(); const userId = new ObjectId().toHexString(); - mediaBoardService.findIdsByExternalReference.mockResolvedValueOnce([board.id]); - mediaBoardService.deleteByExternalReference.mockResolvedValueOnce(1); + mediaBoardService.findByExternalReference.mockResolvedValueOnce([board]); return { board, @@ -76,14 +82,11 @@ describe(UserDeletedEventHandlerService.name, () => { }; it('should delete all user boards', async () => { - const { userId } = setup(); + const { board, userId } = setup(); await service.deleteUserData(userId); - expect(mediaBoardService.deleteByExternalReference).toHaveBeenCalledWith({ - type: BoardExternalReferenceType.User, - id: userId, - }); + expect(boardNodeService.delete).toHaveBeenCalledWith(board); }); it('should return a report report', async () => { diff --git a/apps/server/src/modules/board/service/event/user-deleted-event-handler.service.ts b/apps/server/src/modules/board/service/event/user-deleted-event-handler.service.ts index 795feb0d241..9ec1fde5598 100644 --- a/apps/server/src/modules/board/service/event/user-deleted-event-handler.service.ts +++ b/apps/server/src/modules/board/service/event/user-deleted-event-handler.service.ts @@ -13,15 +13,17 @@ import { } from '@modules/deletion'; import { Injectable } from '@nestjs/common'; import { EventBus, EventsHandler, IEventHandler } from '@nestjs/cqrs'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; import { EntityId } from '@shared/domain/types'; import { Logger } from '@src/core/logger'; -import { MediaBoardService } from '../media-board'; +import { BoardExternalReferenceType, MediaBoard } from '../../domain'; +import { BoardNodeService } from '../board-node.service'; +import { MediaBoardService } from '../media-board/media-board.service'; @Injectable() @EventsHandler(UserDeletedEvent) export class UserDeletedEventHandlerService implements DeletionService, IEventHandler { constructor( + private readonly boardNodeService: BoardNodeService, private readonly mediaBoardService: MediaBoardService, private readonly logger: Logger, private readonly eventBus: EventBus, @@ -42,15 +44,19 @@ export class UserDeletedEventHandlerService implements DeletionService, IEventHa new DataDeletionDomainOperationLoggable('Deleting data from Board', DomainName.BOARD, userId, StatusModel.PENDING) ); - const boardIds: EntityId[] = await this.mediaBoardService.findIdsByExternalReference({ + const mediaBoards: MediaBoard[] = await this.mediaBoardService.findByExternalReference({ type: BoardExternalReferenceType.User, id: userId, }); - const numberOfDeletedBoards: number = await this.mediaBoardService.deleteByExternalReference({ - type: BoardExternalReferenceType.User, - id: userId, - }); + await Promise.all( + mediaBoards.map(async (mb) => { + await this.boardNodeService.delete(mb); + }) + ); + + const numberOfDeletedBoards = mediaBoards.length; + const boardIds = mediaBoards.map((mb) => mb.id); const result: DomainDeletionReport = DomainDeletionReportBuilder.build(DomainName.BOARD, [ DomainOperationReportBuilder.build(OperationType.DELETE, numberOfDeletedBoards, boardIds), diff --git a/apps/server/src/modules/board/service/index.ts b/apps/server/src/modules/board/service/index.ts index b0934f59ad8..8a3b9cde4cc 100644 --- a/apps/server/src/modules/board/service/index.ts +++ b/apps/server/src/modules/board/service/index.ts @@ -1,9 +1,7 @@ -export * from './board-do-authorizable.service'; -export * from './board-do.service'; -export * from './card.service'; +export * from './board-common-tool.service'; +export * from './board-node-authorizable.service'; +export * from './board-node-permission.service'; +export * from './board-node.service'; export * from './column-board.service'; -export * from './column.service'; -export * from './content-element.service'; -export * from './submission-item.service'; -export * from './media-board'; export * from './event'; +export * from './media-board'; diff --git a/apps/server/src/modules/board/service/internal/board-context.service.spec.ts b/apps/server/src/modules/board/service/internal/board-context.service.spec.ts new file mode 100644 index 00000000000..46e09f3598e --- /dev/null +++ b/apps/server/src/modules/board/service/internal/board-context.service.spec.ts @@ -0,0 +1,206 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { Test, TestingModule } from '@nestjs/testing'; +import { CourseRepo } from '@shared/repo'; +import { courseFactory, setupEntities, userFactory } from '@shared/testing'; +import { columnFactory, columnBoardFactory } from '../../testing'; +import { BoardExternalReferenceType, BoardRoles, UserWithBoardRoles } from '../../domain'; +import { BoardContextService } from './board-context.service'; + +describe(`${BoardContextService.name}`, () => { + let module: TestingModule; + let service: BoardContextService; + let courseRepo: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + BoardContextService, + { + provide: CourseRepo, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(BoardContextService); + courseRepo = module.get(CourseRepo); + + await setupEntities(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('getUsersWithBoardRoles', () => { + describe('when node has no context', () => { + const setup = () => { + const column = columnFactory.build({}); + + return { column }; + }; + + it('should return empty array', async () => { + const { column } = setup(); + + const result = await service.getUsersWithBoardRoles(column); + + expect(result).toHaveLength(0); + }); + }); + + describe('when node has wrong context type', () => { + const setup = () => { + const columnBoard = columnBoardFactory.build({ + context: { id: new ObjectId().toHexString(), type: 'foo' as BoardExternalReferenceType }, + }); + + return { columnBoard }; + }; + + it('should throw an error', async () => { + const { columnBoard } = setup(); + + await expect(service.getUsersWithBoardRoles(columnBoard)).rejects.toThrowError(); + }); + }); + + describe('when node has a user context', () => { + const setup = () => { + const columnBoard = columnBoardFactory.build({ + context: { id: new ObjectId().toHexString(), type: BoardExternalReferenceType.User }, + }); + + return { columnBoard }; + }; + + it('should return user id + editor role', async () => { + const { columnBoard } = setup(); + + const result = await service.getUsersWithBoardRoles(columnBoard); + const expected: UserWithBoardRoles[] = [ + { + userId: columnBoard.context.id, + roles: [BoardRoles.EDITOR], + }, + ]; + + expect(result).toEqual(expected); + }); + }); + + describe('when node has a course context', () => { + describe('when teachers are associated with the course', () => { + const setup = () => { + const teacher = userFactory.build(); + const course = courseFactory.buildWithId({ teachers: [teacher] }); + const columnBoard = columnBoardFactory.build({ + context: { id: course.id, type: BoardExternalReferenceType.Course }, + }); + courseRepo.findById.mockResolvedValue(course); + + return { columnBoard, teacher }; + }; + + it('should return their information + editor role', async () => { + const { columnBoard, teacher } = setup(); + + const result = await service.getUsersWithBoardRoles(columnBoard); + const expected: UserWithBoardRoles[] = [ + { + userId: teacher.id, + firstName: teacher.firstName, + lastName: teacher.lastName, + roles: [BoardRoles.EDITOR], + }, + ]; + + expect(result).toEqual(expected); + }); + }); + + describe('when substitution teachers are associated with the course', () => { + const setup = () => { + const substitutionTeacher = userFactory.build(); + const course = courseFactory.buildWithId({ substitutionTeachers: [substitutionTeacher] }); + const columnBoard = columnBoardFactory.build({ + context: { id: course.id, type: BoardExternalReferenceType.Course }, + }); + courseRepo.findById.mockResolvedValue(course); + + return { columnBoard, substitutionTeacher }; + }; + + it('should return their information + editor role', async () => { + const { columnBoard, substitutionTeacher } = setup(); + + const result = await service.getUsersWithBoardRoles(columnBoard); + const expected: UserWithBoardRoles[] = [ + { + userId: substitutionTeacher.id, + firstName: substitutionTeacher.firstName, + lastName: substitutionTeacher.lastName, + roles: [BoardRoles.EDITOR], + }, + ]; + + expect(result).toEqual(expected); + }); + }); + + describe('when students are associated with the course', () => { + const setup = () => { + const student = userFactory.build(); + const course = courseFactory.buildWithId({ students: [student] }); + const columnBoard = columnBoardFactory.build({ + context: { id: course.id, type: BoardExternalReferenceType.Course }, + }); + courseRepo.findById.mockResolvedValue(course); + + return { columnBoard, student }; + }; + + it('should return their information + reader role', async () => { + const { columnBoard, student } = setup(); + + const result = await service.getUsersWithBoardRoles(columnBoard); + const expected: UserWithBoardRoles[] = [ + { + userId: student.id, + firstName: student.firstName, + lastName: student.lastName, + roles: [BoardRoles.READER], + }, + ]; + + expect(result).toEqual(expected); + }); + }); + + describe('when evaluating the course context', () => { + const setup = () => { + const course = courseFactory.buildWithId(); + const columnBoard = columnBoardFactory.build({ + context: { id: course.id, type: BoardExternalReferenceType.Course }, + }); + courseRepo.findById.mockResolvedValue(course); + + return { columnBoard }; + }; + + it('should call the course repo', async () => { + const { columnBoard } = setup(); + + await service.getUsersWithBoardRoles(columnBoard); + + expect(courseRepo.findById).toHaveBeenCalledWith(columnBoard.context.id); + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/board/service/internal/board-context.service.ts b/apps/server/src/modules/board/service/internal/board-context.service.ts new file mode 100644 index 00000000000..42725a45c36 --- /dev/null +++ b/apps/server/src/modules/board/service/internal/board-context.service.ts @@ -0,0 +1,70 @@ +import { Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain/types'; +import { CourseRepo } from '@shared/repo'; +import { AnyBoardNode, BoardExternalReferenceType, BoardRoles, UserWithBoardRoles } from '../../domain'; + +@Injectable() +export class BoardContextService { + constructor(private readonly courseRepo: CourseRepo) {} + + async getUsersWithBoardRoles(rootNode: AnyBoardNode): Promise { + if (!('context' in rootNode)) { + return []; + } + + let usersWithRoles: UserWithBoardRoles[] = []; + + if (rootNode.context.type === BoardExternalReferenceType.Course) + usersWithRoles = await this.getFromCourse(rootNode.context.id); + else if (rootNode.context.type === BoardExternalReferenceType.User) { + usersWithRoles = this.getFromUser(rootNode.context.id); + } else { + throw new Error(`Unknown context type: '${rootNode.context.type as string}'`); + } + + return usersWithRoles; + } + + private async getFromCourse(courseId: EntityId): Promise { + const course = await this.courseRepo.findById(courseId); + const usersWithRoles: UserWithBoardRoles[] = [ + ...course.getTeachersList().map((user) => { + return { + userId: user.id, + firstName: user.firstName, + lastName: user.lastName, + roles: [BoardRoles.EDITOR], + }; + }), + ...course.getSubstitutionTeachersList().map((user) => { + return { + userId: user.id, + firstName: user.firstName, + lastName: user.lastName, + roles: [BoardRoles.EDITOR], + }; + }), + ...course.getStudentsList().map((user) => { + return { + userId: user.id, + firstName: user.firstName, + lastName: user.lastName, + roles: [BoardRoles.READER], + }; + }), + ]; + + return usersWithRoles; + } + + private getFromUser(userId: EntityId): UserWithBoardRoles[] { + const usersWithRoles: UserWithBoardRoles[] = [ + { + userId, + roles: [BoardRoles.EDITOR], + }, + ]; + + return usersWithRoles; + } +} diff --git a/apps/server/src/modules/board/service/internal/board-node-copy-context.spec.ts b/apps/server/src/modules/board/service/internal/board-node-copy-context.spec.ts new file mode 100644 index 00000000000..410e8d818ea --- /dev/null +++ b/apps/server/src/modules/board/service/internal/board-node-copy-context.spec.ts @@ -0,0 +1,45 @@ +import { createMock } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { FileRecordParentType } from '@src/infra/rabbitmq'; +import { FilesStorageClientAdapterService } from '@src/modules/files-storage-client'; +import { BoardNodeCopyContext } from './board-node-copy-context'; + +describe(BoardNodeCopyContext.name, () => { + describe('copyFilesOfParent', () => { + const setup = () => { + const contextProps = { + sourceSchoolId: new ObjectId().toHexString(), + targetSchoolId: new ObjectId().toHexString(), + userId: new ObjectId().toHexString(), + filesStorageClientAdapterService: createMock(), + }; + + const copyContext = new BoardNodeCopyContext(contextProps); + + const sourceParentId = new ObjectId().toHexString(); + const targetParentId = new ObjectId().toHexString(); + + return { contextProps, copyContext, sourceParentId, targetParentId }; + }; + + it('should use the service to copy the files', async () => { + const { contextProps, copyContext, sourceParentId, targetParentId } = setup(); + + await copyContext.copyFilesOfParent(sourceParentId, targetParentId); + + expect(contextProps.filesStorageClientAdapterService.copyFilesOfParent).toHaveBeenCalledWith({ + source: { + parentId: sourceParentId, + parentType: FileRecordParentType.BoardNode, + schoolId: contextProps.sourceSchoolId, + }, + target: { + parentId: targetParentId, + parentType: FileRecordParentType.BoardNode, + schoolId: contextProps.targetSchoolId, + }, + userId: contextProps.userId, + }); + }); + }); +}); diff --git a/apps/server/src/modules/board/service/internal/board-node-copy-context.ts b/apps/server/src/modules/board/service/internal/board-node-copy-context.ts new file mode 100644 index 00000000000..f32c148ad55 --- /dev/null +++ b/apps/server/src/modules/board/service/internal/board-node-copy-context.ts @@ -0,0 +1,32 @@ +import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; +import { CopyFileDto } from '@modules/files-storage-client/dto'; +import { EntityId } from '@shared/domain/types'; +import { FileRecordParentType } from '@src/infra/rabbitmq'; +import { CopyContext } from './board-node-copy.service'; + +export type BoardNodeCopyContextProps = { + sourceSchoolId: EntityId; + targetSchoolId: EntityId; + userId: EntityId; + filesStorageClientAdapterService: FilesStorageClientAdapterService; +}; + +export class BoardNodeCopyContext implements CopyContext { + constructor(private readonly props: BoardNodeCopyContextProps) {} + + copyFilesOfParent(sourceParentId: EntityId, targetParentId: EntityId): Promise { + return this.props.filesStorageClientAdapterService.copyFilesOfParent({ + source: { + parentId: sourceParentId, + parentType: FileRecordParentType.BoardNode, + schoolId: this.props.sourceSchoolId, + }, + target: { + parentId: targetParentId, + parentType: FileRecordParentType.BoardNode, + schoolId: this.props.targetSchoolId, + }, + userId: this.props.userId, + }); + } +} diff --git a/apps/server/src/modules/board/service/internal/board-node-copy-general.service.spec.ts b/apps/server/src/modules/board/service/internal/board-node-copy-general.service.spec.ts new file mode 100644 index 00000000000..2ae327b906d --- /dev/null +++ b/apps/server/src/modules/board/service/internal/board-node-copy-general.service.spec.ts @@ -0,0 +1,266 @@ +import { createMock } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; +import { ContextExternalToolService } from '@modules/tool/context-external-tool/service'; +import { IToolFeatures, ToolFeatures } from '@modules/tool/tool-config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { setupEntities } from '@shared/testing'; +import { FilesStorageClientAdapterService } from '@src/modules/files-storage-client'; +import { + cardFactory, + collaborativeTextEditorFactory, + columnBoardFactory, + columnFactory, + drawingElementFactory, + externalToolElementFactory, + fileElementFactory, + linkElementFactory, + mediaBoardFactory, + mediaExternalToolElementFactory, + mediaLineFactory, + richTextElementFactory, + submissionContainerElementFactory, + submissionItemFactory, +} from '../../testing'; +import { BoardNodeCopyContext, BoardNodeCopyContextProps } from './board-node-copy-context'; +import { BoardNodeCopyService } from './board-node-copy.service'; + +describe(BoardNodeCopyService.name, () => { + let module: TestingModule; + let service: BoardNodeCopyService; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + BoardNodeCopyService, + { + provide: ToolFeatures, + useValue: createMock(), + }, + { + provide: ContextExternalToolService, + useValue: createMock(), + }, + { + provide: CopyHelperService, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(BoardNodeCopyService); + + await setupEntities(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + const setup = () => { + const contextProps: BoardNodeCopyContextProps = { + sourceSchoolId: new ObjectId().toHexString(), + targetSchoolId: new ObjectId().toHexString(), + userId: new ObjectId().toHexString(), + filesStorageClientAdapterService: createMock(), + }; + + const copyContext = new BoardNodeCopyContext(contextProps); + + const mockStatus: CopyStatus = { + status: CopyStatusEnum.SUCCESS, + type: CopyElementType.BOARD, + }; + + jest.spyOn(service, 'copyColumnBoard').mockResolvedValue(mockStatus); + jest.spyOn(service, 'copyColumn').mockResolvedValue(mockStatus); + jest.spyOn(service, 'copyCard').mockResolvedValue(mockStatus); + jest.spyOn(service, 'copyFileElement').mockResolvedValue(mockStatus); + jest.spyOn(service, 'copyLinkElement').mockResolvedValue(mockStatus); + jest.spyOn(service, 'copyRichTextElement').mockResolvedValue(mockStatus); + jest.spyOn(service, 'copyDrawingElement').mockResolvedValue(mockStatus); + jest.spyOn(service, 'copySubmissionContainerElement').mockResolvedValue(mockStatus); + jest.spyOn(service, 'copySubmissionItem').mockResolvedValue(mockStatus); + jest.spyOn(service, 'copyExternalToolElement').mockResolvedValue(mockStatus); + jest.spyOn(service, 'copyCollaborativeTextEditorElement').mockResolvedValue(mockStatus); + jest.spyOn(service, 'copyMediaBoard').mockResolvedValue(mockStatus); + jest.spyOn(service, 'copyMediaLine').mockResolvedValue(mockStatus); + jest.spyOn(service, 'copyMediaExternalToolElement').mockResolvedValue(mockStatus); + + return { copyContext, mockStatus }; + }; + + describe('copy', () => { + describe('when called with column board', () => { + it('should copy column board', async () => { + const { copyContext, mockStatus } = setup(); + const node = columnBoardFactory.build(); + + const result = await service.copy(node, copyContext); + + expect(service.copyColumnBoard).toHaveBeenCalledWith(node, copyContext); + expect(result).toEqual(mockStatus); + }); + }); + + describe('when called with column', () => { + it('should copy column', async () => { + const { copyContext, mockStatus } = setup(); + const node = columnFactory.build(); + + const result = await service.copy(node, copyContext); + + expect(service.copyColumn).toHaveBeenCalledWith(node, copyContext); + expect(result).toEqual(mockStatus); + }); + }); + + describe('when called with card', () => { + it('should copy card', async () => { + const { copyContext, mockStatus } = setup(); + const node = cardFactory.build(); + + const result = await service.copy(node, copyContext); + + expect(service.copyCard).toHaveBeenCalledWith(node, copyContext); + expect(result).toEqual(mockStatus); + }); + }); + + describe('when called with file element', () => { + it('should copy file element', async () => { + const { copyContext, mockStatus } = setup(); + const node = fileElementFactory.build(); + + const result = await service.copy(node, copyContext); + + expect(service.copyFileElement).toHaveBeenCalledWith(node, copyContext); + expect(result).toEqual(mockStatus); + }); + + describe('when called with link element', () => { + it('should copy link element', async () => { + const { copyContext, mockStatus } = setup(); + const node = linkElementFactory.build(); + + const result = await service.copy(node, copyContext); + + expect(service.copyLinkElement).toHaveBeenCalledWith(node, copyContext); + expect(result).toEqual(mockStatus); + }); + }); + + describe('when called with rich text element', () => { + it('should copy rich text element', async () => { + const { copyContext, mockStatus } = setup(); + const node = richTextElementFactory.build(); + + const result = await service.copy(node, copyContext); + + expect(service.copyRichTextElement).toHaveBeenCalledWith(node, copyContext); + expect(result).toEqual(mockStatus); + }); + }); + + describe('when called with drawing element', () => { + it('should copy drawing element', async () => { + const { copyContext, mockStatus } = setup(); + const node = drawingElementFactory.build(); + + const result = await service.copy(node, copyContext); + + expect(service.copyDrawingElement).toHaveBeenCalledWith(node, copyContext); + expect(result).toEqual(mockStatus); + }); + }); + + describe('when called with submission container element', () => { + it('should copy submission container element', async () => { + const { copyContext, mockStatus } = setup(); + const node = submissionContainerElementFactory.build(); + + const result = await service.copy(node, copyContext); + + expect(service.copySubmissionContainerElement).toHaveBeenCalledWith(node, copyContext); + expect(result).toEqual(mockStatus); + }); + }); + + describe('when called with submission item', () => { + it('should copy submission item', async () => { + const { copyContext, mockStatus } = setup(); + const node = submissionItemFactory.build(); + + const result = await service.copy(node, copyContext); + + expect(service.copySubmissionItem).toHaveBeenCalledWith(node, copyContext); + expect(result).toEqual(mockStatus); + }); + }); + + describe('when called with external tool element', () => { + it('should copy external tool element', async () => { + const { copyContext, mockStatus } = setup(); + const node = externalToolElementFactory.build(); + + const result = await service.copy(node, copyContext); + + expect(service.copyExternalToolElement).toHaveBeenCalledWith(node, copyContext); + expect(result).toEqual(mockStatus); + }); + }); + + describe('when called with collaborative text editor element', () => { + it('should copy collaborative text editor element', async () => { + const { copyContext, mockStatus } = setup(); + const node = collaborativeTextEditorFactory.build(); + + const result = await service.copy(node, copyContext); + + expect(service.copyCollaborativeTextEditorElement).toHaveBeenCalledWith(node, copyContext); + expect(result).toEqual(mockStatus); + }); + }); + + describe('when called with media board', () => { + it('should copy collaborative media board', async () => { + const { copyContext, mockStatus } = setup(); + const node = mediaBoardFactory.build(); + + const result = await service.copy(node, copyContext); + + expect(service.copyMediaBoard).toHaveBeenCalledWith(node, copyContext); + expect(result).toEqual(mockStatus); + }); + }); + + describe('when called with media line', () => { + it('should copy collaborative media line', async () => { + const { copyContext, mockStatus } = setup(); + const node = mediaLineFactory.build(); + + const result = await service.copy(node, copyContext); + + expect(service.copyMediaLine).toHaveBeenCalledWith(node, copyContext); + expect(result).toEqual(mockStatus); + }); + }); + + describe('when called with media external tool element', () => { + it('should copy collaborative media external tool element', async () => { + const { copyContext, mockStatus } = setup(); + const node = mediaExternalToolElementFactory.build(); + + const result = await service.copy(node, copyContext); + + expect(service.copyMediaExternalToolElement).toHaveBeenCalledWith(node, copyContext); + expect(result).toEqual(mockStatus); + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/board/service/internal/board-node-copy-specific.service.spec.ts b/apps/server/src/modules/board/service/internal/board-node-copy-specific.service.spec.ts new file mode 100644 index 00000000000..5a61350fb8c --- /dev/null +++ b/apps/server/src/modules/board/service/internal/board-node-copy-specific.service.spec.ts @@ -0,0 +1,600 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; +import { ContextExternalToolService } from '@modules/tool/context-external-tool/service'; +import { IToolFeatures, ToolFeatures } from '@modules/tool/tool-config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { setupEntities } from '@shared/testing'; +import { FilesStorageClientAdapterService } from '@src/modules/files-storage-client'; +import { CopyFileDto } from '@src/modules/files-storage-client/dto'; +import { contextExternalToolFactory } from '@src/modules/tool/context-external-tool/testing'; +import { + Card, + CollaborativeTextEditorElement, + Column, + ColumnBoard, + DrawingElement, + ExternalToolElement, + FileElement, + LinkElement, + RichTextElement, + SubmissionContainerElement, +} from '../../domain'; +import { + cardFactory, + collaborativeTextEditorFactory, + columnBoardFactory, + columnFactory, + drawingElementFactory, + externalToolElementFactory, + fileElementFactory, + linkElementFactory, + mediaBoardFactory, + mediaExternalToolElementFactory, + mediaLineFactory, + richTextElementFactory, + submissionContainerElementFactory, + submissionItemFactory, +} from '../../testing'; +import { BoardNodeCopyContext } from './board-node-copy-context'; +import { BoardNodeCopyService } from './board-node-copy.service'; + +describe(BoardNodeCopyService.name, () => { + let module: TestingModule; + let service: BoardNodeCopyService; + const toolFeatures: IToolFeatures = { + ctlToolsTabEnabled: false, + ltiToolsTabEnabled: false, + maxExternalToolLogoSizeInBytes: 0, + backEndUrl: '', + ctlToolsCopyEnabled: false, + ctlToolsReloadTimeMs: 0, + }; + let contextExternalToolService: DeepMocked; + let copyHelperService: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + BoardNodeCopyService, + { + provide: ToolFeatures, + useValue: toolFeatures, + }, + { + provide: ContextExternalToolService, + useValue: createMock(), + }, + { + provide: CopyHelperService, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(BoardNodeCopyService); + contextExternalToolService = module.get(ContextExternalToolService); + copyHelperService = module.get(CopyHelperService); + + await setupEntities(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + const setupContext = () => { + const contextProps = { + sourceSchoolId: new ObjectId().toHexString(), + targetSchoolId: new ObjectId().toHexString(), + userId: new ObjectId().toHexString(), + filesStorageClientAdapterService: createMock(), + }; + + const copyContext = new BoardNodeCopyContext(contextProps); + + return { copyContext }; + }; + + describe('copy column board', () => { + const setup = () => { + const { copyContext } = setupContext(); + const columnBoard = columnBoardFactory.build({ children: columnFactory.buildList(1) }); + + return { copyContext, columnBoard }; + }; + + it('should copy the node', async () => { + const { copyContext, columnBoard } = setup(); + + const result = await service.copyColumnBoard(columnBoard, copyContext); + + expect(result.copyEntity).toBeInstanceOf(ColumnBoard); + }); + + it('should copy the children', async () => { + const { copyContext, columnBoard } = setup(); + + const result = await service.copyColumnBoard(columnBoard, copyContext); + + expect((result.elements ?? [])[0].copyEntity).toBeInstanceOf(Column); + }); + + it('should use the service to derive status from children', async () => { + const { copyContext, columnBoard } = setup(); + + const result = await service.copyColumnBoard(columnBoard, copyContext); + + expect(copyHelperService.deriveStatusFromElements).toHaveBeenCalledWith(result.elements); + }); + }); + + describe('copy column', () => { + const setup = () => { + const { copyContext } = setupContext(); + const column = columnFactory.build({ children: cardFactory.buildList(1) }); + + return { copyContext, column }; + }; + + it('should copy the node', async () => { + const { copyContext, column } = setup(); + + const result = await service.copyColumn(column, copyContext); + + expect(result.copyEntity).toBeInstanceOf(Column); + }); + + it('should copy the children', async () => { + const { copyContext, column } = setup(); + + const result = await service.copyColumn(column, copyContext); + + expect((result.elements ?? [])[0].copyEntity).toBeInstanceOf(Card); + }); + }); + + describe('copy card', () => { + const setup = () => { + const { copyContext } = setupContext(); + const card = cardFactory.build({ children: richTextElementFactory.buildList(1) }); + + return { copyContext, card }; + }; + + it('should copy the node', async () => { + const { copyContext, card } = setup(); + + const result = await service.copyCard(card, copyContext); + + expect(result.copyEntity).toBeInstanceOf(Card); + }); + + it('should copy the children', async () => { + const { copyContext, card } = setup(); + + const result = await service.copyCard(card, copyContext); + + expect((result.elements ?? [])[0].copyEntity).toBeInstanceOf(RichTextElement); + }); + }); + + describe('copy file element', () => { + const setup = () => { + const { copyContext } = setupContext(); + const fileElement = fileElementFactory.build(); + const fileCopyStatus: CopyFileDto = { + name: 'bird.jpg', + id: new ObjectId().toHexString(), + sourceId: new ObjectId().toHexString(), + }; + jest.spyOn(copyContext, 'copyFilesOfParent').mockResolvedValueOnce([fileCopyStatus]); + + return { copyContext, fileElement, fileCopyStatus }; + }; + + it('should copy the node', async () => { + const { copyContext, fileElement } = setup(); + + const result = await service.copyFileElement(fileElement, copyContext); + + expect(result.copyEntity).toBeInstanceOf(FileElement); + }); + + it('should copy the files', async () => { + const { copyContext, fileElement, fileCopyStatus } = setup(); + + const result = await service.copyFileElement(fileElement, copyContext); + + expect(copyContext.copyFilesOfParent).toHaveBeenCalledWith(fileElement.id, result.copyEntity?.id); + expect(result.elements ?? []).toEqual([expect.objectContaining({ title: fileCopyStatus.name })]); + }); + }); + + describe('copy link element', () => { + const setup = () => { + const { copyContext } = setupContext(); + const sourceId = new ObjectId().toHexString(); + const linkElement = linkElementFactory.build({ + id: sourceId, + imageUrl: `https://example.com/${sourceId}/bird.jpg`, + }); + const linkElementWithoutId = linkElementFactory.build({ + id: sourceId, + imageUrl: `https://example.com/plane.jpg`, + }); + const fileCopyStatus: CopyFileDto = { + name: 'bird.jpg', + id: new ObjectId().toHexString(), + sourceId, + }; + jest.spyOn(copyContext, 'copyFilesOfParent').mockResolvedValueOnce([fileCopyStatus]); + + return { copyContext, linkElement, linkElementWithoutId, fileCopyStatus }; + }; + + it('should copy the node', async () => { + const { copyContext, linkElement } = setup(); + + const result = await service.copyLinkElement(linkElement, copyContext); + + expect(result.copyEntity).toBeInstanceOf(LinkElement); + }); + + it('should copy the files', async () => { + const { copyContext, linkElement, fileCopyStatus } = setup(); + + const result = await service.copyLinkElement(linkElement, copyContext); + + expect(copyContext.copyFilesOfParent).toHaveBeenCalledWith(linkElement.id, result.copyEntity?.id); + expect(result.elements ?? []).toEqual([expect.objectContaining({ title: fileCopyStatus.name })]); + }); + + describe('when imageUrl includes source id', () => { + it('should replace the source id in image url', async () => { + const { copyContext, linkElement, fileCopyStatus } = setup(); + + const result = await service.copyLinkElement(linkElement, copyContext); + + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + expect((result.copyEntity as LinkElement).imageUrl).toBe(`https://example.com/${fileCopyStatus.id}/bird.jpg`); + }); + }); + + describe('when imageUrl doesnt include source id', () => { + it('should set blank image url', async () => { + const { copyContext, linkElementWithoutId } = setup(); + + const result = await service.copyLinkElement(linkElementWithoutId, copyContext); + + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + expect((result.copyEntity as LinkElement).imageUrl).toBe(''); + }); + }); + + describe('when no imageUrl is present', () => { + const setupWithoutImageUrl = () => { + const { copyContext, fileCopyStatus } = setup(); + const linkElement = linkElementFactory.build({ imageUrl: undefined }); + + return { copyContext, linkElement, fileCopyStatus }; + }; + it('should replace the source id in image urls', async () => { + const { copyContext, linkElement } = setupWithoutImageUrl(); + + const result = await service.copyLinkElement(linkElement, copyContext); + + expect((result.copyEntity as LinkElement).imageUrl).toBe(''); + }); + }); + }); + + describe('copy rich text element', () => { + const setup = () => { + const { copyContext } = setupContext(); + const richTextElement = richTextElementFactory.build(); + + return { copyContext, richTextElement }; + }; + + it('should copy the node', async () => { + const { copyContext, richTextElement } = setup(); + + const result = await service.copyRichTextElement(richTextElement, copyContext); + + expect(result.copyEntity).toBeInstanceOf(RichTextElement); + }); + }); + + describe('copy drawing element', () => { + const setup = () => { + const { copyContext } = setupContext(); + const drawingElement = drawingElementFactory.build(); + + return { copyContext, drawingElement }; + }; + + it('should copy the node', async () => { + const { copyContext, drawingElement } = setup(); + + const result = await service.copyDrawingElement(drawingElement, copyContext); + + expect(result.copyEntity).toBeInstanceOf(DrawingElement); + }); + }); + + describe('copy submission container element', () => { + const setup = () => { + const { copyContext } = setupContext(); + const submissionContainerElement = submissionContainerElementFactory.build({ + children: submissionItemFactory.buildList(1), + }); + + return { copyContext, submissionContainerElement }; + }; + + it('should copy the node', async () => { + const { copyContext, submissionContainerElement } = setup(); + + const result = await service.copySubmissionContainerElement(submissionContainerElement, copyContext); + + expect(result.copyEntity).toBeInstanceOf(SubmissionContainerElement); + }); + + it('should copy the children', async () => { + const { copyContext, submissionContainerElement } = setup(); + + const result = await service.copySubmissionContainerElement(submissionContainerElement, copyContext); + + const expectedChildStatus: CopyStatus = { + type: CopyElementType.SUBMISSION_ITEM, + status: CopyStatusEnum.NOT_DOING, + }; + expect((result.elements ?? [])[0]).toEqual(expectedChildStatus); + }); + }); + + describe('copy submission item', () => { + const setup = () => { + const { copyContext } = setupContext(); + const submissionItem = submissionItemFactory.build(); + + return { copyContext, submissionItem }; + }; + + it('should copy the node', async () => { + const { copyContext, submissionItem } = setup(); + + const result = await service.copySubmissionItem(submissionItem, copyContext); + + const expectedChildStatus: CopyStatus = { + type: CopyElementType.SUBMISSION_ITEM, + status: CopyStatusEnum.NOT_DOING, + }; + expect(result).toEqual(expectedChildStatus); + }); + }); + + describe('copy external tool element', () => { + const setup = () => { + const { copyContext } = setupContext(); + const externalToolElement = externalToolElementFactory.build({ + contextExternalToolId: new ObjectId().toHexString(), + }); + + return { copyContext, externalToolElement }; + }; + + it('should copy the node', async () => { + const { copyContext, externalToolElement } = setup(); + + const result = await service.copyExternalToolElement(externalToolElement, copyContext); + + expect(result.copyEntity).toBeInstanceOf(ExternalToolElement); + }); + + describe('when ctl tools copy is enabled', () => { + const setupCopyEnabled = () => { + const { copyContext, externalToolElement } = setup(); + + toolFeatures.ctlToolsCopyEnabled = true; + + return { copyContext, externalToolElement }; + }; + + describe('when linked external tool is found', () => { + const setupToolElement = () => { + const { copyContext, externalToolElement } = setupCopyEnabled(); + + const tool = contextExternalToolFactory.build(); + const toolCopy = contextExternalToolFactory.build(); + contextExternalToolService.findById.mockResolvedValueOnce(tool); + contextExternalToolService.copyContextExternalTool.mockResolvedValueOnce(toolCopy); + externalToolElement.contextExternalToolId = tool.id; + + return { copyContext, externalToolElement, tool, toolCopy }; + }; + + it('should copy the external tool', async () => { + const { copyContext, externalToolElement, tool, toolCopy } = setupToolElement(); + + const result = await service.copyExternalToolElement(externalToolElement, copyContext); + + expect(contextExternalToolService.findById).toHaveBeenCalledWith(tool.id); + expect(contextExternalToolService.copyContextExternalTool).toHaveBeenCalledWith(tool, result.copyEntity?.id); + expect((result.copyEntity as ExternalToolElement).contextExternalToolId).toEqual(toolCopy.id); + }); + }); + + describe('when linked external tool is not found', () => { + const setupToolNotFound = () => { + const { copyContext, externalToolElement } = setupCopyEnabled(); + + contextExternalToolService.findById.mockResolvedValueOnce(null); + + return { copyContext, externalToolElement }; + }; + + it('should return failure status', async () => { + const { copyContext, externalToolElement } = setupToolNotFound(); + + const result = await service.copyExternalToolElement(externalToolElement, copyContext); + + expect(result.status).toEqual(CopyStatusEnum.FAIL); + }); + }); + + describe('when no external tool is linked', () => { + const setupNoExternalTool = () => { + const { copyContext, externalToolElement } = setupCopyEnabled(); + + externalToolElement.contextExternalToolId = undefined; + + return { copyContext, externalToolElement }; + }; + + it('should return success status', async () => { + const { copyContext, externalToolElement } = setupNoExternalTool(); + + const result = await service.copyExternalToolElement(externalToolElement, copyContext); + + expect(result.status).toEqual(CopyStatusEnum.SUCCESS); + }); + }); + }); + + describe('when ctl tools copy is disabled', () => { + const setupCopyDisabled = () => { + const { copyContext, externalToolElement } = setup(); + + toolFeatures.ctlToolsCopyEnabled = false; + + return { copyContext, externalToolElement }; + }; + + it('should return success status', async () => { + const { copyContext, externalToolElement } = setupCopyDisabled(); + + const result = await service.copyExternalToolElement(externalToolElement, copyContext); + + expect(result.status).toEqual(CopyStatusEnum.SUCCESS); + }); + }); + }); + + describe('copy collaborative text editor element', () => { + const setup = () => { + const { copyContext } = setupContext(); + const collaborativeTextEditor = collaborativeTextEditorFactory.build(); + + return { copyContext, collaborativeTextEditor }; + }; + + it('should copy the node', async () => { + const { copyContext, collaborativeTextEditor } = setup(); + + const result = await service.copyCollaborativeTextEditorElement(collaborativeTextEditor, copyContext); + + expect(result.copyEntity).toBeInstanceOf(CollaborativeTextEditorElement); + }); + + it('should return the correct type and status', async () => { + const { copyContext, collaborativeTextEditor } = setup(); + + const result = await service.copyCollaborativeTextEditorElement(collaborativeTextEditor, copyContext); + + expect(result).toEqual( + expect.objectContaining({ + type: CopyElementType.COLLABORATIVE_TEXT_EDITOR_ELEMENT, + status: CopyStatusEnum.PARTIAL, + }) + ); + }); + }); + + describe('copy media board', () => { + const setup = () => { + const { copyContext } = setupContext(); + const mediaBoard = mediaBoardFactory.build({ children: mediaLineFactory.buildList(1) }); + + return { copyContext, mediaBoard }; + }; + + it('should return the correct type and status', async () => { + const { copyContext, mediaBoard } = setup(); + + const result = await service.copyMediaBoard(mediaBoard, copyContext); + + expect(result).toEqual( + expect.objectContaining({ + type: CopyElementType.MEDIA_BOARD, + status: CopyStatusEnum.NOT_DOING, + }) + ); + }); + + it('should copy the children', async () => { + const { copyContext, mediaBoard } = setup(); + + const result = await service.copyMediaBoard(mediaBoard, copyContext); + + expect(result.elements ?? []).toHaveLength(1); + }); + }); + + describe('copy media line', () => { + const setup = () => { + const { copyContext } = setupContext(); + const mediaLine = mediaLineFactory.build({ children: mediaExternalToolElementFactory.buildList(1) }); + + return { copyContext, mediaLine }; + }; + + it('should return the correct type and status', async () => { + const { copyContext, mediaLine } = setup(); + + const result = await service.copyMediaLine(mediaLine, copyContext); + + expect(result).toEqual( + expect.objectContaining({ + type: CopyElementType.MEDIA_LINE, + status: CopyStatusEnum.NOT_DOING, + }) + ); + }); + + it('should copy the children', async () => { + const { copyContext, mediaLine } = setup(); + + const result = await service.copyMediaLine(mediaLine, copyContext); + + expect(result.elements ?? []).toHaveLength(1); + }); + }); + + describe('copy media external tool element', () => { + const setup = () => { + const { copyContext } = setupContext(); + const mediaExternalToolElement = mediaExternalToolElementFactory.build(); + + return { copyContext, mediaExternalToolElement }; + }; + + it('should return the correct type and status', async () => { + const { copyContext, mediaExternalToolElement } = setup(); + + const result = await service.copyMediaExternalToolElement(mediaExternalToolElement, copyContext); + + expect(result).toEqual( + expect.objectContaining({ + type: CopyElementType.MEDIA_EXTERNAL_TOOL_ELEMENT, + status: CopyStatusEnum.NOT_DOING, + }) + ); + }); + }); +}); diff --git a/apps/server/src/modules/board/service/internal/board-node-copy.service.ts b/apps/server/src/modules/board/service/internal/board-node-copy.service.ts new file mode 100644 index 00000000000..dd254390eb9 --- /dev/null +++ b/apps/server/src/modules/board/service/internal/board-node-copy.service.ts @@ -0,0 +1,410 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; +import { CopyFileDto } from '@modules/files-storage-client/dto'; +import { ContextExternalToolService } from '@modules/tool/context-external-tool'; +import { ContextExternalTool } from '@modules/tool/context-external-tool/domain'; +import { IToolFeatures, ToolFeatures } from '@modules/tool/tool-config'; +import { Inject, Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain/types'; +import { + AnyBoardNode, + BoardNodeType, + Card, + CollaborativeTextEditorElement, + Column, + ColumnBoard, + DrawingElement, + ExternalToolElement, + FileElement, + getBoardNodeType, + handleNonExhaustiveSwitch, + LinkElement, + MediaBoard, + MediaExternalToolElement, + MediaLine, + RichTextElement, + SubmissionContainerElement, + SubmissionItem, +} from '../../domain'; + +export interface CopyContext { + copyFilesOfParent(sourceParentId: EntityId, targetParentId: EntityId): Promise; +} + +@Injectable() +export class BoardNodeCopyService { + constructor( + @Inject(ToolFeatures) private readonly toolFeatures: IToolFeatures, + private readonly contextExternalToolService: ContextExternalToolService, + private readonly copyHelperService: CopyHelperService + ) {} + + async copy(boardNode: AnyBoardNode, context: CopyContext): Promise { + const type = getBoardNodeType(boardNode); + + let result!: CopyStatus; + + switch (type) { + case BoardNodeType.COLUMN_BOARD: + result = await this.copyColumnBoard(boardNode as ColumnBoard, context); + break; + case BoardNodeType.COLUMN: + result = await this.copyColumn(boardNode as Column, context); + break; + case BoardNodeType.CARD: + result = await this.copyCard(boardNode as Card, context); + break; + case BoardNodeType.FILE_ELEMENT: + result = await this.copyFileElement(boardNode as FileElement, context); + break; + case BoardNodeType.LINK_ELEMENT: + result = await this.copyLinkElement(boardNode as LinkElement, context); + break; + case BoardNodeType.RICH_TEXT_ELEMENT: + result = await this.copyRichTextElement(boardNode as RichTextElement, context); + break; + case BoardNodeType.DRAWING_ELEMENT: + result = await this.copyDrawingElement(boardNode as DrawingElement, context); + break; + case BoardNodeType.SUBMISSION_CONTAINER_ELEMENT: + result = await this.copySubmissionContainerElement(boardNode as SubmissionContainerElement, context); + break; + case BoardNodeType.SUBMISSION_ITEM: + result = await this.copySubmissionItem(boardNode as SubmissionItem, context); + break; + case BoardNodeType.EXTERNAL_TOOL: + result = await this.copyExternalToolElement(boardNode as ExternalToolElement, context); + break; + case BoardNodeType.COLLABORATIVE_TEXT_EDITOR: + result = await this.copyCollaborativeTextEditorElement(boardNode as CollaborativeTextEditorElement, context); + break; + case BoardNodeType.MEDIA_BOARD: + result = await this.copyMediaBoard(boardNode as MediaBoard, context); + break; + case BoardNodeType.MEDIA_LINE: + result = await this.copyMediaLine(boardNode as MediaLine, context); + break; + case BoardNodeType.MEDIA_EXTERNAL_TOOL_ELEMENT: + result = await this.copyMediaExternalToolElement(boardNode as MediaExternalToolElement, context); + break; + default: + /* istanbul ignore next */ + handleNonExhaustiveSwitch(type); + } + + return result; + } + + async copyColumnBoard(original: ColumnBoard, context: CopyContext): Promise { + const childrenResults = await this.copyChildrenOf(original, context); + const childrenCopyStatus = this.copyHelperService.deriveStatusFromElements(childrenResults); + + const copy = new ColumnBoard({ + ...original.getProps(), + ...this.buildSpecificProps(childrenResults), + }); + + const result: CopyStatus = { + copyEntity: copy, + type: CopyElementType.COLUMNBOARD, + status: childrenCopyStatus, + elements: childrenResults, + }; + + return result; + } + + async copyColumn(original: Column, context: CopyContext): Promise { + const childrenResults = await this.copyChildrenOf(original, context); + + const copy = new Column({ + ...original.getProps(), + ...this.buildSpecificProps(childrenResults), + }); + + const result: CopyStatus = { + copyEntity: copy, + type: CopyElementType.COLUMN, + status: CopyStatusEnum.SUCCESS, + elements: childrenResults, + }; + + return result; + } + + async copyCard(original: Card, context: CopyContext): Promise { + const childrenResults = await this.copyChildrenOf(original, context); + + const copy = new Card({ + ...original.getProps(), + ...this.buildSpecificProps(childrenResults), + }); + + const result: CopyStatus = { + copyEntity: copy, + type: CopyElementType.CARD, + status: CopyStatusEnum.SUCCESS, + elements: childrenResults, + }; + + return result; + } + + async copyFileElement(original: FileElement, context: CopyContext): Promise { + const copy = new FileElement({ + ...original.getProps(), + ...this.buildSpecificProps([]), + }); + + const fileCopy = await context.copyFilesOfParent(original.id, copy.id); + + const fileCopyStatus = fileCopy.map((copyFileDto) => { + return { + type: CopyElementType.FILE, + status: copyFileDto.id ? CopyStatusEnum.SUCCESS : CopyStatusEnum.FAIL, + title: copyFileDto.name ?? `(old fileid: ${copyFileDto.sourceId})`, + }; + }); + + const result: CopyStatus = { + copyEntity: copy, + type: CopyElementType.FILE_ELEMENT, + status: CopyStatusEnum.SUCCESS, + elements: fileCopyStatus, + }; + + return result; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async copyLinkElement(original: LinkElement, context: CopyContext): Promise { + const copy = new LinkElement({ + ...original.getProps(), + ...this.buildSpecificProps([]), + }); + + const result: CopyStatus = { + copyEntity: copy, + type: CopyElementType.LINK_ELEMENT, + status: CopyStatusEnum.SUCCESS, + }; + + if (original.imageUrl) { + const fileCopy = await context.copyFilesOfParent(original.id, copy.id); + + 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; + } + + return Promise.resolve(result); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async copyRichTextElement(original: RichTextElement, context: CopyContext): Promise { + const copy = new RichTextElement({ + ...original.getProps(), + ...this.buildSpecificProps([]), + }); + + const result: CopyStatus = { + copyEntity: copy, + type: CopyElementType.RICHTEXT_ELEMENT, + status: CopyStatusEnum.SUCCESS, + }; + + return Promise.resolve(result); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async copyDrawingElement(original: DrawingElement, context: CopyContext): Promise { + const copy = new DrawingElement({ + ...original.getProps(), + ...this.buildSpecificProps([]), + }); + + const result: CopyStatus = { + copyEntity: copy, + type: CopyElementType.DRAWING_ELEMENT, + status: CopyStatusEnum.SUCCESS, + }; + + return Promise.resolve(result); + } + + async copySubmissionContainerElement( + original: SubmissionContainerElement, + context: CopyContext + ): Promise { + const childrenResults = await this.copyChildrenOf(original, context); + + const copy = new SubmissionContainerElement({ + ...original.getProps(), + ...this.buildSpecificProps(childrenResults), + }); + + const result: CopyStatus = { + copyEntity: copy, + type: CopyElementType.SUBMISSION_CONTAINER_ELEMENT, + status: CopyStatusEnum.SUCCESS, + elements: childrenResults, + }; + + return Promise.resolve(result); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async copySubmissionItem(original: SubmissionItem, context: CopyContext): Promise { + const result: CopyStatus = { + type: CopyElementType.SUBMISSION_ITEM, + status: CopyStatusEnum.NOT_DOING, + }; + + return Promise.resolve(result); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async copyExternalToolElement(original: ExternalToolElement, context: CopyContext): Promise { + const copy = new ExternalToolElement({ + ...original.getProps(), + ...this.buildSpecificProps([]), + }); + + let status: CopyStatusEnum; + if (this.toolFeatures.ctlToolsCopyEnabled && original.contextExternalToolId) { + const linkedTool = await this.contextExternalToolService.findById(original.contextExternalToolId); + + if (linkedTool) { + const contextExternalToolCopy: ContextExternalTool = + await this.contextExternalToolService.copyContextExternalTool(linkedTool, copy.id); + + copy.contextExternalToolId = contextExternalToolCopy.id; + + status = CopyStatusEnum.SUCCESS; + } else { + status = CopyStatusEnum.FAIL; + } + } else { + status = CopyStatusEnum.SUCCESS; + } + + const result: CopyStatus = { + copyEntity: copy, + type: CopyElementType.EXTERNAL_TOOL_ELEMENT, + status, + }; + + return Promise.resolve(result); + } + + async copyCollaborativeTextEditorElement( + original: CollaborativeTextEditorElement, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + context: CopyContext + ): Promise { + const copy = new CollaborativeTextEditorElement({ + ...original.getProps(), + ...this.buildSpecificProps([]), + }); + + const result: CopyStatus = { + copyEntity: copy, + type: CopyElementType.COLLABORATIVE_TEXT_EDITOR_ELEMENT, + status: CopyStatusEnum.PARTIAL, + }; + return Promise.resolve(result); + } + + async copyMediaBoard(original: MediaBoard, context: CopyContext): Promise { + const childrenResults = await this.copyChildrenOf(original, context); + + const result: CopyStatus = { + type: CopyElementType.MEDIA_BOARD, + status: CopyStatusEnum.NOT_DOING, + elements: childrenResults, + }; + + return Promise.resolve(result); + } + + async copyMediaLine(original: MediaLine, context: CopyContext): Promise { + const childrenResults = await this.copyChildrenOf(original, context); + + const result: CopyStatus = { + type: CopyElementType.MEDIA_LINE, + status: CopyStatusEnum.NOT_DOING, + elements: childrenResults, + }; + + return Promise.resolve(result); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async copyMediaExternalToolElement(original: MediaExternalToolElement, context: CopyContext): Promise { + const result: CopyStatus = { + type: CopyElementType.MEDIA_EXTERNAL_TOOL_ELEMENT, + status: CopyStatusEnum.NOT_DOING, + }; + + return Promise.resolve(result); + } + + // ---- + + private async copyChildrenOf(boardNode: AnyBoardNode, context: CopyContext): Promise { + const allSettled = await Promise.allSettled( + boardNode.children.map(async (child) => { + return { + id: child.id, + status: await this.copy(child, context), + }; + }) + ); + + const resultMap = new Map(); + allSettled.forEach((res) => { + if (res.status === 'fulfilled') { + resultMap.set(res.value.id, res.value.status); + } + }); + + const results: CopyStatus[] = []; + boardNode.children.forEach((child) => { + const childStatus = resultMap.get(child.id); + if (childStatus) { + results.push(childStatus); + } + }); + + return results; + } + + private buildSpecificProps(childrenResults: CopyStatus[]): { + id: EntityId; + createdAt: Date; + updatedAt: Date; + children: AnyBoardNode[]; + } { + return { + id: new ObjectId().toHexString(), + createdAt: new Date(), + updatedAt: new Date(), + children: childrenResults.map((r) => r.copyEntity).filter((c) => c !== undefined) as AnyBoardNode[], + }; + } +} diff --git a/apps/server/src/modules/board/service/internal/board-node-delete-hooks.service.spec.ts b/apps/server/src/modules/board/service/internal/board-node-delete-hooks.service.spec.ts new file mode 100644 index 00000000000..7134dad1ad0 --- /dev/null +++ b/apps/server/src/modules/board/service/internal/board-node-delete-hooks.service.spec.ts @@ -0,0 +1,165 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { setupEntities } from '@shared/testing'; +import { CollaborativeTextEditorService } from '@src/modules/collaborative-text-editor'; +import { FilesStorageClientAdapterService } from '@src/modules/files-storage-client'; +import { DrawingElementAdapterService } from '@src/modules/tldraw-client'; +import { ContextExternalToolService } from '@src/modules/tool/context-external-tool'; +import { contextExternalToolFactory } from '@src/modules/tool/context-external-tool/testing'; +import { + collaborativeTextEditorFactory, + drawingElementFactory, + externalToolElementFactory, + fileElementFactory, + linkElementFactory, +} from '../../testing'; +import { BoardNodeDeleteHooksService } from './board-node-delete-hooks.service'; + +describe(BoardNodeDeleteHooksService.name, () => { + let module: TestingModule; + let service: BoardNodeDeleteHooksService; + let filesStorageClientAdapterService: DeepMocked; + let drawingElementAdapterService: DeepMocked; + let contextExternalToolService: DeepMocked; + let collaborativeTextEditorService: CollaborativeTextEditorService; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + BoardNodeDeleteHooksService, + { + provide: FilesStorageClientAdapterService, + useValue: createMock(), + }, + { + provide: ContextExternalToolService, + useValue: createMock(), + }, + { + provide: DrawingElementAdapterService, + useValue: createMock(), + }, + { + provide: CollaborativeTextEditorService, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(BoardNodeDeleteHooksService); + filesStorageClientAdapterService = module.get(FilesStorageClientAdapterService); + drawingElementAdapterService = module.get(DrawingElementAdapterService); + contextExternalToolService = module.get(ContextExternalToolService); + collaborativeTextEditorService = module.get(CollaborativeTextEditorService); + + await setupEntities(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('afterDelete', () => { + describe('when called with file element', () => { + const setup = () => { + return { boardNode: fileElementFactory.build() }; + }; + + it('should delete files', async () => { + const { boardNode } = setup(); + + await service.afterDelete(boardNode); + + expect(filesStorageClientAdapterService.deleteFilesOfParent).toHaveBeenCalledWith(boardNode.id); + }); + }); + + describe('when called with link element', () => { + const setup = () => { + return { boardNode: linkElementFactory.build() }; + }; + + it('should delete files', async () => { + const { boardNode } = setup(); + + await service.afterDelete(boardNode); + + expect(filesStorageClientAdapterService.deleteFilesOfParent).toHaveBeenCalledWith(boardNode.id); + }); + }); + + describe('when called with drawing element', () => { + const setup = () => { + return { boardNode: drawingElementFactory.build() }; + }; + + it('should delete drawing data', async () => { + const { boardNode } = setup(); + + await service.afterDelete(boardNode); + + expect(drawingElementAdapterService.deleteDrawingBinData).toHaveBeenCalledWith(boardNode.id); + }); + + it('should delete files', async () => { + const { boardNode } = setup(); + + await service.afterDelete(boardNode); + + expect(filesStorageClientAdapterService.deleteFilesOfParent).toHaveBeenCalledWith(boardNode.id); + }); + }); + + describe('when called with external tool element', () => { + const setup = () => { + const tool = contextExternalToolFactory.build(); + contextExternalToolService.findById.mockResolvedValueOnce(tool); + return { boardNode: externalToolElementFactory.build(), tool }; + }; + + it('should delete linked tool', async () => { + const { boardNode, tool } = setup(); + + await service.afterDelete(boardNode); + + expect(contextExternalToolService.deleteContextExternalTool).toHaveBeenCalledWith(tool); + }); + }); + + describe('when called with collaborative text editor element', () => { + const setup = () => { + return { boardNode: collaborativeTextEditorFactory.build() }; + }; + + it('should delete editor', async () => { + const { boardNode } = setup(); + + await service.afterDelete(boardNode); + + expect(collaborativeTextEditorService.deleteCollaborativeTextEditorByParentId).toHaveBeenCalledWith( + boardNode.id + ); + }); + }); + + describe('when called with media external tool element', () => { + const setup = () => { + const tool = contextExternalToolFactory.build(); + contextExternalToolService.findById.mockResolvedValueOnce(tool); + return { boardNode: externalToolElementFactory.build(), tool }; + }; + + it('should delete linked tool', async () => { + const { boardNode, tool } = setup(); + + await service.afterDelete(boardNode); + + expect(contextExternalToolService.deleteContextExternalTool).toHaveBeenCalledWith(tool); + }); + }); + }); +}); diff --git a/apps/server/src/modules/board/service/internal/board-node-delete-hooks.service.ts b/apps/server/src/modules/board/service/internal/board-node-delete-hooks.service.ts new file mode 100644 index 00000000000..bb10e6e579e --- /dev/null +++ b/apps/server/src/modules/board/service/internal/board-node-delete-hooks.service.ts @@ -0,0 +1,96 @@ +import { Utils } from '@mikro-orm/core'; +import { CollaborativeTextEditorService } from '@modules/collaborative-text-editor'; +import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; +import { DrawingElementAdapterService } from '@modules/tldraw-client'; +import { ContextExternalToolService } from '@modules/tool/context-external-tool/service'; +import { Injectable } from '@nestjs/common'; +import { + AnyBoardNode, + CollaborativeTextEditorElement, + DrawingElement, + ExternalToolElement, + FileElement, + isCollaborativeTextEditorElement, + isDrawingElement, + isExternalToolElement, + isFileElement, + isLinkElement, + isMediaExternalToolElement, + LinkElement, + MediaExternalToolElement, +} from '../../domain'; + +@Injectable() +export class BoardNodeDeleteHooksService { + constructor( + private readonly filesStorageClientAdapterService: FilesStorageClientAdapterService, + private readonly contextExternalToolService: ContextExternalToolService, + private readonly drawingElementAdapterService: DrawingElementAdapterService, + private readonly collaborativeTextEditorService: CollaborativeTextEditorService + ) {} + + async afterDelete(boardNode: AnyBoardNode | AnyBoardNode[]): Promise { + const boardNodes = Utils.asArray(boardNode); + + await Promise.allSettled(boardNodes.map(async (bn) => this.singleAfterDelete(bn))); + } + + private async singleAfterDelete(boardNode: AnyBoardNode): Promise { + // TODO improve this e.g. using exhaustive check or discriminated union + if (isFileElement(boardNode)) { + await this.afterDeleteFileElement(boardNode); + } else if (isLinkElement(boardNode)) { + await this.afterDeleteLinkElement(boardNode); + } else if (isDrawingElement(boardNode)) { + await this.afterDeleteDrawingElement(boardNode); + } else if (isExternalToolElement(boardNode)) { + await this.afterDeleteExternalToolElement(boardNode); + } else if (isCollaborativeTextEditorElement(boardNode)) { + await this.afterDeleteCollaborativeTextEditorElement(boardNode); + } else if (isMediaExternalToolElement(boardNode)) { + await this.afterDeleteMediaExternalToolElement(boardNode); + } else { + // noop + } + await Promise.allSettled(boardNode.children.map(async (child) => this.afterDelete(child))); + } + + async afterDeleteFileElement(fileElement: FileElement): Promise { + await this.filesStorageClientAdapterService.deleteFilesOfParent(fileElement.id); + } + + async afterDeleteLinkElement(linkElement: LinkElement): Promise { + await this.filesStorageClientAdapterService.deleteFilesOfParent(linkElement.id); + } + + async afterDeleteDrawingElement(drawingElement: DrawingElement): Promise { + await this.drawingElementAdapterService.deleteDrawingBinData(drawingElement.id); + await this.filesStorageClientAdapterService.deleteFilesOfParent(drawingElement.id); + } + + async afterDeleteExternalToolElement(externalToolElement: ExternalToolElement): Promise { + if (externalToolElement.contextExternalToolId) { + const linkedTool = await this.contextExternalToolService.findById(externalToolElement.contextExternalToolId); + + if (linkedTool) { + await this.contextExternalToolService.deleteContextExternalTool(linkedTool); + } + } + } + + async afterDeleteCollaborativeTextEditorElement( + collaborativeTextEditorElement: CollaborativeTextEditorElement + ): Promise { + await this.collaborativeTextEditorService.deleteCollaborativeTextEditorByParentId( + collaborativeTextEditorElement.id + ); + } + + async afterDeleteMediaExternalToolElement(mediaElement: MediaExternalToolElement): Promise { + const linkedTool = await this.contextExternalToolService.findById(mediaElement.contextExternalToolId); + + if (linkedTool) { + await this.contextExternalToolService.deleteContextExternalTool(linkedTool); + } + } +} diff --git a/apps/server/src/modules/board/service/internal/column-board-copy.service.spec.ts b/apps/server/src/modules/board/service/internal/column-board-copy.service.spec.ts new file mode 100644 index 00000000000..95cb90c9379 --- /dev/null +++ b/apps/server/src/modules/board/service/internal/column-board-copy.service.spec.ts @@ -0,0 +1,110 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { UserService } from '@modules/user/service/user.service'; +import { Test, TestingModule } from '@nestjs/testing'; +import { CourseRepo } from '@shared/repo/course/course.repo'; +import { courseFactory, setupEntities, userDoFactory } from '@shared/testing'; +import { CopyElementType, CopyStatus, CopyStatusEnum } from '@src/modules/copy-helper'; +import { FilesStorageClientAdapterService } from '@src/modules/files-storage-client/service/files-storage-client.service'; +import { BoardExternalReferenceType, ColumnBoard } from '../../domain'; +import { columnBoardFactory } from '../../testing'; +import { BoardNodeService } from '../board-node.service'; +import { ColumnBoardCopyService } from './column-board-copy.service'; +import { ColumnBoardTitleService } from './column-board-title.service'; +// Important: Don't move the BoardNodeCopyService import up to prevent import cycle! +import { BoardNodeCopyService } from './board-node-copy.service'; + +describe(ColumnBoardCopyService.name, () => { + let module: TestingModule; + let service: ColumnBoardCopyService; + + let boardNodeService: DeepMocked; + let courseRepo: DeepMocked; + let userService: DeepMocked; + let boardNodeCopyService: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + ColumnBoardCopyService, + { + provide: BoardNodeService, + useValue: createMock(), + }, + { + provide: ColumnBoardTitleService, + useValue: createMock(), + }, + { + provide: CourseRepo, + useValue: createMock(), + }, + { + provide: UserService, + useValue: createMock(), + }, + { + provide: BoardNodeCopyService, + useValue: createMock(), + }, + { + provide: FilesStorageClientAdapterService, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(ColumnBoardCopyService); + boardNodeService = module.get(BoardNodeService); + courseRepo = module.get(CourseRepo); + userService = module.get(UserService); + boardNodeCopyService = module.get(BoardNodeCopyService); + + await setupEntities(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + const setup = () => { + const userId = new ObjectId().toHexString(); + const user = userDoFactory.build({ id: userId }); + userService.findById.mockResolvedValueOnce(user); + const course = courseFactory.buildWithId(); + courseRepo.findById.mockResolvedValueOnce(course); + const originalBoard = columnBoardFactory.build({ + context: { id: course.id, type: BoardExternalReferenceType.Course }, + }); + boardNodeService.findByClassAndId.mockResolvedValueOnce(originalBoard); + const boardCopy = columnBoardFactory.build({ + context: { id: course.id, type: BoardExternalReferenceType.Course }, + }); + const status: CopyStatus = { + copyEntity: boardCopy, + type: CopyElementType.BOARD, + status: CopyStatusEnum.SUCCESS, + }; + boardNodeCopyService.copy.mockResolvedValueOnce(status); + + return { originalBoard, userId }; + }; + + it('should copy the board', async () => { + const { originalBoard, userId } = setup(); + + const result = await service.copyColumnBoard({ + originalColumnBoardId: originalBoard.id, + destinationExternalReference: originalBoard.context, + userId, + copyTitle: 'Another Title', + }); + + expect(boardNodeCopyService.copy).toHaveBeenCalled(); + expect((result.copyEntity as ColumnBoard).title).toBe('Another Title'); + }); +}); diff --git a/apps/server/src/modules/board/service/internal/column-board-copy.service.ts b/apps/server/src/modules/board/service/internal/column-board-copy.service.ts new file mode 100644 index 00000000000..6ebe47bd942 --- /dev/null +++ b/apps/server/src/modules/board/service/internal/column-board-copy.service.ts @@ -0,0 +1,66 @@ +import { CopyStatus } from '@modules/copy-helper'; +import { UserService } from '@modules/user'; +import { Injectable, InternalServerErrorException, NotImplementedException } from '@nestjs/common'; +import { EntityId } from '@shared/domain/types'; +import { CourseRepo } from '@shared/repo'; +import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; +import { BoardExternalReference, BoardExternalReferenceType, ColumnBoard, isColumnBoard } from '../../domain'; +import { BoardNodeCopyContext } from './board-node-copy-context'; +import { BoardNodeCopyService } from './board-node-copy.service'; +import { BoardNodeService } from '../board-node.service'; +import { ColumnBoardTitleService } from './column-board-title.service'; + +@Injectable() +export class ColumnBoardCopyService { + constructor( + private readonly boardNodeService: BoardNodeService, + private readonly columnBoardTitleService: ColumnBoardTitleService, + private readonly courseRepo: CourseRepo, + private readonly userService: UserService, + private readonly boardNodeCopyService: BoardNodeCopyService, + private readonly filesStorageClientAdapterService: FilesStorageClientAdapterService + ) {} + + async copyColumnBoard(props: { + originalColumnBoardId: EntityId; + destinationExternalReference: BoardExternalReference; + userId: EntityId; + copyTitle?: string; + }): Promise { + const originalBoard = await this.boardNodeService.findByClassAndId(ColumnBoard, props.originalColumnBoardId); + + const user = await this.userService.findById(props.userId); + /* istanbul ignore next */ + if (originalBoard.context.type !== BoardExternalReferenceType.Course) { + throw new NotImplementedException('only courses are supported as board parents'); + } + const course = await this.courseRepo.findById(originalBoard.context.id); // TODO: get rid of this + + const copyContext = new BoardNodeCopyContext({ + sourceSchoolId: course.school.id, + targetSchoolId: user.schoolId, + userId: props.userId, + filesStorageClientAdapterService: this.filesStorageClientAdapterService, + }); + + const copyStatus = await this.boardNodeCopyService.copy(originalBoard, copyContext); + + /* istanbul ignore next */ + if (!isColumnBoard(copyStatus.copyEntity)) { + throw new InternalServerErrorException('expected copy of columnboard to be a columnboard'); + } + + if (props.copyTitle) { + copyStatus.copyEntity.title = props.copyTitle; + } else { + copyStatus.copyEntity.title = await this.columnBoardTitleService.deriveColumnBoardTitle( + originalBoard.title, + props.destinationExternalReference + ); + } + copyStatus.copyEntity.context = props.destinationExternalReference; + await this.boardNodeService.addRoot(copyStatus.copyEntity); + + return copyStatus; + } +} diff --git a/apps/server/src/modules/board/service/internal/column-board-link.service.spec.ts b/apps/server/src/modules/board/service/internal/column-board-link.service.spec.ts new file mode 100644 index 00000000000..aca7d8ea204 --- /dev/null +++ b/apps/server/src/modules/board/service/internal/column-board-link.service.spec.ts @@ -0,0 +1,100 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { Test, TestingModule } from '@nestjs/testing'; +import { EntityId } from '@shared/domain/types'; +import { setupEntities } from '@shared/testing'; +import { ColumnBoard, LinkElement } from '../../domain'; +import { BoardNodeRepo } from '../../repo'; +import { + cardFactory, + columnBoardFactory, + columnFactory, + linkElementFactory, + richTextElementFactory, +} from '../../testing'; +import { BoardNodeService } from '../board-node.service'; +import { ColumnBoardLinkService } from './column-board-link.service'; + +describe(ColumnBoardLinkService.name, () => { + let module: TestingModule; + let service: ColumnBoardLinkService; + let boardNodeService: DeepMocked; + let boardNodeRepo: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + ColumnBoardLinkService, + { + provide: BoardNodeService, + useValue: createMock(), + }, + { + provide: BoardNodeRepo, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(ColumnBoardLinkService); + boardNodeService = module.get(BoardNodeService); + boardNodeRepo = module.get(BoardNodeRepo); + + await setupEntities(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('swap link ids', () => { + const setup = () => { + const oldId = new ObjectId().toHexString(); + const newId = new ObjectId().toHexString(); + const idMap = new Map(); + idMap.set(oldId, newId); + + const elements = [ + richTextElementFactory.build(), + linkElementFactory.build({ url: `https://example.com/${oldId}/article` }), + ]; + const card = cardFactory.build({ children: elements }); + const column = columnFactory.build({ children: [card] }); + const board = columnBoardFactory.build({ children: [column] }); + + boardNodeService.findByClassAndId.mockResolvedValueOnce(board); + + return { board, linkElement: elements[1] as LinkElement, idMap, oldId, newId }; + }; + + it('should find the board', async () => { + const { board, idMap } = setup(); + + await service.swapLinkedIds(board.id, idMap); + + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(ColumnBoard, board.id); + }); + + describe('when board node is a link element', () => { + it('should replace ids in urls', async () => { + const { board, linkElement, idMap, newId } = setup(); + + await service.swapLinkedIds(board.id, idMap); + + expect(linkElement.url).toBe(`https://example.com/${newId}/article`); + }); + + it('should save the board', async () => { + const { board, idMap } = setup(); + + await service.swapLinkedIds(board.id, idMap); + + expect(boardNodeRepo.save).toHaveBeenCalledWith(board); + }); + }); + }); +}); diff --git a/apps/server/src/modules/board/service/internal/column-board-link.service.ts b/apps/server/src/modules/board/service/internal/column-board-link.service.ts new file mode 100644 index 00000000000..4ef8df53945 --- /dev/null +++ b/apps/server/src/modules/board/service/internal/column-board-link.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain/types'; +import { AnyBoardNode, ColumnBoard, isLinkElement } from '../../domain'; +import { BoardNodeRepo } from '../../repo/board-node.repo'; +import { BoardNodeService } from '../board-node.service'; + +@Injectable() +export class ColumnBoardLinkService { + constructor(private readonly boardNodeService: BoardNodeService, private readonly boardNodeRepo: BoardNodeRepo) {} + + async swapLinkedIds(boardId: EntityId, idMap: Map) { + const board = await this.boardNodeService.findByClassAndId(ColumnBoard, boardId); + + this.updateLinkElements(board, idMap); + await this.boardNodeRepo.save(board); + + return board; + } + + private updateLinkElements(boardNode: AnyBoardNode, idMap: Map) { + if (isLinkElement(boardNode)) { + idMap.forEach((value, key) => { + boardNode.url = boardNode.url.replace(key, value); + }); + } + boardNode.children.forEach((bn) => this.updateLinkElements(bn, idMap)); + } +} diff --git a/apps/server/src/modules/board/service/internal/column-board-reference.service.ts b/apps/server/src/modules/board/service/internal/column-board-reference.service.ts new file mode 100644 index 00000000000..8d0424938d8 --- /dev/null +++ b/apps/server/src/modules/board/service/internal/column-board-reference.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@nestjs/common'; +import { BoardExternalReference, ColumnBoard, isColumnBoard } from '../../domain'; +import { BoardNodeRepo } from '../../repo'; + +@Injectable() +export class ColumnBoardReferenceService { + constructor(private readonly boardNodeRepo: BoardNodeRepo) {} + + async findByExternalReference(reference: BoardExternalReference, depth?: number): Promise { + const boardNodes = await this.boardNodeRepo.findByExternalReference(reference, depth); + + const boards = boardNodes.filter((bn) => isColumnBoard(bn)); + + return boards as ColumnBoard[]; + } +} diff --git a/apps/server/src/modules/board/service/internal/column-board-title.service.ts b/apps/server/src/modules/board/service/internal/column-board-title.service.ts new file mode 100644 index 00000000000..e43cbda1afe --- /dev/null +++ b/apps/server/src/modules/board/service/internal/column-board-title.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common'; +import { CopyHelperService } from '@modules/copy-helper'; +import { BoardExternalReference } from '../../domain'; +import { ColumnBoardReferenceService } from './column-board-reference.service'; + +@Injectable() +export class ColumnBoardTitleService { + constructor( + private readonly columnBoardReferenceService: ColumnBoardReferenceService, + private readonly copyHelperService: CopyHelperService + ) {} + + async deriveColumnBoardTitle( + originalTitle: string, + destinationExternalReference: BoardExternalReference + ): Promise { + const existingBoards = await this.columnBoardReferenceService.findByExternalReference( + destinationExternalReference, + 0 + ); + const existingTitles = existingBoards.map((board) => board.title); + const copyName = this.copyHelperService.deriveCopyName(originalTitle, Object.values(existingTitles)); + + return copyName; + } +} diff --git a/apps/server/src/modules/board/service/internal/content-element-update.service.spec.ts b/apps/server/src/modules/board/service/internal/content-element-update.service.spec.ts new file mode 100644 index 00000000000..7709cea2a1d --- /dev/null +++ b/apps/server/src/modules/board/service/internal/content-element-update.service.spec.ts @@ -0,0 +1,139 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { InputFormat } from '@shared/domain/types'; +import { ContentElementUpdateService } from './content-element-update.service'; +import { + FileContentBody, + LinkContentBody, + RichTextContentBody, + DrawingContentBody, + SubmissionContainerContentBody, + ExternalToolContentBody, +} from '../../controller/dto'; +import { BoardNodeRepo } from '../../repo'; + +import { + drawingElementFactory, + externalToolElementFactory, + fileElementFactory, + linkElementFactory, + richTextElementFactory, + submissionContainerElementFactory, +} from '../../testing'; + +describe('ContentElementUpdateService', () => { + let module: TestingModule; + let service: ContentElementUpdateService; + let repo: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + ContentElementUpdateService, + { + provide: BoardNodeRepo, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(ContentElementUpdateService); + repo = module.get(BoardNodeRepo); + }); + + afterAll(async () => { + await module.close(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should update FileElement', async () => { + const element = fileElementFactory.build(); + const content = new FileContentBody(); + content.caption = 'caption'; + content.alternativeText = 'alternativeText'; + + await service.updateContent(element, content); + + expect(element.caption).toBe('caption'); + expect(element.alternativeText).toBe('alternativeText'); + expect(repo.save).toHaveBeenCalledWith(element); + }); + + it('should update LinkElement', async () => { + const element = linkElementFactory.build(); + const content = new LinkContentBody(); + content.url = 'http://example.com/'; + content.title = 'title'; + content.description = 'description'; + content.imageUrl = 'relative-image.jpg'; + + await service.updateContent(element, content); + + expect(element.url).toBe('http://example.com/'); + expect(element.title).toBe('title'); + expect(element.description).toBe('description'); + expect(element.imageUrl).toBe('relative-image.jpg'); + expect(repo.save).toHaveBeenCalledWith(element); + }); + + it('should update RichTextElement', async () => { + const element = richTextElementFactory.build(); + const content = new RichTextContentBody(); + content.text = 'text'; + content.inputFormat = InputFormat.PLAIN_TEXT; + + await service.updateContent(element, content); + + expect(element.text).toBe('text'); + expect(element.inputFormat).toBe(InputFormat.PLAIN_TEXT); + expect(repo.save).toHaveBeenCalledWith(element); + }); + + it('should update DrawingElement', async () => { + const element = drawingElementFactory.build(); + const content = new DrawingContentBody(); + content.description = 'description'; + + await service.updateContent(element, content); + + expect(element.description).toBe('description'); + expect(repo.save).toHaveBeenCalledWith(element); + }); + + it('should update SubmissionContainerElement', async () => { + const element = submissionContainerElementFactory.build(); + const content = new SubmissionContainerContentBody(); + content.dueDate = new Date(); + + await service.updateContent(element, content); + + expect(element.dueDate).toEqual(content.dueDate); + expect(repo.save).toHaveBeenCalledWith(element); + }); + + it('should update ExternalToolElement', async () => { + const element = externalToolElementFactory.build(); + const content = new ExternalToolContentBody(); + content.contextExternalToolId = 'contextExternalToolId'; + + await service.updateContent(element, content); + + expect(element.contextExternalToolId).toBe('contextExternalToolId'); + expect(repo.save).toHaveBeenCalledWith(element); + }); + + it('should throw error for unknown element type', async () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const element = {} as any; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const content = {} as any; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + await expect(service.updateContent(element, content)).rejects.toThrowError( + "Cannot update element of type: 'Object'" + ); + }); +}); diff --git a/apps/server/src/modules/board/service/internal/content-element-update.service.ts b/apps/server/src/modules/board/service/internal/content-element-update.service.ts new file mode 100644 index 00000000000..085078f9696 --- /dev/null +++ b/apps/server/src/modules/board/service/internal/content-element-update.service.ts @@ -0,0 +1,98 @@ +import { Injectable } from '@nestjs/common'; +import { sanitizeRichText } from '@shared/controller'; +import { InputFormat } from '@shared/domain/types'; +import { + AnyElementContentBody, + DrawingContentBody, + ExternalToolContentBody, + FileContentBody, + LinkContentBody, + RichTextContentBody, + SubmissionContainerContentBody, +} from '../../controller/dto'; +import { + AnyContentElement, + DrawingElement, + ExternalToolElement, + FileElement, + isDrawingElement, + isExternalToolElement, + isFileElement, + isLinkElement, + isRichTextElement, + isSubmissionContainerElement, + LinkElement, + RichTextElement, + SubmissionContainerElement, +} from '../../domain'; +import { BoardNodeRepo } from '../../repo'; + +@Injectable() +export class ContentElementUpdateService { + constructor(private readonly boardNodeRepo: BoardNodeRepo) {} + + async updateContent(element: AnyContentElement, content: AnyElementContentBody): Promise { + // TODO refactor if ... else to e.g. discriminated union or non-exhaustive check + if (isFileElement(element) && content instanceof FileContentBody) { + this.updateFileElement(element, content); + } else if (isLinkElement(element) && content instanceof LinkContentBody) { + this.updateLinkElement(element, content); + } else if (isRichTextElement(element) && content instanceof RichTextContentBody) { + this.updateRichTextElement(element, content); + } else if (isDrawingElement(element) && content instanceof DrawingContentBody) { + this.updateDrawingElement(element, content); + } else if (isSubmissionContainerElement(element) && content instanceof SubmissionContainerContentBody) { + this.updateSubmissionContainerElement(element, content); + } else if (isExternalToolElement(element) && content instanceof ExternalToolContentBody) { + this.updateExternalToolElement(element, content); + } else { + throw new Error(`Cannot update element of type: '${element.constructor.name}'`); + } + + await this.boardNodeRepo.save(element); + } + + updateFileElement(element: FileElement, content: FileContentBody): void { + element.caption = sanitizeRichText(content.caption, InputFormat.PLAIN_TEXT); + element.alternativeText = sanitizeRichText(content.alternativeText, InputFormat.PLAIN_TEXT); + } + + updateLinkElement(element: LinkElement, content: LinkContentBody): void { + element.url = new URL(content.url).toString(); + element.title = content.title ?? ''; + element.description = content.description ?? ''; + if (content.imageUrl) { + const isRelativeUrl = (url: string) => { + const fallbackHostname = 'https://www.fallback-url-if-url-is-relative.org'; + const imageUrlObject = new URL(url, fallbackHostname); + return imageUrlObject.origin === fallbackHostname; + }; + + if (isRelativeUrl(content.imageUrl)) { + element.imageUrl = content.imageUrl; + } + } + } + + updateRichTextElement(element: RichTextElement, content: RichTextContentBody): void { + element.text = sanitizeRichText(content.text, content.inputFormat); + element.inputFormat = content.inputFormat; + } + + updateDrawingElement(element: DrawingElement, content: DrawingContentBody): void { + element.description = content.description; + } + + updateSubmissionContainerElement(element: SubmissionContainerElement, content: SubmissionContainerContentBody): void { + if (content.dueDate !== undefined) { + element.dueDate = content.dueDate; + } + } + + updateExternalToolElement(element: ExternalToolElement, content: ExternalToolContentBody): void { + if (content.contextExternalToolId !== undefined) { + // Updates should not remove an existing reference to a tool, to prevent orphan tool instances + element.contextExternalToolId = content.contextExternalToolId; + } + } +} diff --git a/apps/server/src/modules/board/service/internal/index.ts b/apps/server/src/modules/board/service/internal/index.ts new file mode 100644 index 00000000000..bd082ac1702 --- /dev/null +++ b/apps/server/src/modules/board/service/internal/index.ts @@ -0,0 +1,9 @@ +export * from './board-context.service'; +export * from './board-node-copy-context'; +export * from './board-node-copy.service'; +export * from './board-node-delete-hooks.service'; +export * from './column-board-copy.service'; +export * from './column-board-link.service'; +export * from './column-board-reference.service'; +export * from './column-board-title.service'; +export * from './content-element-update.service'; diff --git a/apps/server/src/modules/board/service/media-board/index.ts b/apps/server/src/modules/board/service/media-board/index.ts index 299545cba0f..a65204b1373 100644 --- a/apps/server/src/modules/board/service/media-board/index.ts +++ b/apps/server/src/modules/board/service/media-board/index.ts @@ -1,4 +1,2 @@ -export { MediaLineService } from './media-line.service'; export { MediaBoardService } from './media-board.service'; -export { MediaElementService } from './media-element.service'; export { MediaAvailableLineService } from './media-available-line.service'; diff --git a/apps/server/src/modules/board/service/media-board/media-available-line.service.spec.ts b/apps/server/src/modules/board/service/media-board/media-available-line.service.spec.ts index 998a0eade26..357478eddcc 100644 --- a/apps/server/src/modules/board/service/media-board/media-available-line.service.spec.ts +++ b/apps/server/src/modules/board/service/media-board/media-available-line.service.spec.ts @@ -12,10 +12,13 @@ import { SchoolExternalToolService } from '@modules/tool/school-external-tool'; import { SchoolExternalTool } from '@modules/tool/school-external-tool/domain'; import { schoolExternalToolFactory } from '@modules/tool/school-external-tool/testing'; import { Test, TestingModule } from '@nestjs/testing'; -import { MediaAvailableLine, MediaBoard, MediaExternalToolElement, Page } from '@shared/domain/domainobject'; import { User } from '@shared/domain/entity'; -import { mediaBoardFactory, mediaExternalToolElementFactory, setupEntities, userFactory } from '@shared/testing'; +import { setupEntities, userFactory } from '@shared/testing'; +import { mediaBoardFactory, mediaExternalToolElementFactory } from '@modules/board/testing'; +import { Page } from '@shared/domain/domainobject'; +import { MediaAvailableLine, MediaBoard, MediaExternalToolElement } from '../../domain'; import { MediaAvailableLineService } from './media-available-line.service'; +import { MediaBoardService } from './media-board.service'; describe(MediaAvailableLineService.name, () => { let module: TestingModule; @@ -25,6 +28,7 @@ describe(MediaAvailableLineService.name, () => { let schoolExternalToolService: DeepMocked; let contextExternalToolService: DeepMocked; let externalToolLogoService: DeepMocked; + let mediaBoardService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -46,6 +50,10 @@ describe(MediaAvailableLineService.name, () => { provide: ExternalToolLogoService, useValue: createMock(), }, + { + provide: MediaBoardService, + useValue: createMock(), + }, ], }).compile(); @@ -54,6 +62,7 @@ describe(MediaAvailableLineService.name, () => { schoolExternalToolService = module.get(SchoolExternalToolService); contextExternalToolService = module.get(ContextExternalToolService); externalToolLogoService = module.get(ExternalToolLogoService); + mediaBoardService = module.get(MediaBoardService); await setupEntities(); }); @@ -85,14 +94,16 @@ describe(MediaAvailableLineService.name, () => { const mediaExternalToolElement: MediaExternalToolElement = mediaExternalToolElementFactory.build({ contextExternalToolId: usedContextExternalTool.id, }); - const board: MediaBoard = mediaBoardFactory.addChild(mediaExternalToolElement).build(); + const board: MediaBoard = mediaBoardFactory.build({ children: [mediaExternalToolElement] }); - schoolExternalToolService.findSchoolExternalTools.mockResolvedValue([ + schoolExternalToolService.findSchoolExternalTools.mockResolvedValueOnce([ schoolExternalTool, usedSchoolExternalTool, ]); contextExternalToolService.findById.mockResolvedValueOnce(usedContextExternalTool); + mediaBoardService.findMediaElements.mockReturnValueOnce([mediaExternalToolElement]); + return { user, board, mediaExternalToolElement, schoolExternalTool }; }; @@ -152,12 +163,12 @@ describe(MediaAvailableLineService.name, () => { contextExternalToolId: new ObjectId().toHexString(), } ); - const board: MediaBoard = mediaBoardFactory - .addChild(mediaExternalToolElement) - .addChild(mediaExternalToolElementWithDeletedTool) - .build(); + const board: MediaBoard = mediaBoardFactory.build({ + children: [mediaExternalToolElement, mediaExternalToolElementWithDeletedTool], + }); + mediaBoardService.findMediaElements.mockReturnValueOnce([mediaExternalToolElement]); - schoolExternalToolService.findSchoolExternalTools.mockResolvedValue([ + schoolExternalToolService.findSchoolExternalTools.mockResolvedValueOnce([ schoolExternalTool, usedSchoolExternalTool, ]); @@ -370,8 +381,8 @@ describe(MediaAvailableLineService.name, () => { logoUrl: undefined, }, ], - backgroundColor: mediaBoard.mediaAvailableLineBackgroundColor, - collapsed: mediaBoard.mediaAvailableLineCollapsed, + backgroundColor: mediaBoard.backgroundColor, + collapsed: mediaBoard.collapsed, }); }); }); diff --git a/apps/server/src/modules/board/service/media-board/media-available-line.service.ts b/apps/server/src/modules/board/service/media-board/media-available-line.service.ts index 855ae0ee8ea..19290b70987 100644 --- a/apps/server/src/modules/board/service/media-board/media-available-line.service.ts +++ b/apps/server/src/modules/board/service/media-board/media-available-line.service.ts @@ -6,14 +6,10 @@ import { ExternalToolLogoService, ExternalToolService } from '@modules/tool/exte import { SchoolExternalToolService } from '@modules/tool/school-external-tool'; import { SchoolExternalTool } from '@modules/tool/school-external-tool/domain'; import { Injectable } from '@nestjs/common'; -import { - MediaAvailableLine, - MediaAvailableLineElement, - MediaBoard, - MediaExternalToolElement, - Page, -} from '@shared/domain/domainobject'; +import { Page } from '@shared/domain/domainobject'; import { User } from '@shared/domain/entity'; +import { MediaAvailableLine, MediaAvailableLineElement, MediaBoard, MediaExternalToolElement } from '../../domain'; +import { MediaBoardService } from './media-board.service'; @Injectable() export class MediaAvailableLineService { @@ -21,7 +17,8 @@ export class MediaAvailableLineService { private readonly externalToolService: ExternalToolService, private readonly schoolExternalToolService: SchoolExternalToolService, private readonly contextExternalToolService: ContextExternalToolService, - private readonly externalToolLogoService: ExternalToolLogoService + private readonly externalToolLogoService: ExternalToolLogoService, + private readonly mediaBoardService: MediaBoardService ) {} public async getUnusedAvailableSchoolExternalTools(user: User, board: MediaBoard): Promise { @@ -92,8 +89,8 @@ export class MediaAvailableLineService { } private async getContextExternalToolsByBoard(board: MediaBoard): Promise { - const contextExternalTools: Promise[] = board - .getChildrenOfType(MediaExternalToolElement) + const contextExternalTools: Promise[] = this.mediaBoardService + .findMediaElements(board) .map((element: MediaExternalToolElement) => this.contextExternalToolService.findById(element.contextExternalToolId) ); @@ -132,8 +129,8 @@ export class MediaAvailableLineService { const line: MediaAvailableLine = new MediaAvailableLine({ elements: lineElements, - backgroundColor: mediaBoard.mediaAvailableLineBackgroundColor, - collapsed: mediaBoard.mediaAvailableLineCollapsed, + backgroundColor: mediaBoard.backgroundColor, + collapsed: mediaBoard.collapsed, }); return line; diff --git a/apps/server/src/modules/board/service/media-board/media-board.service.spec.ts b/apps/server/src/modules/board/service/media-board/media-board.service.spec.ts index 7314ff8ca27..424fceea009 100644 --- a/apps/server/src/modules/board/service/media-board/media-board.service.spec.ts +++ b/apps/server/src/modules/board/service/media-board/media-board.service.spec.ts @@ -1,299 +1,163 @@ -import { createMock, type DeepMocked } from '@golevelup/ts-jest'; -import { ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardExternalReferenceType, MediaBoard } from '@shared/domain/domainobject'; -import { columnBoardFactory, mediaBoardFactory, mediaLineFactory } from '@shared/testing'; -import { MediaBoardColors, MediaBoardLayoutType } from '../../domain'; -import { InvalidBoardTypeLoggableException } from '../../loggable'; -import { BoardDoRepo } from '../../repo'; -import { BoardDoService } from '../board-do.service'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ContextExternalToolService } from '@modules/tool/context-external-tool/service'; +import { schoolExternalToolFactory } from '@modules/tool/school-external-tool/testing'; +import { contextExternalToolFactory } from '@modules/tool/context-external-tool/testing'; +import { mediaBoardFactory, mediaExternalToolElementFactory, mediaLineFactory } from '../../testing'; +import { BoardExternalReference, BoardExternalReferenceType, BoardLayout, MediaBoardColors } from '../../domain'; +import { BoardNodeRepo } from '../../repo'; import { MediaBoardService } from './media-board.service'; -describe(MediaBoardService.name, () => { +describe('MediaBoardService', () => { let module: TestingModule; let service: MediaBoardService; - - let boardDoRepo: DeepMocked; - let boardDoService: DeepMocked; + let boardNodeRepo: DeepMocked; + let contextExternalToolService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ providers: [ MediaBoardService, { - provide: BoardDoRepo, - useValue: createMock(), + provide: BoardNodeRepo, + useValue: createMock(), }, { - provide: BoardDoService, - useValue: createMock(), + provide: ContextExternalToolService, + useValue: createMock(), }, ], }).compile(); - service = module.get(MediaBoardService); - boardDoRepo = module.get(BoardDoRepo); - boardDoService = module.get(BoardDoService); + service = module.get(MediaBoardService); + boardNodeRepo = module.get(BoardNodeRepo); + contextExternalToolService = module.get(ContextExternalToolService); }); afterAll(async () => { await module.close(); }); - afterEach(() => { + beforeEach(() => { jest.resetAllMocks(); }); - describe('findById', () => { - describe('when a board with the id exists', () => { - const setup = () => { - const board = mediaBoardFactory.build(); - - boardDoRepo.findByClassAndId.mockResolvedValueOnce(board); - - return { - board, - }; - }; - - it('should return the board', async () => { - const { board } = setup(); - - const result = await service.findById(board.id); - - expect(result).toEqual(board); - }); + describe('findByExternalReference', () => { + const setup = () => { + const mediaBoard = mediaBoardFactory.build(); + boardNodeRepo.findByExternalReference.mockResolvedValueOnce([mediaBoard]); + return { mediaBoard }; + }; + it('should call boardNodeRepo.findByExternalReference', async () => { + const reference: BoardExternalReference = { type: BoardExternalReferenceType.User, id: 'id' }; + await service.findByExternalReference(reference); + expect(boardNodeRepo.findByExternalReference).toHaveBeenCalledWith(reference); }); - }); - - describe('findIdsByExternalReference', () => { - describe('when a board for the context exists', () => { - const setup = () => { - const boardId = new ObjectId().toHexString(); - const userId = new ObjectId().toHexString(); - - boardDoRepo.findIdsByExternalReference.mockResolvedValueOnce([boardId]); - - return { - boardId, - userId, - }; - }; - - it('should return the board id', async () => { - const { boardId, userId } = setup(); - - const result = await service.findIdsByExternalReference({ type: BoardExternalReferenceType.User, id: userId }); - - expect(result).toEqual([boardId]); - }); - }); - }); - - describe('create', () => { - describe('when creating a new board', () => { - const setup = () => { - const userId = new ObjectId().toHexString(); - - return { - userId, - }; - }; - - it('should return the board', async () => { - const { userId } = setup(); - - const result = await service.create({ type: BoardExternalReferenceType.User, id: userId }); - - expect(result).toEqual( - expect.objectContaining({ - id: expect.any(String), - children: [], - createdAt: expect.any(Date), - updatedAt: expect.any(Date), - context: { - type: BoardExternalReferenceType.User, - id: userId, - }, - layout: MediaBoardLayoutType.LIST, - mediaAvailableLineBackgroundColor: MediaBoardColors.TRANSPARENT, - mediaAvailableLineCollapsed: false, - }) - ); - }); - - it('should save the new board', async () => { - const { userId } = setup(); - - const result = await service.create({ type: BoardExternalReferenceType.User, id: userId }); - - expect(boardDoRepo.save).toHaveBeenCalledWith(result); - }); + it('should return MediaBoard', async () => { + const { mediaBoard } = setup(); + const reference: BoardExternalReference = { type: BoardExternalReferenceType.User, id: 'id' }; + const result = await service.findByExternalReference(reference); + expect(result).toEqual([mediaBoard]); }); }); - describe('updateAvailableLineColor', () => { - describe('when changing the color of the available line', () => { + describe('checkElementExists', () => { + describe('when element does not exist', () => { const setup = () => { - const board = mediaBoardFactory.build({ - mediaAvailableLineBackgroundColor: MediaBoardColors.TRANSPARENT, + const schoolExternalTool = schoolExternalToolFactory.build(); + const contextExternalTool = contextExternalToolFactory.build({ schoolToolRef: schoolExternalTool }); + const mediaExternalToolElement = mediaExternalToolElementFactory.build({ + contextExternalToolId: contextExternalTool.id, }); + const mediaLine = mediaLineFactory.build({ children: [mediaExternalToolElement] }); + const mediaBoard = mediaBoardFactory.build({ children: [mediaLine] }); - boardDoService.getRootBoardDo.mockResolvedValueOnce(board); + contextExternalToolService.findContextExternalTools.mockResolvedValueOnce([]); - return { - board, - }; + return { schoolExternalTool, mediaBoard }; }; - - it('should set the color of the line', async () => { - const { board } = setup(); - - await service.updateAvailableLineColor(board, MediaBoardColors.RED); - - expect(boardDoRepo.save).toHaveBeenCalledWith( - new MediaBoard({ ...board.getProps(), mediaAvailableLineBackgroundColor: MediaBoardColors.RED }) - ); - }); - }); - }); - - describe('collapseAvailableLine', () => { - describe('when changing the visibility of the available line', () => { - const setup = () => { - const board = mediaBoardFactory.build({ - mediaAvailableLineCollapsed: false, + it('should call findContextExternalTools', async () => { + const schoolExternalTool = schoolExternalToolFactory.build(); + const mediaBoard = mediaBoardFactory.build(); + contextExternalToolService.findContextExternalTools.mockResolvedValueOnce([]); + await service.checkElementExists(mediaBoard, schoolExternalTool); + expect(contextExternalToolService.findContextExternalTools).toHaveBeenCalledWith({ + schoolToolRef: { schoolToolId: schoolExternalTool.id }, }); - - boardDoService.getRootBoardDo.mockResolvedValueOnce(board); - - return { - board, - }; - }; - - it('should set the visibility of the line', async () => { - const { board } = setup(); - - await service.collapseAvailableLine(board, true); - - expect(boardDoRepo.save).toHaveBeenCalledWith( - new MediaBoard({ ...board.getProps(), mediaAvailableLineCollapsed: true }) - ); + }); + it('should return false if element does not exist in MediaBoard', async () => { + const { schoolExternalTool, mediaBoard } = setup(); + const exists = await service.checkElementExists(mediaBoard, schoolExternalTool); + expect(exists).toBe(false); }); }); - }); - describe('setLayout', () => { - describe('when changing the layout of the board', () => { + describe('when element exists', () => { const setup = () => { - const board = mediaBoardFactory.build({ - layout: MediaBoardLayoutType.LIST, + const schoolExternalTool = schoolExternalToolFactory.build(); + const contextExternalTool = contextExternalToolFactory.build({ schoolToolRef: schoolExternalTool }); + const mediaExternalToolElement = mediaExternalToolElementFactory.build({ + contextExternalToolId: contextExternalTool.id, }); + const mediaLine = mediaLineFactory.build({ children: [mediaExternalToolElement] }); + const mediaBoard = mediaBoardFactory.build({ children: [mediaLine] }); - boardDoService.getRootBoardDo.mockResolvedValueOnce(board); + contextExternalToolService.findContextExternalTools.mockResolvedValueOnce([contextExternalTool]); - return { - board, - }; + return { schoolExternalTool, mediaBoard }; }; - - it('should set the layout of the board', async () => { - const { board } = setup(); - - await service.setLayout(board, MediaBoardLayoutType.GRID); - - expect(boardDoRepo.save).toHaveBeenCalledWith( - new MediaBoard({ ...board.getProps(), layout: MediaBoardLayoutType.GRID }) - ); + it('should return true if element exists in MediaBoard', async () => { + const { schoolExternalTool, mediaBoard } = setup(); + const exists = await service.checkElementExists(mediaBoard, schoolExternalTool); + expect(exists).toBe(true); }); }); }); - describe('delete', () => { - describe('when deleting a board', () => { - const setup = () => { - const board = mediaBoardFactory.build(); - - return { - board, - }; - }; - - it('should delete the board', async () => { - const { board } = setup(); - - await service.delete(board); - - expect(boardDoService.deleteWithDescendants).toHaveBeenCalledWith(board); - }); + describe('createContextExternalToolForMediaBoard', () => { + const setup = () => { + const mediaBoard = mediaBoardFactory.build(); + const schoolExternalTool = schoolExternalToolFactory.build(); + const contextExternalTool = contextExternalToolFactory.build(); + contextExternalToolService.saveContextExternalTool.mockResolvedValue(contextExternalTool); + return { mediaBoard, schoolExternalTool, contextExternalTool }; + }; + it('should call saveContextExternalTool', async () => { + const { mediaBoard, schoolExternalTool } = setup(); + await service.createContextExternalToolForMediaBoard('1', schoolExternalTool, mediaBoard); + expect(contextExternalToolService.saveContextExternalTool).toHaveBeenCalled(); }); - }); - - describe('deleteByExternalReference', () => { - describe('when deleting a board', () => { - const setup = () => { - const userId = new ObjectId().toHexString(); - - return { - userId, - }; - }; - - it('should delete the board', async () => { - const { userId } = setup(); - - await service.deleteByExternalReference({ type: BoardExternalReferenceType.User, id: userId }); - - expect(boardDoRepo.deleteByExternalReference).toHaveBeenCalledWith({ - type: BoardExternalReferenceType.User, - id: userId, - }); - }); + it('should return contextExternalTool', async () => { + const { mediaBoard, schoolExternalTool, contextExternalTool } = setup(); + const result = await service.createContextExternalToolForMediaBoard('1', schoolExternalTool, mediaBoard); + expect(result).toBe(contextExternalTool); }); }); - describe('findByDescendant', () => { - describe('when a board for a descendant exists', () => { - const setup = () => { - const board = mediaBoardFactory.build(); - const line = mediaLineFactory.build(); - - boardDoService.getRootBoardDo.mockResolvedValueOnce(board); - - return { - board, - line, - }; - }; - - it('should return the board', async () => { - const { board, line } = setup(); - - const result = await service.findByDescendant(line); - - expect(result).toEqual(board); - }); + describe('update functions', () => { + const setup = () => { + const mediaBoard = mediaBoardFactory.build(); + return { mediaBoard }; + }; + it('should update MediaBoard backgroundColor', async () => { + const { mediaBoard } = setup(); + await service.updateBackgroundColor(mediaBoard, MediaBoardColors.TRANSPARENT); + expect(mediaBoard.backgroundColor).toBe(MediaBoardColors.TRANSPARENT); + expect(boardNodeRepo.save).toHaveBeenCalledWith(mediaBoard); }); - describe('when the board does not have the correct type', () => { - const setup = () => { - const board = columnBoardFactory.build(); - const line = mediaLineFactory.build(); - - boardDoService.getRootBoardDo.mockResolvedValueOnce(board); - - return { - board, - line, - }; - }; - - it('should throw an exception', async () => { - const { line } = setup(); + it('should update MediaBoard collapsed state', async () => { + const { mediaBoard } = setup(); + await service.updateCollapsed(mediaBoard, true); + expect(mediaBoard.collapsed).toBe(true); + expect(boardNodeRepo.save).toHaveBeenCalledWith(mediaBoard); + }); - await expect(service.findByDescendant(line)).rejects.toThrow(InvalidBoardTypeLoggableException); - }); + it('should update MediaBoard layout', async () => { + const { mediaBoard } = setup(); + await service.updateLayout(mediaBoard, BoardLayout.GRID); + expect(mediaBoard.layout).toBe(BoardLayout.GRID); + expect(boardNodeRepo.save).toHaveBeenCalledWith(mediaBoard); }); }); }); diff --git a/apps/server/src/modules/board/service/media-board/media-board.service.ts b/apps/server/src/modules/board/service/media-board/media-board.service.ts index 90603b73931..7949aeb2f5f 100644 --- a/apps/server/src/modules/board/service/media-board/media-board.service.ts +++ b/apps/server/src/modules/board/service/media-board/media-board.service.ts @@ -1,79 +1,103 @@ -import { ObjectId } from '@mikro-orm/mongodb'; -import type { AuthorizationLoaderServiceGeneric } from '@modules/authorization'; import { Injectable } from '@nestjs/common'; -import { AnyBoardDo, BoardExternalReference, ColumnBoard, MediaBoard } from '@shared/domain/domainobject'; -import type { EntityId } from '@shared/domain/types'; -import { MediaBoardColors, MediaBoardLayoutType } from '../../domain'; -import { InvalidBoardTypeLoggableException } from '../../loggable'; -import { BoardDoRepo } from '../../repo'; -import { BoardDoService } from '../board-do.service'; +import { EntityId } from '@shared/domain/types'; -@Injectable() -export class MediaBoardService implements AuthorizationLoaderServiceGeneric { - constructor(private readonly boardDoRepo: BoardDoRepo, private readonly boardDoService: BoardDoService) {} +import { ToolContextType } from '@modules/tool/common/enum'; +import { ContextExternalToolService } from '@modules/tool/context-external-tool/service'; +import { ContextExternalTool, ContextRef } from '@modules/tool/context-external-tool/domain'; +import { SchoolExternalTool, SchoolExternalToolRef } from '@modules/tool/school-external-tool/domain'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { + AnyMediaBoardNode, + BoardExternalReference, + isMediaBoard, + isMediaExternalToolElement, + MediaBoard, + MediaExternalToolElement, +} from '../../domain'; +import { BoardNodeRepo } from '../../repo'; + +type WithLayout = Extract; +type WithCollapsed = Extract; +type WithBackgroundColor = Extract; - public async findById(boardId: EntityId): Promise { - const board: MediaBoard = await this.boardDoRepo.findByClassAndId(MediaBoard, boardId); +@Injectable() +export class MediaBoardService { + constructor( + private readonly boardNodeRepo: BoardNodeRepo, + private readonly contextExternalToolService: ContextExternalToolService + ) {} - return board; - } + async findByExternalReference(reference: BoardExternalReference): Promise { + const boardNodes = await this.boardNodeRepo.findByExternalReference(reference); - public async findIdsByExternalReference(reference: BoardExternalReference): Promise { - const ids: EntityId[] = await this.boardDoRepo.findIdsByExternalReference(reference); + const boards = boardNodes.filter((bn): bn is MediaBoard => isMediaBoard(bn)); - return ids; + return boards; } - public async findByDescendant(descendant: AnyBoardDo): Promise { - const mediaBoard: MediaBoard | ColumnBoard = await this.boardDoService.getRootBoardDo(descendant); - - if (!(mediaBoard instanceof MediaBoard)) { - throw new InvalidBoardTypeLoggableException(MediaBoard, mediaBoard.id); - } - - return mediaBoard; + public async createContextExternalToolForMediaBoard( + schoolId: EntityId, + schoolExternalTool: SchoolExternalTool, + mediaBoard: MediaBoard + ): Promise { + const contextExternalTool: ContextExternalTool = await this.contextExternalToolService.saveContextExternalTool( + new ContextExternalTool({ + id: new ObjectId().toHexString(), + schoolToolRef: new SchoolExternalToolRef({ schoolId, schoolToolId: schoolExternalTool.id }), + contextRef: new ContextRef({ id: mediaBoard.id, type: ToolContextType.MEDIA_BOARD }), + parameters: [], + }) + ); + + return contextExternalTool; } - public async create(context: BoardExternalReference): Promise { - const mediaBoard: MediaBoard = new MediaBoard({ - id: new ObjectId().toHexString(), - children: [], - createdAt: new Date(), - updatedAt: new Date(), - context, - layout: MediaBoardLayoutType.LIST, - mediaAvailableLineBackgroundColor: MediaBoardColors.TRANSPARENT, - mediaAvailableLineCollapsed: false, + public async checkElementExists(mediaBoard: MediaBoard, schoolExternalTool: SchoolExternalTool): Promise { + const contextExternalTools: ContextExternalTool[] = await this.contextExternalToolService.findContextExternalTools({ + schoolToolRef: { schoolToolId: schoolExternalTool.id }, }); - await this.boardDoRepo.save(mediaBoard); + const existing = this.findMediaElements(mediaBoard); + + const exists = existing.some((element) => + contextExternalTools.some((tool) => tool.id === element.contextExternalToolId) + ); - return mediaBoard; + return exists; } - public async updateAvailableLineColor(mediaBoard: MediaBoard, color: MediaBoardColors): Promise { - mediaBoard.mediaAvailableLineBackgroundColor = color; + public findMediaElements(boardNode: AnyMediaBoardNode): MediaExternalToolElement[] { + const elements = boardNode.children.reduce((result: MediaExternalToolElement[], bn) => { + result.push(...this.findMediaElements(bn as AnyMediaBoardNode)); - await this.boardDoRepo.save(mediaBoard); - } + if (isMediaExternalToolElement(bn)) { + result.push(bn); + } - public async collapseAvailableLine(mediaBoard: MediaBoard, mediaAvailableLineCollapsed: boolean): Promise { - mediaBoard.mediaAvailableLineCollapsed = mediaAvailableLineCollapsed; + return result; + }, []); - await this.boardDoRepo.save(mediaBoard); + return elements; } - public async setLayout(mediaBoard: MediaBoard, layout: MediaBoardLayoutType): Promise { - mediaBoard.layout = layout; + public async updateBackgroundColor>( + node: T, + backgroundColor: T['backgroundColor'] + ) { + node.backgroundColor = backgroundColor; - await this.boardDoRepo.save(mediaBoard); + await this.boardNodeRepo.save(node); } - public async delete(board: MediaBoard): Promise { - await this.boardDoService.deleteWithDescendants(board); + public async updateCollapsed>(node: T, collapsed: T['collapsed']) { + node.collapsed = collapsed; + + await this.boardNodeRepo.save(node); } - public async deleteByExternalReference(reference: BoardExternalReference): Promise { - return this.boardDoRepo.deleteByExternalReference(reference); + public async updateLayout>(node: T, layout: T['layout']) { + node.layout = layout; + + await this.boardNodeRepo.save(node); } } diff --git a/apps/server/src/modules/board/service/media-board/media-element.service.spec.ts b/apps/server/src/modules/board/service/media-board/media-element.service.spec.ts deleted file mode 100644 index 51acfc7f9c2..00000000000 --- a/apps/server/src/modules/board/service/media-board/media-element.service.spec.ts +++ /dev/null @@ -1,308 +0,0 @@ -import { createMock, type DeepMocked } from '@golevelup/ts-jest'; -import { ObjectId } from '@mikro-orm/mongodb'; -import { ToolContextType } from '@modules/tool/common/enum'; -import { ContextExternalTool } from '@modules/tool/context-external-tool/domain'; -import { ContextExternalToolService } from '@modules/tool/context-external-tool/service'; -import { contextExternalToolFactory } from '@modules/tool/context-external-tool/testing'; -import { SchoolExternalTool } from '@modules/tool/school-external-tool/domain'; -import { schoolExternalToolFactory } from '@modules/tool/school-external-tool/testing'; -import { NotFoundException } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import { - mediaBoardFactory, - mediaExternalToolElementFactory, - mediaLineFactory, - setupEntities, - userFactory, -} from '@shared/testing'; -import { BoardDoRepo } from '../../repo'; -import { BoardDoService } from '../board-do.service'; -import { MediaElementService } from './media-element.service'; - -describe(MediaElementService.name, () => { - let module: TestingModule; - let service: MediaElementService; - - let boardDoRepo: DeepMocked; - let boardDoService: DeepMocked; - let contextExternalToolService: DeepMocked; - - beforeAll(async () => { - module = await Test.createTestingModule({ - providers: [ - MediaElementService, - { - provide: BoardDoRepo, - useValue: createMock(), - }, - { - provide: BoardDoService, - useValue: createMock(), - }, - { - provide: ContextExternalToolService, - useValue: createMock(), - }, - ], - }).compile(); - - service = module.get(MediaElementService); - boardDoRepo = module.get(BoardDoRepo); - boardDoService = module.get(BoardDoService); - contextExternalToolService = module.get(ContextExternalToolService); - - await setupEntities(); - }); - - afterAll(async () => { - await module.close(); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - describe('findById', () => { - describe('when an element with the id exists', () => { - const setup = () => { - const element = mediaExternalToolElementFactory.build(); - - boardDoRepo.findById.mockResolvedValueOnce(element); - - return { - element, - }; - }; - - it('should return the element', async () => { - const { element } = setup(); - - const result = await service.findById(element.id); - - expect(result).toEqual(element); - }); - }); - - describe('when no element with the id exists', () => { - const setup = () => { - const board = mediaBoardFactory.build(); - - boardDoRepo.findById.mockResolvedValueOnce(board); - - return { - board, - }; - }; - - it('should return the element', async () => { - const { board } = setup(); - - await expect(service.findById(board.id)).rejects.toThrow(NotFoundException); - }); - }); - }); - - describe('createContextExternalToolForMediaBoard', () => { - describe('when creating a new context external tool', () => { - const setup = () => { - const user = userFactory.build(); - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.withSchoolId(user.school.id).build(); - const mediaBoard = mediaBoardFactory.build(); - const contextExternalTool: ContextExternalTool = contextExternalToolFactory - .withSchoolExternalToolRef(schoolExternalTool.id, user.school.id) - .withContextRef(mediaBoard.id, ToolContextType.MEDIA_BOARD) - .buildWithId(); - - contextExternalToolService.saveContextExternalTool.mockResolvedValueOnce(contextExternalTool); - - return { - user, - mediaBoard, - schoolExternalTool, - contextExternalTool, - }; - }; - - it('should return the context external tool', async () => { - const { user, schoolExternalTool, contextExternalTool, mediaBoard } = setup(); - - const result = await service.createContextExternalToolForMediaBoard(user, schoolExternalTool, mediaBoard); - - expect(result).toEqual( - new ContextExternalTool({ - id: expect.any(String), - displayName: contextExternalTool.displayName, - schoolToolRef: { - schoolId: user.school.id, - schoolToolId: schoolExternalTool.id, - }, - contextRef: { - id: mediaBoard.id, - type: ToolContextType.MEDIA_BOARD, - }, - parameters: contextExternalTool.parameters, - }) - ); - }); - - it('should save the new context external tool', async () => { - const { user, schoolExternalTool, mediaBoard } = setup(); - - await service.createContextExternalToolForMediaBoard(user, schoolExternalTool, mediaBoard); - - expect(contextExternalToolService.saveContextExternalTool).toHaveBeenCalledWith( - new ContextExternalTool({ - id: expect.any(String), - schoolToolRef: { - schoolId: user.school.id, - schoolToolId: schoolExternalTool.id, - }, - contextRef: { - id: mediaBoard.id, - type: ToolContextType.MEDIA_BOARD, - }, - parameters: [], - }) - ); - }); - }); - }); - - describe('createExternalToolElement', () => { - describe('when creating a new element', () => { - const setup = () => { - const line = mediaLineFactory.build(); - const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId(); - - return { - line, - contextExternalTool, - }; - }; - - it('should return the element', async () => { - const { line, contextExternalTool } = setup(); - - const result = await service.createExternalToolElement(line, 0, contextExternalTool); - - expect(result).toEqual( - expect.objectContaining({ - id: expect.any(String), - children: [], - createdAt: expect.any(Date), - updatedAt: expect.any(Date), - contextExternalToolId: contextExternalTool.id, - }) - ); - }); - - it('should save the new element', async () => { - const { line, contextExternalTool } = setup(); - - const result = await service.createExternalToolElement(line, 0, contextExternalTool); - - expect(boardDoRepo.save).toHaveBeenCalledWith([result], line); - }); - }); - }); - - describe('delete', () => { - describe('when deleting an element', () => { - const setup = () => { - const element = mediaExternalToolElementFactory.build(); - - return { - element, - }; - }; - - it('should delete the element', async () => { - const { element } = setup(); - - await service.delete(element); - - expect(boardDoService.deleteWithDescendants).toHaveBeenCalledWith(element); - }); - }); - }); - - describe('move', () => { - describe('when deleting an element', () => { - const setup = () => { - const line = mediaLineFactory.build(); - const element = mediaExternalToolElementFactory.build(); - - return { - line, - element, - }; - }; - - it('should move the element', async () => { - const { line, element } = setup(); - - await service.move(element, line, 3); - - expect(boardDoService.move).toHaveBeenCalledWith(element, line, 3); - }); - }); - }); - - describe('checkElementExists', () => { - describe('when an element exists', () => { - const setup = () => { - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.buildWithId(); - const contextExternalTool: ContextExternalTool = contextExternalToolFactory - .withSchoolExternalToolRef(schoolExternalTool.id) - .build(); - const element = mediaExternalToolElementFactory.build({ contextExternalToolId: contextExternalTool.id }); - const line = mediaLineFactory.addChild(element).build(); - const mediaBoard = mediaBoardFactory.addChild(line).build(); - - contextExternalToolService.findContextExternalTools.mockResolvedValueOnce([contextExternalTool]); - - return { - mediaBoard, - schoolExternalTool, - }; - }; - - it('should return true if the element exists', async () => { - const { mediaBoard, schoolExternalTool } = setup(); - - const result = await service.checkElementExists(mediaBoard, schoolExternalTool); - - expect(result).toBe(true); - }); - }); - - describe('when an element does not exist', () => { - const setup = () => { - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.buildWithId(); - const contextExternalTool: ContextExternalTool = contextExternalToolFactory - .withSchoolExternalToolRef(schoolExternalTool.id) - .build(); - const element = mediaExternalToolElementFactory.build({ contextExternalToolId: new ObjectId().toHexString() }); - const line = mediaLineFactory.addChild(element).build(); - const mediaBoard = mediaBoardFactory.addChild(line).build(); - - contextExternalToolService.findContextExternalTools.mockResolvedValueOnce([contextExternalTool]); - - return { - mediaBoard, - schoolExternalTool, - }; - }; - - it('should return false if the element does not exist', async () => { - const { mediaBoard, schoolExternalTool } = setup(); - - contextExternalToolService.findContextExternalTools.mockResolvedValueOnce([]); - - const result = await service.checkElementExists(mediaBoard, schoolExternalTool); - - expect(result).toBe(false); - }); - }); - }); -}); diff --git a/apps/server/src/modules/board/service/media-board/media-element.service.ts b/apps/server/src/modules/board/service/media-board/media-element.service.ts deleted file mode 100644 index f164a86a226..00000000000 --- a/apps/server/src/modules/board/service/media-board/media-element.service.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { ObjectId } from '@mikro-orm/mongodb'; -import { ToolContextType } from '@modules/tool/common/enum'; -import { ContextExternalToolService } from '@modules/tool/context-external-tool'; -import { ContextExternalTool, ContextRef } from '@modules/tool/context-external-tool/domain'; -import { SchoolExternalTool, SchoolExternalToolRef } from '@modules/tool/school-external-tool/domain'; -import { Injectable, NotFoundException } from '@nestjs/common'; -import { - AnyBoardDo, - type AnyMediaContentElementDo, - isAnyMediaContentElement, - MediaBoard, - MediaExternalToolElement, - MediaLine, -} from '@shared/domain/domainobject'; -import { User } from '@shared/domain/entity'; -import { EntityId } from '@shared/domain/types'; -import { BoardDoRepo } from '../../repo'; -import { BoardDoService } from '../board-do.service'; - -@Injectable() -export class MediaElementService { - constructor( - private readonly boardDoRepo: BoardDoRepo, - private readonly boardDoService: BoardDoService, - private readonly contextExternalToolService: ContextExternalToolService - ) {} - - public async findById(elementId: EntityId): Promise { - const element: AnyBoardDo = await this.boardDoRepo.findById(elementId); - - if (!isAnyMediaContentElement(element)) { - throw new NotFoundException(`There is no '${element.constructor.name}' with this id`); - } - - return element; - } - - public async checkElementExists(mediaBoard: MediaBoard, schoolExternalTool: SchoolExternalTool): Promise { - const contextExternalTools: ContextExternalTool[] = await this.contextExternalToolService.findContextExternalTools({ - schoolToolRef: { schoolToolId: schoolExternalTool.id }, - }); - - const exists = mediaBoard - .getChildrenOfType(MediaExternalToolElement) - .some((element) => contextExternalTools.some((tool) => tool.id === element.contextExternalToolId)); - - return exists; - } - - public async createContextExternalToolForMediaBoard( - user: User, - schoolExternalTool: SchoolExternalTool, - mediaBoard: MediaBoard - ): Promise { - const contextExternalTool: ContextExternalTool = await this.contextExternalToolService.saveContextExternalTool( - new ContextExternalTool({ - id: new ObjectId().toHexString(), - schoolToolRef: new SchoolExternalToolRef({ schoolId: user.school.id, schoolToolId: schoolExternalTool.id }), - contextRef: new ContextRef({ id: mediaBoard.id, type: ToolContextType.MEDIA_BOARD }), - parameters: [], - }) - ); - - return contextExternalTool; - } - - public async createExternalToolElement( - parent: MediaLine, - position: number, - contextExternalTool: ContextExternalTool - ): Promise { - const element: MediaExternalToolElement = new MediaExternalToolElement({ - id: new ObjectId().toHexString(), - children: [], - createdAt: new Date(), - updatedAt: new Date(), - contextExternalToolId: contextExternalTool.id, - }); - - parent.addChild(element, position); - - await this.boardDoRepo.save(parent.children, parent); - - return element; - } - - public async delete(element: AnyMediaContentElementDo): Promise { - await this.boardDoService.deleteWithDescendants(element); - } - - public async move(element: AnyMediaContentElementDo, targetLine: MediaLine, targetPosition: number): Promise { - await this.boardDoService.move(element, targetLine, targetPosition); - } -} diff --git a/apps/server/src/modules/board/service/media-board/media-line.service.spec.ts b/apps/server/src/modules/board/service/media-board/media-line.service.spec.ts deleted file mode 100644 index 3e6d29ed1c8..00000000000 --- a/apps/server/src/modules/board/service/media-board/media-line.service.spec.ts +++ /dev/null @@ -1,243 +0,0 @@ -import { createMock, type DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { mediaBoardFactory, mediaLineFactory } from '@shared/testing'; -import { MediaBoardColors } from '../../domain'; -import { BoardDoRepo } from '../../repo'; -import { BoardDoService } from '../board-do.service'; -import { MediaLineService } from './media-line.service'; - -describe(MediaLineService.name, () => { - let module: TestingModule; - let service: MediaLineService; - - let boardDoRepo: DeepMocked; - let boardDoService: DeepMocked; - - beforeAll(async () => { - module = await Test.createTestingModule({ - providers: [ - MediaLineService, - { - provide: BoardDoRepo, - useValue: createMock(), - }, - { - provide: BoardDoService, - useValue: createMock(), - }, - ], - }).compile(); - - service = module.get(MediaLineService); - boardDoRepo = module.get(BoardDoRepo); - boardDoService = module.get(BoardDoService); - }); - - afterAll(async () => { - await module.close(); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - describe('findById', () => { - describe('when a line with the id exists', () => { - const setup = () => { - const line = mediaLineFactory.build(); - - boardDoRepo.findByClassAndId.mockResolvedValueOnce(line); - - return { - line, - }; - }; - - it('should return the line', async () => { - const { line } = setup(); - - const result = await service.findById(line.id); - - expect(result).toEqual(line); - }); - }); - }); - - describe('create', () => { - describe('when creating a new line', () => { - const setup = () => { - const board = mediaBoardFactory.build(); - - return { - board, - }; - }; - - it('should return the line', async () => { - const { board } = setup(); - - const result = await service.create(board, { - title: 'lineTitle', - backgroundColor: MediaBoardColors.TRANSPARENT, - collapsed: false, - }); - - expect(result).toEqual( - expect.objectContaining({ - id: expect.any(String), - children: [], - createdAt: expect.any(Date), - updatedAt: expect.any(Date), - title: 'lineTitle', - backgroundColor: MediaBoardColors.TRANSPARENT, - collapsed: false, - }) - ); - }); - - it('should save the new line', async () => { - const { board } = setup(); - - const result = await service.create(board); - - expect(boardDoRepo.save).toHaveBeenCalledWith([result], board); - }); - }); - }); - - describe('delete', () => { - describe('when deleting a line', () => { - const setup = () => { - const line = mediaLineFactory.build(); - - return { - line, - }; - }; - - it('should delete the element', async () => { - const { line } = setup(); - - await service.delete(line); - - expect(boardDoService.deleteWithDescendants).toHaveBeenCalledWith(line); - }); - }); - }); - - describe('move', () => { - describe('when deleting a line', () => { - const setup = () => { - const board = mediaBoardFactory.build(); - const line = mediaLineFactory.build(); - - return { - line, - board, - }; - }; - - it('should move the line', async () => { - const { line, board } = setup(); - - await service.move(line, board, 3); - - expect(boardDoService.move).toHaveBeenCalledWith(line, board, 3); - }); - }); - }); - - describe('updateTitle', () => { - describe('when updating the title', () => { - const setup = () => { - const board = mediaBoardFactory.build(); - const line = mediaLineFactory.build(); - - boardDoRepo.findParentOfId.mockResolvedValueOnce(board); - - return { - line, - board, - }; - }; - - it('should update the title', async () => { - const { line, board } = setup(); - - await service.updateTitle(line, 'newTitle'); - - expect(boardDoRepo.save).toHaveBeenCalledWith( - expect.objectContaining({ - id: line.id, - title: 'newTitle', - }), - board - ); - }); - }); - }); - - describe('updateColor', () => { - describe('when updating the color', () => { - const setup = () => { - const board = mediaBoardFactory.build(); - const line = mediaLineFactory.build({ - backgroundColor: MediaBoardColors.TRANSPARENT, - }); - - boardDoRepo.findParentOfId.mockResolvedValueOnce(board); - - return { - line, - board, - }; - }; - - it('should update the color', async () => { - const { line, board } = setup(); - - await service.updateColor(line, MediaBoardColors.RED); - - expect(boardDoRepo.save).toHaveBeenCalledWith( - expect.objectContaining({ - id: line.id, - backgroundColor: MediaBoardColors.RED, - }), - board - ); - }); - }); - }); - - describe('collapse', () => { - describe('when updating the visibility', () => { - const setup = () => { - const board = mediaBoardFactory.build(); - const line = mediaLineFactory.build({ - collapsed: false, - }); - - boardDoRepo.findParentOfId.mockResolvedValueOnce(board); - - return { - line, - board, - }; - }; - - it('should update the visibility', async () => { - const { line, board } = setup(); - - await service.collapse(line, true); - - expect(boardDoRepo.save).toHaveBeenCalledWith( - expect.objectContaining({ - id: line.id, - collapsed: true, - }), - board - ); - }); - }); - }); -}); diff --git a/apps/server/src/modules/board/service/media-board/media-line.service.ts b/apps/server/src/modules/board/service/media-board/media-line.service.ts deleted file mode 100644 index 8b75d762aea..00000000000 --- a/apps/server/src/modules/board/service/media-board/media-line.service.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { ObjectId } from '@mikro-orm/mongodb'; -import { Injectable } from '@nestjs/common'; -import { type AnyBoardDo, MediaBoard, MediaLine, MediaLineInitProps } from '@shared/domain/domainobject'; -import { EntityId } from '@shared/domain/types'; -import { MediaBoardColors } from '../../domain'; -import { BoardDoRepo } from '../../repo'; -import { BoardDoService } from '../board-do.service'; - -@Injectable() -export class MediaLineService { - constructor(private readonly boardDoRepo: BoardDoRepo, private readonly boardDoService: BoardDoService) {} - - public async findById(lineId: EntityId): Promise { - const line: MediaLine = await this.boardDoRepo.findByClassAndId(MediaLine, lineId); - - return line; - } - - public async create(parent: MediaBoard, props?: MediaLineInitProps): Promise { - const line: MediaLine = new MediaLine({ - id: new ObjectId().toHexString(), - title: props?.title ?? '', - backgroundColor: props?.backgroundColor ?? MediaBoardColors.TRANSPARENT, - children: [], - createdAt: new Date(), - updatedAt: new Date(), - collapsed: props?.collapsed ?? false, - }); - - parent.addChild(line); - - await this.boardDoRepo.save(parent.children, parent); - - return line; - } - - public async delete(line: MediaLine): Promise { - await this.boardDoService.deleteWithDescendants(line); - } - - public async move(line: MediaLine, targetBoard: MediaBoard, targetPosition?: number): Promise { - await this.boardDoService.move(line, targetBoard, targetPosition); - } - - public async updateTitle(line: MediaLine, title: string): Promise { - const parent: AnyBoardDo | undefined = await this.boardDoRepo.findParentOfId(line.id); - - line.title = title; - - await this.boardDoRepo.save(line, parent); - } - - public async updateColor(line: MediaLine, color: MediaBoardColors): Promise { - const parent: AnyBoardDo | undefined = await this.boardDoRepo.findParentOfId(line.id); - - line.backgroundColor = color; - - await this.boardDoRepo.save(line, parent); - } - - public async collapse(line: MediaLine, collapsed: boolean): Promise { - const parent: AnyBoardDo | undefined = await this.boardDoRepo.findParentOfId(line.id); - - line.collapsed = collapsed; - - await this.boardDoRepo.save(line, parent); - } -} diff --git a/apps/server/src/modules/board/service/submission-item.service.spec.ts b/apps/server/src/modules/board/service/submission-item.service.spec.ts deleted file mode 100644 index d95e7657cd2..00000000000 --- a/apps/server/src/modules/board/service/submission-item.service.spec.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { NotFoundException, UnprocessableEntityException } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import { ValidationError } from '@shared/common'; -import { SubmissionItem } from '@shared/domain/domainobject'; -import { richTextElementFactory, setupEntities, userFactory } from '@shared/testing'; -import { - cardFactory, - submissionContainerElementFactory, - submissionItemFactory, -} from '@shared/testing/factory/domainobject'; -import { BoardDoRepo } from '../repo'; -import { BoardDoService } from './board-do.service'; -import { SubmissionItemService } from './submission-item.service'; - -describe(SubmissionItemService.name, () => { - let module: TestingModule; - let service: SubmissionItemService; - let boardDoRepo: DeepMocked; - - beforeAll(async () => { - module = await Test.createTestingModule({ - providers: [ - SubmissionItemService, - { - provide: BoardDoRepo, - useValue: createMock(), - }, - { - provide: BoardDoService, - useValue: createMock(), - }, - ], - }).compile(); - - service = module.get(SubmissionItemService); - boardDoRepo = module.get(BoardDoRepo); - - await setupEntities(); - }); - - afterAll(async () => { - await module.close(); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - describe('create', () => { - describe('when calling the create method', () => { - const setup = () => { - const user = userFactory.buildWithId(); - const submissionContainer = submissionContainerElementFactory.build(); - - return { submissionContainer, user }; - }; - - it('should return an instance of SubmissionItem', async () => { - const { submissionContainer, user } = setup(); - const result = await service.create(user.id, submissionContainer, { completed: true }); - expect(result).toBeInstanceOf(SubmissionItem); - }); - }); - }); - - describe('findById', () => { - describe('when trying get SubmissionItem by id', () => { - const setup = () => { - const submissionItem = submissionItemFactory.build(); - boardDoRepo.findById.mockResolvedValue(submissionItem); - - return { submissionItem }; - }; - - it('should return instance of SubmissionItem', async () => { - const { submissionItem } = setup(); - - const result = await service.findById(submissionItem.id); - - expect(result).toBeInstanceOf(SubmissionItem); - }); - }); - - describe('when trying get an wrong element by id', () => { - const setup = () => { - const cardElement = cardFactory.build(); - boardDoRepo.findById.mockResolvedValue(cardElement); - - return { cardElement }; - }; - - it('should throw NotFoundException', async () => { - const { cardElement } = setup(); - - await expect(service.findById(cardElement.id)).rejects.toThrowError(NotFoundException); - }); - }); - }); - - describe('update', () => { - const setup = () => { - const submissionContainer = submissionContainerElementFactory.build(); - const submissionItem = submissionItemFactory.build(); - - boardDoRepo.findParentOfId.mockResolvedValueOnce(submissionContainer); - - return { submissionContainer, submissionItem }; - }; - - it('should fetch the parent', async () => { - const { submissionItem } = setup(); - - await service.update(submissionItem, true); - - expect(boardDoRepo.findParentOfId).toHaveBeenCalledWith(submissionItem.id); - }); - - it('should throw if parent is not SubmissionContainerElement', async () => { - const submissionItem = submissionItemFactory.build(); - const richTextElement = richTextElementFactory.build(); - boardDoRepo.findParentOfId.mockResolvedValueOnce(richTextElement); - - await expect(service.update(submissionItem, true)).rejects.toThrow(UnprocessableEntityException); - }); - - it('should call bord repo to save submission item', async () => { - const { submissionItem, submissionContainer } = setup(); - - await service.update(submissionItem, true); - - expect(boardDoRepo.save).toHaveBeenCalledWith(submissionItem, submissionContainer); - }); - - it('should save completion', async () => { - const { submissionItem, submissionContainer } = setup(); - - await service.update(submissionItem, false); - - expect(boardDoRepo.save).toHaveBeenCalledWith(expect.objectContaining({ completed: false }), submissionContainer); - }); - - it('should throw if parent SubmissionContainer dueDate is in the past', async () => { - const { submissionItem, submissionContainer } = setup(); - - const yesterday = new Date(Date.now() - 86400000); - submissionContainer.dueDate = yesterday; - boardDoRepo.findParentOfId.mockResolvedValue(submissionContainer); - - await expect(service.update(submissionItem, true)).rejects.toThrowError(ValidationError); - }); - }); - - describe('delete', () => { - const setup = () => { - const submissionContainer = submissionContainerElementFactory.build(); - const submissionItem = submissionItemFactory.build(); - - boardDoRepo.findParentOfId.mockResolvedValueOnce(submissionContainer); - - return { submissionContainer, submissionItem }; - }; - - it('should fetch the parent', async () => { - const { submissionItem } = setup(); - - await service.delete(submissionItem); - - expect(boardDoRepo.findParentOfId).toHaveBeenCalledWith(submissionItem.id); - }); - - it('should throw if parent is not SubmissionContainerElement', async () => { - const submissionItem = submissionItemFactory.build(); - const richTextElement = richTextElementFactory.build(); - boardDoRepo.findParentOfId.mockResolvedValueOnce(richTextElement); - - await expect(service.update(submissionItem, true)).rejects.toThrow(UnprocessableEntityException); - }); - - it('should call bord repo to delete submission item', async () => { - const { submissionItem } = setup(); - - await service.delete(submissionItem); - - expect(boardDoRepo.delete).toHaveBeenCalledWith(submissionItem); - }); - - it('should throw if parent SubmissionContainer dueDate is in the past', async () => { - const { submissionItem, submissionContainer } = setup(); - - const yesterday = new Date(Date.now() - 86400000); - submissionContainer.dueDate = yesterday; - boardDoRepo.findParentOfId.mockResolvedValue(submissionContainer); - - await expect(service.delete(submissionItem)).rejects.toThrowError(ValidationError); - }); - }); -}); diff --git a/apps/server/src/modules/board/service/submission-item.service.ts b/apps/server/src/modules/board/service/submission-item.service.ts deleted file mode 100644 index 4ff79def319..00000000000 --- a/apps/server/src/modules/board/service/submission-item.service.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Injectable, NotFoundException, UnprocessableEntityException } from '@nestjs/common'; -import { ObjectId } from '@mikro-orm/mongodb'; - -import { ValidationError } from '@shared/common'; -import { isSubmissionContainerElement, SubmissionContainerElement, SubmissionItem } from '@shared/domain/domainobject'; -import { EntityId } from '@shared/domain/types'; - -import { BoardDoRepo } from '../repo'; -import { BoardDoService } from './board-do.service'; - -@Injectable() -export class SubmissionItemService { - constructor(private readonly boardDoRepo: BoardDoRepo, private readonly boardDoService: BoardDoService) {} - - async findById(id: EntityId): Promise { - const element = await this.boardDoRepo.findById(id); - - if (!(element instanceof SubmissionItem)) { - throw new NotFoundException(`There is no '${element.constructor.name}' with this id`); - } - - return element; - } - - async create( - userId: EntityId, - submissionContainer: SubmissionContainerElement, - payload: { completed: boolean } - ): Promise { - this.checkNotLocked(submissionContainer); - - const submissionItem = new SubmissionItem({ - id: new ObjectId().toHexString(), - createdAt: new Date(), - updatedAt: new Date(), - completed: payload.completed, - userId, - }); - - submissionContainer.addChild(submissionItem); - - await this.boardDoRepo.save(submissionContainer.children, submissionContainer); - - return submissionItem; - } - - async update(submissionItem: SubmissionItem, completed: boolean): Promise { - const parent = await this.getParent(submissionItem); - - submissionItem.completed = completed; - - await this.boardDoRepo.save(submissionItem, parent); - } - - async delete(submissionItem: SubmissionItem): Promise { - await this.getParent(submissionItem); - - await this.boardDoRepo.delete(submissionItem); - } - - private async getParent(submissionItem: SubmissionItem): Promise { - const submissionContainterElement = await this.boardDoRepo.findParentOfId(submissionItem.id); - - if (!isSubmissionContainerElement(submissionContainterElement)) { - throw new UnprocessableEntityException(); - } - - this.checkNotLocked(submissionContainterElement); - - return submissionContainterElement; - } - - private checkNotLocked(submissionContainterElement: SubmissionContainerElement): void { - const now = new Date(); - if (submissionContainterElement.dueDate && submissionContainterElement.dueDate < now) { - throw new ValidationError('not allowed to save anymore'); - } - } -} diff --git a/apps/server/src/modules/board/testing/board-node-authorizable.factory.ts b/apps/server/src/modules/board/testing/board-node-authorizable.factory.ts new file mode 100644 index 00000000000..e34d399b5c7 --- /dev/null +++ b/apps/server/src/modules/board/testing/board-node-authorizable.factory.ts @@ -0,0 +1,19 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { DomainObjectFactory } from '@shared/testing'; +import { BoardNodeAuthorizable, BoardNodeAuthorizableProps } from '../domain'; +import { columnBoardFactory } from './column-board.factory'; +import { columnFactory } from './column.factory'; + +export const boardNodeAuthorizableFactory = DomainObjectFactory.define< + BoardNodeAuthorizable, + BoardNodeAuthorizableProps +>(BoardNodeAuthorizable, () => { + const boardNode = columnFactory.build(); + const rootNode = columnBoardFactory.build({ children: [boardNode] }); + return { + id: new ObjectId().toHexString(), + users: [], + boardNode, + rootNode, + }; +}); diff --git a/apps/server/src/shared/testing/factory/domainobject/board/card.do.factory.ts b/apps/server/src/modules/board/testing/card.factory.ts similarity index 51% rename from apps/server/src/shared/testing/factory/domainobject/board/card.do.factory.ts rename to apps/server/src/modules/board/testing/card.factory.ts index 141465b609d..71999aef7e8 100644 --- a/apps/server/src/shared/testing/factory/domainobject/board/card.do.factory.ts +++ b/apps/server/src/modules/board/testing/card.factory.ts @@ -1,15 +1,20 @@ /* istanbul ignore file */ -import { Card, CardProps } from '@shared/domain/domainobject'; import { ObjectId } from '@mikro-orm/mongodb'; -import { BaseFactory } from '../../base.factory'; +import { BaseFactory } from '@shared/testing'; +import { Card, CardProps, ROOT_PATH } from '../domain'; -export const cardFactory = BaseFactory.define(Card, ({ sequence, params }) => { - return { +export const cardFactory = BaseFactory.define(Card, ({ sequence }) => { + const props: CardProps = { id: new ObjectId().toHexString(), + path: ROOT_PATH, + level: 0, title: `card #${sequence}`, - height: 150, - children: params?.children || [], + position: 0, + children: [], createdAt: new Date(), updatedAt: new Date(), + height: 42, }; + + return props; }); diff --git a/apps/server/src/modules/board/testing/collaborative-text-editor.factory.ts b/apps/server/src/modules/board/testing/collaborative-text-editor.factory.ts new file mode 100644 index 00000000000..1bc197060a3 --- /dev/null +++ b/apps/server/src/modules/board/testing/collaborative-text-editor.factory.ts @@ -0,0 +1,18 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BaseFactory } from '@shared/testing'; +import { CollaborativeTextEditorElement, CollaborativeTextEditorElementProps, ROOT_PATH } from '../domain'; + +export const collaborativeTextEditorFactory = BaseFactory.define< + CollaborativeTextEditorElement, + CollaborativeTextEditorElementProps +>(CollaborativeTextEditorElement, () => { + return { + id: new ObjectId().toHexString(), + path: ROOT_PATH, + level: 0, + position: 0, + children: [], + createdAt: new Date(), + updatedAt: new Date(), + }; +}); diff --git a/apps/server/src/shared/testing/factory/domainobject/board/column-board.do.factory.ts b/apps/server/src/modules/board/testing/column-board.factory.ts similarity index 57% rename from apps/server/src/shared/testing/factory/domainobject/board/column-board.do.factory.ts rename to apps/server/src/modules/board/testing/column-board.factory.ts index 92a95c8c8d6..48c925dcaf6 100644 --- a/apps/server/src/shared/testing/factory/domainobject/board/column-board.do.factory.ts +++ b/apps/server/src/modules/board/testing/column-board.factory.ts @@ -1,10 +1,7 @@ /* istanbul ignore file */ -import { ColumnBoard, ColumnBoardProps } from '@shared/domain/domainobject'; -import { BoardExternalReferenceType, BoardLayout } from '@shared/domain/domainobject/board/types'; import { ObjectId } from '@mikro-orm/mongodb'; -import { BaseFactory } from '../../base.factory'; - -export type IColumnBoardProperties = Readonly; +import { BaseFactory } from '@shared/testing'; +import { BoardExternalReferenceType, BoardLayout, ColumnBoard, ColumnBoardProps, ROOT_PATH } from '../domain'; class ColumnBoardFactory extends BaseFactory { withoutContext(): this { @@ -12,11 +9,15 @@ class ColumnBoardFactory extends BaseFactory { return this.params(params); } } -export const columnBoardFactory = ColumnBoardFactory.define(ColumnBoard, ({ sequence, params }) => { - return { + +export const columnBoardFactory = ColumnBoardFactory.define(ColumnBoard, ({ sequence }) => { + const props: ColumnBoardProps = { id: new ObjectId().toHexString(), + path: ROOT_PATH, + level: 0, title: `column board #${sequence}`, - children: params?.children ?? [], + position: 0, + children: [], createdAt: new Date(), updatedAt: new Date(), context: { @@ -24,6 +25,8 @@ export const columnBoardFactory = ColumnBoardFactory.define(ColumnBoard, ({ sequ id: new ObjectId().toHexString(), }, isVisible: true, - layout: params?.layout ?? BoardLayout.COLUMNS, + layout: BoardLayout.COLUMNS, }; + + return props; }); diff --git a/apps/server/src/shared/testing/factory/domainobject/board/column.do.factory.ts b/apps/server/src/modules/board/testing/column.factory.ts similarity index 61% rename from apps/server/src/shared/testing/factory/domainobject/board/column.do.factory.ts rename to apps/server/src/modules/board/testing/column.factory.ts index fcb9597aacd..9de33a2765a 100644 --- a/apps/server/src/shared/testing/factory/domainobject/board/column.do.factory.ts +++ b/apps/server/src/modules/board/testing/column.factory.ts @@ -1,14 +1,19 @@ /* istanbul ignore file */ -import { Column, ColumnProps } from '@shared/domain/domainobject'; import { ObjectId } from '@mikro-orm/mongodb'; -import { BaseFactory } from '../../base.factory'; +import { BaseFactory } from '@shared/testing'; +import { Column, ColumnProps, ROOT_PATH } from '../domain'; export const columnFactory = BaseFactory.define(Column, ({ sequence }) => { - return { + const props: ColumnProps = { id: new ObjectId().toHexString(), + path: ROOT_PATH, + level: 0, title: `column #${sequence}`, + position: 0, children: [], createdAt: new Date(), updatedAt: new Date(), }; + + return props; }); diff --git a/apps/server/src/shared/testing/factory/domainobject/board/drawing-element.do.factory.ts b/apps/server/src/modules/board/testing/drawing-element.factory.ts similarity index 56% rename from apps/server/src/shared/testing/factory/domainobject/board/drawing-element.do.factory.ts rename to apps/server/src/modules/board/testing/drawing-element.factory.ts index 282f457499e..678a23384bc 100644 --- a/apps/server/src/shared/testing/factory/domainobject/board/drawing-element.do.factory.ts +++ b/apps/server/src/modules/board/testing/drawing-element.factory.ts @@ -1,18 +1,19 @@ -/* istanbul ignore file */ import { ObjectId } from '@mikro-orm/mongodb'; -import { DrawingElement, DrawingElementProps } from '@shared/domain/domainobject/board/drawing-element.do'; -import { BaseFactory } from '../../base.factory'; +import { BaseFactory } from '@shared/testing'; +import { DrawingElement, DrawingElementProps, ROOT_PATH } from '../domain'; export const drawingElementFactory = BaseFactory.define( DrawingElement, ({ sequence }) => { return { id: new ObjectId().toHexString(), - title: `element #${sequence}`, + path: ROOT_PATH, + level: 0, + position: 0, children: [], + description: `caption #${sequence}`, createdAt: new Date(), updatedAt: new Date(), - description: '', }; } ); diff --git a/apps/server/src/modules/board/testing/entity/board-node-entity.factory.ts b/apps/server/src/modules/board/testing/entity/board-node-entity.factory.ts new file mode 100644 index 00000000000..48bdea50a60 --- /dev/null +++ b/apps/server/src/modules/board/testing/entity/board-node-entity.factory.ts @@ -0,0 +1,170 @@ +import { BuildOptions, DeepPartial, Factory, GeneratorFn, HookFn } from 'fishery'; +import { AnyBoardNodeProps, BoardNodeType, pathOfChildren } from '../../domain'; +import { BoardNodeEntity } from '../../repo'; + +export type PropsWithType = T & { type: BoardNodeType }; + +/** + * Entity factory based on thoughtbot/fishery + * https://github.com/thoughtbot/fishery + * + * @template T The entity to be built + * @template U The properties interface of the entity + * @template I The transient parameters that your factory supports + * @template C The class of the factory object being created. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export class BoardNodeEntityFactory { + protected readonly propsFactory: Factory; + + constructor(propsFactory: Factory) { + this.propsFactory = propsFactory; + } + + /** + * Define a factory + * @template T The entity to be built + * @template I The transient parameters that your factory supports + * @template C The class of the factory object being created. + * @param EntityClass The constructor of the entity to be built. + * @param generator Your factory function - see `Factory.define()` in thoughtbot/fishery + * @returns + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static define>( + this: new (propsFactory: Factory) => F, + generator: GeneratorFn + ): F { + const propsFactory = Factory.define(generator); + const factory = new this(propsFactory); + return factory; + } + + withParent(parent: BoardNodeEntity) { + return this.params({ path: pathOfChildren(parent), level: parent.level + 1 } as DeepPartial); + } + + /** + * Build an entity using your factory + * @param params + * @returns an entity + */ + build(params?: DeepPartial, options: BuildOptions = {}): BoardNodeEntity { + const props = this.propsFactory.build(params, options); + const entity = new BoardNodeEntity(); + Object.assign(entity, props); + + return entity; + } + + /** + * Build an entity using your factory and generate a id for it. + * @param params + * @param id + * @returns an entity + */ + buildWithId(params?: DeepPartial, id?: string, options: BuildOptions = {}): BoardNodeEntity { + const entity = this.build(params, options); + if (id) { + entity.id = id; + } + + return entity; + } + + /** + * Build a list of entities using your factory + * @param number + * @param params + * @returns a list of entities + */ + buildList(number: number, params?: DeepPartial, options: BuildOptions = {}): BoardNodeEntity[] { + const list: BoardNodeEntity[] = []; + for (let i = 0; i < number; i += 1) { + list.push(this.build(params, options)); + } + + return list; + } + + buildListWithId(number: number, params?: DeepPartial, options: BuildOptions = {}): BoardNodeEntity[] { + const list: BoardNodeEntity[] = []; + for (let i = 0; i < number; i += 1) { + list.push(this.buildWithId(params, undefined, options)); + } + + return list; + } + + /** + * Extend the factory by adding a function to be called after an object is built. + * @param afterBuildFn - the function to call. It accepts your object of type T. The value this function returns gets returned from "build" + * @returns a new factory + */ + afterBuild(afterBuildFn: HookFn): this { + const newPropsFactory = this.propsFactory.afterBuild(afterBuildFn); + const newFactory = this.clone(newPropsFactory); + + return newFactory; + } + + /** + * Extend the factory by adding default associations to be passed to the factory when "build" is called + * @param associations + * @returns a new factory + */ + associations(associations: Partial): this { + const newPropsFactory = this.propsFactory.associations(associations); + const newFactory = this.clone(newPropsFactory); + + return newFactory; + } + + /** + * Extend the factory by adding default parameters to be passed to the factory when "build" is called + * @param params + * @returns a new factory + */ + params(params: DeepPartial): this { + const newPropsFactory = this.propsFactory.params(params); + const newFactory = this.clone(newPropsFactory); + + return newFactory; + } + + /** + * Extend the factory by adding default transient parameters to be passed to the factory when "build" is called + * @param transient - transient params + * @returns a new factory + */ + transient(transient: Partial): this { + const newPropsFactory = this.propsFactory.transient(transient); + const newFactory = this.clone(newPropsFactory); + + return newFactory; + } + + /** + * Set sequence back to its default value + */ + rewindSequence(): void { + this.propsFactory.rewindSequence(); + } + + protected clone>(this: F, propsFactory: Factory): F { + const copy = new (this.constructor as { + new (propsOfFactory: Factory): F; + })(propsFactory); + + return copy; + } + + /** + * Get the next sequence value + * @returns the next sequence value + */ + protected sequence(): number { + // eslint-disable-next-line @typescript-eslint/dot-notation + return this.propsFactory['sequence'](); + } +} diff --git a/apps/server/src/modules/board/testing/entity/card-entity.factory.ts b/apps/server/src/modules/board/testing/entity/card-entity.factory.ts new file mode 100644 index 00000000000..a01b29089c4 --- /dev/null +++ b/apps/server/src/modules/board/testing/entity/card-entity.factory.ts @@ -0,0 +1,21 @@ +/* istanbul ignore file */ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BoardNodeType, CardProps, ROOT_PATH } from '../../domain'; +import { BoardNodeEntityFactory, PropsWithType } from './board-node-entity.factory'; + +export const cardEntityFactory = BoardNodeEntityFactory.define>(({ sequence }) => { + const props: PropsWithType = { + id: new ObjectId().toHexString(), + path: ROOT_PATH, + level: 0, + title: `card #${sequence}`, + position: 0, + children: [], + createdAt: new Date(), + updatedAt: new Date(), + height: 42, + type: BoardNodeType.CARD, + }; + + return props; +}); diff --git a/apps/server/src/modules/board/testing/entity/collaborative-text-editor-entity.factory.ts b/apps/server/src/modules/board/testing/entity/collaborative-text-editor-entity.factory.ts new file mode 100644 index 00000000000..35a3029fb03 --- /dev/null +++ b/apps/server/src/modules/board/testing/entity/collaborative-text-editor-entity.factory.ts @@ -0,0 +1,19 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BoardNodeEntityFactory, PropsWithType } from './board-node-entity.factory'; +import { BoardNodeType, CollaborativeTextEditorElementProps, ROOT_PATH } from '../../domain'; + +export const collaborativeTextEditorEntityFactory = BoardNodeEntityFactory.define< + PropsWithType +>(() => { + const props: PropsWithType = { + id: new ObjectId().toHexString(), + path: ROOT_PATH, + level: 0, + position: 0, + children: [], + createdAt: new Date(), + updatedAt: new Date(), + type: BoardNodeType.COLLABORATIVE_TEXT_EDITOR, + }; + return props; +}); diff --git a/apps/server/src/modules/board/testing/entity/column-board-entity.factory.ts b/apps/server/src/modules/board/testing/entity/column-board-entity.factory.ts new file mode 100644 index 00000000000..a6b94bb55ed --- /dev/null +++ b/apps/server/src/modules/board/testing/entity/column-board-entity.factory.ts @@ -0,0 +1,42 @@ +/* istanbul ignore file */ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BoardNodeEntityFactory, PropsWithType } from './board-node-entity.factory'; +import { BoardExternalReferenceType, BoardLayout, BoardNodeType, ColumnBoardProps, ROOT_PATH } from '../../domain'; +import { Context } from '../../repo/entity/embeddables'; + +class ColumnBoardEntityFactory extends BoardNodeEntityFactory> { + withoutContext(): this { + const params = { context: undefined }; + return this.params(params); + } +} + +export const columnBoardEntityFactory = ColumnBoardEntityFactory.define(({ sequence, params }) => { + const context = + params.context && params.context.type && params.context.id + ? new Context({ + type: params.context.type, + id: params.context.id, + }) + : new Context({ + type: BoardExternalReferenceType.Course, + id: new ObjectId().toHexString(), + }); + + const props: PropsWithType = { + id: new ObjectId().toHexString(), + path: ROOT_PATH, + level: 0, + title: `column board #${sequence}`, + position: 0, + children: [], + createdAt: new Date(), + updatedAt: new Date(), + context, + isVisible: true, + layout: BoardLayout.COLUMNS, + type: BoardNodeType.COLUMN_BOARD, + }; + + return props; +}); diff --git a/apps/server/src/modules/board/testing/entity/column-entity.factory.ts b/apps/server/src/modules/board/testing/entity/column-entity.factory.ts new file mode 100644 index 00000000000..6fb282c0f4a --- /dev/null +++ b/apps/server/src/modules/board/testing/entity/column-entity.factory.ts @@ -0,0 +1,19 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BoardNodeType, ColumnProps, ROOT_PATH } from '../../domain'; +import { BoardNodeEntityFactory, PropsWithType } from './board-node-entity.factory'; + +export const columnEntityFactory = BoardNodeEntityFactory.define>(({ sequence }) => { + const props: PropsWithType = { + id: new ObjectId().toHexString(), + path: ROOT_PATH, + level: 0, + title: `column #${sequence}`, + position: 0, + children: [], + createdAt: new Date(), + updatedAt: new Date(), + type: BoardNodeType.COLUMN, + }; + + return props; +}); diff --git a/apps/server/src/modules/board/testing/entity/drawing-element-entity.factory.ts b/apps/server/src/modules/board/testing/entity/drawing-element-entity.factory.ts new file mode 100644 index 00000000000..b5e50306f20 --- /dev/null +++ b/apps/server/src/modules/board/testing/entity/drawing-element-entity.factory.ts @@ -0,0 +1,21 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BoardNodeEntityFactory, PropsWithType } from './board-node-entity.factory'; +import { BoardNodeType, DrawingElementProps, ROOT_PATH } from '../../domain'; + +export const drawingElementEntityFactory = BoardNodeEntityFactory.define>( + ({ sequence }) => { + const props: PropsWithType = { + id: new ObjectId().toHexString(), + path: ROOT_PATH, + level: 0, + position: 0, + children: [], + description: `caption #${sequence}`, + createdAt: new Date(), + updatedAt: new Date(), + type: BoardNodeType.DRAWING_ELEMENT, + }; + + return props; + } +); diff --git a/apps/server/src/modules/board/testing/entity/external-tool-element-entity.factory.ts b/apps/server/src/modules/board/testing/entity/external-tool-element-entity.factory.ts new file mode 100644 index 00000000000..8392a7f989c --- /dev/null +++ b/apps/server/src/modules/board/testing/entity/external-tool-element-entity.factory.ts @@ -0,0 +1,21 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BoardNodeEntityFactory, PropsWithType } from './board-node-entity.factory'; +import { BoardNodeType, ExternalToolElementProps, ROOT_PATH } from '../../domain'; + +export const externalToolElementEntityFactory = BoardNodeEntityFactory.define>( + () => { + const props: PropsWithType = { + id: new ObjectId().toHexString(), + path: ROOT_PATH, + level: 0, + position: 0, + children: [], + contextExternalToolId: new ObjectId().toHexString(), // TODO check if this should be undefined + createdAt: new Date(), + updatedAt: new Date(), + type: BoardNodeType.EXTERNAL_TOOL, + }; + + return props; + } +); diff --git a/apps/server/src/modules/board/testing/entity/file-element-entity.factory.ts b/apps/server/src/modules/board/testing/entity/file-element-entity.factory.ts new file mode 100644 index 00000000000..a657480599d --- /dev/null +++ b/apps/server/src/modules/board/testing/entity/file-element-entity.factory.ts @@ -0,0 +1,20 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BoardNodeType, FileElementProps, ROOT_PATH } from '../../domain'; +import { BoardNodeEntityFactory, PropsWithType } from './board-node-entity.factory'; + +export const fileElementEntityFactory = BoardNodeEntityFactory.define>( + ({ sequence }) => { + return { + id: new ObjectId().toHexString(), + path: ROOT_PATH, + level: 0, + position: 0, + children: [], + caption: `file #${sequence}`, + alternativeText: `alternative-text #${sequence}`, + createdAt: new Date(), + updatedAt: new Date(), + type: BoardNodeType.FILE_ELEMENT, + }; + } +); diff --git a/apps/server/src/modules/board/testing/entity/index.ts b/apps/server/src/modules/board/testing/entity/index.ts new file mode 100644 index 00000000000..d5cb04e7d62 --- /dev/null +++ b/apps/server/src/modules/board/testing/entity/index.ts @@ -0,0 +1,14 @@ +export * from './card-entity.factory'; +export * from './collaborative-text-editor-entity.factory'; +export * from './column-board-entity.factory'; +export * from './column-entity.factory'; +export * from './drawing-element-entity.factory'; +export * from './external-tool-element-entity.factory'; +export * from './file-element-entity.factory'; +export * from './link-element-entity.factory'; +export * from './media-board-entity.factory'; +export * from './media-external-tool-element-entity.factory'; +export * from './media-line-entity.factory'; +export * from './rich-text-element-entity.factory'; +export * from './submission-container-element-entity.factory'; +export * from './submission-item-entity.factory'; diff --git a/apps/server/src/modules/board/testing/entity/link-element-entity.factory.ts b/apps/server/src/modules/board/testing/entity/link-element-entity.factory.ts new file mode 100644 index 00000000000..737ce08fb68 --- /dev/null +++ b/apps/server/src/modules/board/testing/entity/link-element-entity.factory.ts @@ -0,0 +1,22 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BoardNodeEntityFactory, PropsWithType } from './board-node-entity.factory'; +import { BoardNodeType, LinkElementProps, ROOT_PATH } from '../../domain'; + +export const linkElementEntityFactory = BoardNodeEntityFactory.define>( + ({ sequence }) => { + return { + id: new ObjectId().toHexString(), + path: ROOT_PATH, + level: 0, + title: `link element #${sequence}`, + position: 0, + children: [], + description: `description #${sequence}`, + url: `url #${sequence}`, + imageUrl: `image-url #${sequence}`, + createdAt: new Date(), + updatedAt: new Date(), + type: BoardNodeType.LINK_ELEMENT, + }; + } +); diff --git a/apps/server/src/modules/board/testing/entity/media-board-entity.factory.ts b/apps/server/src/modules/board/testing/entity/media-board-entity.factory.ts new file mode 100644 index 00000000000..b6050b546d0 --- /dev/null +++ b/apps/server/src/modules/board/testing/entity/media-board-entity.factory.ts @@ -0,0 +1,41 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { + BoardExternalReferenceType, + BoardLayout, + BoardNodeType, + MediaBoardColors, + MediaBoardProps, + ROOT_PATH, +} from '../../domain'; +import { Context } from '../../repo/entity/embeddables'; +import { BoardNodeEntityFactory, PropsWithType } from './board-node-entity.factory'; + +export const mediaBoardEntityFactory = BoardNodeEntityFactory.define>(({ params }) => { + const context = + params.context && params.context.type && params.context.id + ? new Context({ + type: params.context.type, + id: params.context.id, + }) + : new Context({ + type: BoardExternalReferenceType.Course, + id: new ObjectId().toHexString(), + }); + + const props: PropsWithType = { + id: new ObjectId().toHexString(), + path: ROOT_PATH, + level: 0, + position: 0, + children: [], + createdAt: new Date(), + updatedAt: new Date(), + context, + backgroundColor: MediaBoardColors.TRANSPARENT, + collapsed: false, + layout: BoardLayout.LIST, + type: BoardNodeType.MEDIA_BOARD, + }; + + return props; +}); diff --git a/apps/server/src/modules/board/testing/entity/media-external-tool-element-entity.factory.ts b/apps/server/src/modules/board/testing/entity/media-external-tool-element-entity.factory.ts new file mode 100644 index 00000000000..d6b2fae5ac1 --- /dev/null +++ b/apps/server/src/modules/board/testing/entity/media-external-tool-element-entity.factory.ts @@ -0,0 +1,21 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BoardNodeType, MediaExternalToolElementProps, ROOT_PATH } from '../../domain'; +import { BoardNodeEntityFactory, PropsWithType } from './board-node-entity.factory'; + +export const mediaExternalToolElementEntityFactory = BoardNodeEntityFactory.define< + PropsWithType +>(() => { + const props: PropsWithType = { + id: new ObjectId().toHexString(), + path: ROOT_PATH, + level: 0, + position: 0, + children: [], + createdAt: new Date(), + updatedAt: new Date(), + contextExternalToolId: new ObjectId().toHexString(), + type: BoardNodeType.MEDIA_EXTERNAL_TOOL_ELEMENT, + }; + + return props; +}); diff --git a/apps/server/src/modules/board/testing/entity/media-line-entity.factory.ts b/apps/server/src/modules/board/testing/entity/media-line-entity.factory.ts new file mode 100644 index 00000000000..3e44dd5f412 --- /dev/null +++ b/apps/server/src/modules/board/testing/entity/media-line-entity.factory.ts @@ -0,0 +1,21 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BoardNodeType, MediaBoardColors, MediaLineProps, ROOT_PATH } from '../../domain'; +import { BoardNodeEntityFactory, PropsWithType } from './board-node-entity.factory'; + +export const mediaLineEntityFactory = BoardNodeEntityFactory.define>(({ sequence }) => { + const props: PropsWithType = { + id: new ObjectId().toHexString(), + path: ROOT_PATH, + level: 0, + title: `Media-Line #${sequence}`, + position: 0, + children: [], + createdAt: new Date(), + updatedAt: new Date(), + backgroundColor: MediaBoardColors.TRANSPARENT, + collapsed: false, + type: BoardNodeType.MEDIA_LINE, + }; + + return props; +}); diff --git a/apps/server/src/modules/board/testing/entity/rich-text-element-entity.factory.ts b/apps/server/src/modules/board/testing/entity/rich-text-element-entity.factory.ts new file mode 100644 index 00000000000..6eb2a363e96 --- /dev/null +++ b/apps/server/src/modules/board/testing/entity/rich-text-element-entity.factory.ts @@ -0,0 +1,22 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { InputFormat } from '@shared/domain/types'; +import { BoardNodeType, RichTextElementProps, ROOT_PATH } from '../../domain'; +import { BoardNodeEntityFactory, PropsWithType } from './board-node-entity.factory'; + +export const richTextElementEntityFactory = BoardNodeEntityFactory.define>( + ({ sequence }) => { + return { + id: new ObjectId().toHexString(), + path: ROOT_PATH, + level: 0, + title: `rich-text #${sequence}`, + position: 0, + children: [], + text: `

text #${sequence}

`, + inputFormat: InputFormat.RICH_TEXT_CK5, + createdAt: new Date(), + updatedAt: new Date(), + type: BoardNodeType.RICH_TEXT_ELEMENT, + }; + } +); diff --git a/apps/server/src/modules/board/testing/entity/submission-container-element-entity.factory.ts b/apps/server/src/modules/board/testing/entity/submission-container-element-entity.factory.ts new file mode 100644 index 00000000000..61e6d533939 --- /dev/null +++ b/apps/server/src/modules/board/testing/entity/submission-container-element-entity.factory.ts @@ -0,0 +1,21 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BoardNodeEntityFactory, PropsWithType } from './board-node-entity.factory'; +import { BoardNodeType, ROOT_PATH, SubmissionContainerElementProps } from '../../domain'; + +export const submissionContainerElementEntityFactory = BoardNodeEntityFactory.define< + PropsWithType +>(() => { + const inThreeDays = new Date(Date.now() + 259200000); + + return { + id: new ObjectId().toHexString(), + path: ROOT_PATH, + level: 0, + position: 0, + children: [], + dueDate: inThreeDays, + createdAt: new Date(), + updatedAt: new Date(), + type: BoardNodeType.SUBMISSION_CONTAINER_ELEMENT, + }; +}); diff --git a/apps/server/src/modules/board/testing/entity/submission-item-entity.factory.ts b/apps/server/src/modules/board/testing/entity/submission-item-entity.factory.ts new file mode 100644 index 00000000000..e21e10f6ad3 --- /dev/null +++ b/apps/server/src/modules/board/testing/entity/submission-item-entity.factory.ts @@ -0,0 +1,18 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BoardNodeEntityFactory, PropsWithType } from './board-node-entity.factory'; +import { BoardNodeType, ROOT_PATH, SubmissionItemProps } from '../../domain'; + +export const submissionItemEntityFactory = BoardNodeEntityFactory.define>(() => { + return { + id: new ObjectId().toHexString(), + path: ROOT_PATH, + level: 0, + position: 0, + children: [], + completed: false, + userId: new ObjectId().toHexString(), + createdAt: new Date(), + updatedAt: new Date(), + type: BoardNodeType.SUBMISSION_ITEM, + }; +}); diff --git a/apps/server/src/shared/testing/factory/domainobject/board/external-tool-element.do.factory.ts b/apps/server/src/modules/board/testing/external-tool-element.factory.ts similarity index 53% rename from apps/server/src/shared/testing/factory/domainobject/board/external-tool-element.do.factory.ts rename to apps/server/src/modules/board/testing/external-tool-element.factory.ts index 480b5a98706..06e6ec4f5ea 100644 --- a/apps/server/src/shared/testing/factory/domainobject/board/external-tool-element.do.factory.ts +++ b/apps/server/src/modules/board/testing/external-tool-element.factory.ts @@ -1,13 +1,17 @@ -import { ExternalToolElement, ExternalToolElementProps } from '@shared/domain/domainobject'; import { ObjectId } from '@mikro-orm/mongodb'; -import { BaseFactory } from '../../base.factory'; +import { BaseFactory } from '@shared/testing'; +import { ExternalToolElement, ExternalToolElementProps, ROOT_PATH } from '../domain'; export const externalToolElementFactory = BaseFactory.define( ExternalToolElement, () => { return { id: new ObjectId().toHexString(), + path: ROOT_PATH, + level: 0, + position: 0, children: [], + contextExternalToolId: new ObjectId().toHexString(), // TODO check if this should be undefined createdAt: new Date(), updatedAt: new Date(), }; diff --git a/apps/server/src/shared/testing/factory/domainobject/board/file-element.do.factory.ts b/apps/server/src/modules/board/testing/file-element.factory.ts similarity index 53% rename from apps/server/src/shared/testing/factory/domainobject/board/file-element.do.factory.ts rename to apps/server/src/modules/board/testing/file-element.factory.ts index b8edde90b9b..5599de53251 100644 --- a/apps/server/src/shared/testing/factory/domainobject/board/file-element.do.factory.ts +++ b/apps/server/src/modules/board/testing/file-element.factory.ts @@ -1,14 +1,16 @@ -/* istanbul ignore file */ -import { FileElement, FileElementProps } from '@shared/domain/domainobject'; import { ObjectId } from '@mikro-orm/mongodb'; -import { BaseFactory } from '../../base.factory'; +import { BaseFactory } from '@shared/testing'; +import { FileElement, FileElementProps, ROOT_PATH } from '../domain'; export const fileElementFactory = BaseFactory.define(FileElement, ({ sequence }) => { return { id: new ObjectId().toHexString(), + path: ROOT_PATH, + level: 0, + position: 0, children: [], - caption: `

caption #${sequence}

`, - alternativeText: `alternativeText #${sequence}`, + caption: `file #${sequence}`, + alternativeText: `alternative-text #${sequence}`, createdAt: new Date(), updatedAt: new Date(), }; diff --git a/apps/server/src/modules/board/testing/index.ts b/apps/server/src/modules/board/testing/index.ts new file mode 100644 index 00000000000..7e6e20039e8 --- /dev/null +++ b/apps/server/src/modules/board/testing/index.ts @@ -0,0 +1,18 @@ +export * from './board-node-authorizable.factory'; +export * from './card.factory'; +export * from './collaborative-text-editor.factory'; +export * from './column-board.factory'; +export * from './column.factory'; +export * from './drawing-element.factory'; +export * from './entity'; +export * from './external-tool-element.factory'; +export * from './file-element.factory'; +export * from './link-element.factory'; +export * from './media-available-line.factory'; +export * from './media-available-line-element.factory'; +export * from './media-board.factory'; +export * from './media-external-tool-element.factory'; +export * from './media-line.factory'; +export * from './rich-text-element.factory'; +export * from './submission-container-element.factory'; +export * from './submission-item.factory'; diff --git a/apps/server/src/modules/board/testing/link-element.factory.ts b/apps/server/src/modules/board/testing/link-element.factory.ts new file mode 100644 index 00000000000..257d1f23f72 --- /dev/null +++ b/apps/server/src/modules/board/testing/link-element.factory.ts @@ -0,0 +1,19 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BaseFactory } from '@shared/testing'; +import { LinkElement, LinkElementProps, ROOT_PATH } from '../domain'; + +export const linkElementFactory = BaseFactory.define(LinkElement, ({ sequence }) => { + return { + id: new ObjectId().toHexString(), + path: ROOT_PATH, + level: 0, + title: `link element #${sequence}`, + position: 0, + children: [], + description: `description #${sequence}`, + url: `url #${sequence}`, + imageUrl: `image-url #${sequence}`, + createdAt: new Date(), + updatedAt: new Date(), + }; +}); diff --git a/apps/server/src/shared/testing/factory/domainobject/board/media-available-line-element.do.factory.ts b/apps/server/src/modules/board/testing/media-available-line-element.factory.ts similarity index 82% rename from apps/server/src/shared/testing/factory/domainobject/board/media-available-line-element.do.factory.ts rename to apps/server/src/modules/board/testing/media-available-line-element.factory.ts index 3b24638e0a5..d1615fc2239 100644 --- a/apps/server/src/shared/testing/factory/domainobject/board/media-available-line-element.do.factory.ts +++ b/apps/server/src/modules/board/testing/media-available-line-element.factory.ts @@ -1,6 +1,6 @@ +import { BaseFactory } from '@shared/testing'; import { ObjectId } from '@mikro-orm/mongodb'; -import { MediaAvailableLineElement, MediaAvailableLineElementProps } from '@shared/domain/domainobject'; -import { BaseFactory } from '../../base.factory'; +import { MediaAvailableLineElement, MediaAvailableLineElementProps } from '../domain'; export const mediaAvailableLineElementFactory = BaseFactory.define< MediaAvailableLineElement, diff --git a/apps/server/src/shared/testing/factory/domainobject/board/media-available-line.do.factory.ts b/apps/server/src/modules/board/testing/media-available-line.factory.ts similarity index 69% rename from apps/server/src/shared/testing/factory/domainobject/board/media-available-line.do.factory.ts rename to apps/server/src/modules/board/testing/media-available-line.factory.ts index f9124850d68..ba9ee9e716b 100644 --- a/apps/server/src/shared/testing/factory/domainobject/board/media-available-line.do.factory.ts +++ b/apps/server/src/modules/board/testing/media-available-line.factory.ts @@ -1,7 +1,6 @@ -import { MediaBoardColors } from '@modules/board/domain'; -import { MediaAvailableLine, MediaAvailableLineElement, MediaAvailableLineProps } from '@shared/domain/domainobject'; import { DeepPartial } from 'fishery'; -import { BaseFactory } from '../../base.factory'; +import { BaseFactory } from '@shared/testing'; +import { MediaBoardColors, MediaAvailableLine, MediaAvailableLineElement, MediaAvailableLineProps } from '../domain'; class MediaAvailableLineFactory extends BaseFactory { withElement(element: MediaAvailableLineElement): this { diff --git a/apps/server/src/modules/board/testing/media-board.factory.ts b/apps/server/src/modules/board/testing/media-board.factory.ts new file mode 100644 index 00000000000..3bbd8a705a0 --- /dev/null +++ b/apps/server/src/modules/board/testing/media-board.factory.ts @@ -0,0 +1,31 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BaseFactory } from '@shared/testing'; +import { + BoardExternalReferenceType, + BoardLayout, + MediaBoard, + MediaBoardColors, + MediaBoardProps, + ROOT_PATH, +} from '../domain'; + +export const mediaBoardFactory = BaseFactory.define(MediaBoard, () => { + const props: MediaBoardProps = { + id: new ObjectId().toHexString(), + path: ROOT_PATH, + level: 0, + position: 0, + children: [], + createdAt: new Date(), + updatedAt: new Date(), + context: { + type: BoardExternalReferenceType.Course, + id: new ObjectId().toHexString(), + }, + backgroundColor: MediaBoardColors.TRANSPARENT, + collapsed: false, + layout: BoardLayout.LIST, + }; + + return props; +}); diff --git a/apps/server/src/shared/testing/factory/domainobject/board/media-external-tool-element.do.factory.ts b/apps/server/src/modules/board/testing/media-external-tool-element.factory.ts similarity index 59% rename from apps/server/src/shared/testing/factory/domainobject/board/media-external-tool-element.do.factory.ts rename to apps/server/src/modules/board/testing/media-external-tool-element.factory.ts index 092b9021fc9..cb1dbf4c2fc 100644 --- a/apps/server/src/shared/testing/factory/domainobject/board/media-external-tool-element.do.factory.ts +++ b/apps/server/src/modules/board/testing/media-external-tool-element.factory.ts @@ -1,16 +1,21 @@ import { ObjectId } from '@mikro-orm/mongodb'; -import { MediaExternalToolElement, type MediaExternalToolElementProps } from '@shared/domain/domainobject'; -import { BaseFactory } from '../../base.factory'; +import { BaseFactory } from '@shared/testing'; +import { MediaExternalToolElement, MediaExternalToolElementProps, ROOT_PATH } from '../domain'; export const mediaExternalToolElementFactory = BaseFactory.define< MediaExternalToolElement, MediaExternalToolElementProps >(MediaExternalToolElement, () => { - return { + const props: MediaExternalToolElementProps = { id: new ObjectId().toHexString(), + path: ROOT_PATH, + level: 0, + position: 0, children: [], createdAt: new Date(), updatedAt: new Date(), contextExternalToolId: new ObjectId().toHexString(), }; + + return props; }); diff --git a/apps/server/src/modules/board/testing/media-line.factory.ts b/apps/server/src/modules/board/testing/media-line.factory.ts new file mode 100644 index 00000000000..2f2c7bf3395 --- /dev/null +++ b/apps/server/src/modules/board/testing/media-line.factory.ts @@ -0,0 +1,21 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BaseFactory } from '@shared/testing'; +import { MediaLine, MediaLineProps, ROOT_PATH } from '../domain'; +import { MediaBoardColors } from '../domain/media-board/types'; + +export const mediaLineFactory = BaseFactory.define(MediaLine, ({ sequence }) => { + const props: MediaLineProps = { + id: new ObjectId().toHexString(), + path: ROOT_PATH, + level: 0, + title: `Media-Line #${sequence}`, + position: 0, + children: [], + createdAt: new Date(), + updatedAt: new Date(), + backgroundColor: MediaBoardColors.TRANSPARENT, + collapsed: false, + }; + + return props; +}); diff --git a/apps/server/src/shared/testing/factory/domainobject/board/rich-text-element.do.factory.ts b/apps/server/src/modules/board/testing/rich-text-element.factory.ts similarity index 66% rename from apps/server/src/shared/testing/factory/domainobject/board/rich-text-element.do.factory.ts rename to apps/server/src/modules/board/testing/rich-text-element.factory.ts index 802d179a52a..215c1d53326 100644 --- a/apps/server/src/shared/testing/factory/domainobject/board/rich-text-element.do.factory.ts +++ b/apps/server/src/modules/board/testing/rich-text-element.factory.ts @@ -1,15 +1,17 @@ -/* istanbul ignore file */ -import { RichTextElement, RichTextElementProps } from '@shared/domain/domainobject'; -import { InputFormat } from '@shared/domain/types'; import { ObjectId } from '@mikro-orm/mongodb'; -import { BaseFactory } from '../../base.factory'; +import { InputFormat } from '@shared/domain/types'; +import { BaseFactory } from '@shared/testing/factory/base.factory'; +import { RichTextElement, RichTextElementProps, ROOT_PATH } from '../domain'; export const richTextElementFactory = BaseFactory.define( RichTextElement, ({ sequence }) => { return { id: new ObjectId().toHexString(), - title: `element #${sequence}`, + path: ROOT_PATH, + level: 0, + title: `rich-text #${sequence}`, + position: 0, children: [], text: `

text #${sequence}

`, inputFormat: InputFormat.RICH_TEXT_CK5, diff --git a/apps/server/src/shared/testing/factory/domainobject/board/submission-container-element.do.factory.ts b/apps/server/src/modules/board/testing/submission-container-element.factory.ts similarity index 58% rename from apps/server/src/shared/testing/factory/domainobject/board/submission-container-element.do.factory.ts rename to apps/server/src/modules/board/testing/submission-container-element.factory.ts index b4950ed4ccf..42d67af9176 100644 --- a/apps/server/src/shared/testing/factory/domainobject/board/submission-container-element.do.factory.ts +++ b/apps/server/src/modules/board/testing/submission-container-element.factory.ts @@ -1,16 +1,18 @@ -/* istanbul ignore file */ -import { SubmissionContainerElement, SubmissionContainerElementProps } from '@shared/domain/domainobject'; import { ObjectId } from '@mikro-orm/mongodb'; -import { BaseFactory } from '../../base.factory'; +import { BaseFactory } from '@shared/testing'; +import { ROOT_PATH, SubmissionContainerElement, SubmissionContainerElementProps } from '../domain'; export const submissionContainerElementFactory = BaseFactory.define< SubmissionContainerElement, SubmissionContainerElementProps ->(SubmissionContainerElement, ({ sequence }) => { +>(SubmissionContainerElement, () => { const inThreeDays = new Date(Date.now() + 259200000); + return { id: new ObjectId().toHexString(), - title: `element #${sequence}`, + path: ROOT_PATH, + level: 0, + position: 0, children: [], dueDate: inThreeDays, createdAt: new Date(), diff --git a/apps/server/src/modules/board/testing/submission-item.factory.ts b/apps/server/src/modules/board/testing/submission-item.factory.ts new file mode 100644 index 00000000000..cc01e1cb114 --- /dev/null +++ b/apps/server/src/modules/board/testing/submission-item.factory.ts @@ -0,0 +1,17 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BaseFactory } from '@shared/testing'; +import { ROOT_PATH, SubmissionItem, SubmissionItemProps } from '../domain'; + +export const submissionItemFactory = BaseFactory.define(SubmissionItem, () => { + return { + id: new ObjectId().toHexString(), + path: ROOT_PATH, + level: 0, + position: 0, + children: [], + completed: false, + userId: new ObjectId().toHexString(), + createdAt: new Date(), + updatedAt: new Date(), + }; +}); diff --git a/apps/server/src/modules/board/uc/base.uc.ts b/apps/server/src/modules/board/uc/base.uc.ts deleted file mode 100644 index e4d8ec21fa0..00000000000 --- a/apps/server/src/modules/board/uc/base.uc.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Action, AuthorizationService } from '@modules/authorization'; -import { AnyBoardDo, BoardRoles, UserWithBoardRoles } from '@shared/domain/domainobject'; -import { EntityId } from '@shared/domain/types'; -import { Permission } from '@shared/domain/interface'; -import { BoardDoAuthorizableService } from '../service'; - -export abstract class BaseUc { - constructor( - protected readonly authorizationService: AuthorizationService, - protected readonly boardDoAuthorizableService: BoardDoAuthorizableService - ) {} - - protected async checkPermission(userId: EntityId, anyBoardDo: AnyBoardDo, action: Action): Promise { - const requiredPermissions: Permission[] = []; - const user = await this.authorizationService.getUserWithPermissions(userId); - const boardDoAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable(anyBoardDo); - - this.authorizationService.checkPermission(user, boardDoAuthorizable, { action, requiredPermissions }); - } - - protected isUserBoardEditor(userId: EntityId, userBoardRoles: UserWithBoardRoles[]): boolean { - const boardDoAuthorisedUser = userBoardRoles.find((user) => user.userId === userId); - - if (boardDoAuthorisedUser) { - return boardDoAuthorisedUser?.roles.includes(BoardRoles.EDITOR); - } - - return false; - } - - protected isUserBoardReader(userId: EntityId, userBoardRoles: UserWithBoardRoles[]): boolean { - const boardDoAuthorisedUser = userBoardRoles.find((user) => user.userId === userId); - - if (boardDoAuthorisedUser) { - return ( - boardDoAuthorisedUser.roles.includes(BoardRoles.READER) && - !boardDoAuthorisedUser.roles.includes(BoardRoles.EDITOR) - ); - } - - return false; - } -} diff --git a/apps/server/src/modules/board/uc/board.uc.spec.ts b/apps/server/src/modules/board/uc/board.uc.spec.ts index 3fcde3b1754..75c2dafc23e 100644 --- a/apps/server/src/modules/board/uc/board.uc.spec.ts +++ b/apps/server/src/modules/board/uc/board.uc.spec.ts @@ -1,25 +1,27 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; import { Action, AuthorizationService } from '@modules/authorization'; -import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardDoAuthorizable, BoardRoles, ContentElementType } from '@shared/domain/domainobject'; +import { Permission } from '@shared/domain/interface'; import { CourseRepo } from '@shared/repo'; import { setupEntities, userFactory } from '@shared/testing'; -import { columnBoardFactory, columnFactory } from '@shared/testing/factory/domainobject'; +import { courseFactory } from '@shared/testing/factory'; import { LegacyLogger } from '@src/core/logger'; -import { ObjectId } from '@mikro-orm/mongodb'; -import { BoardDoAuthorizableService, ColumnBoardService, ColumnService, ContentElementService } from '../service'; -import { ColumnBoardCopyService } from '../service/column-board-copy.service'; +import { CopyElementType, CopyStatus, CopyStatusEnum } from '../../copy-helper'; +import { BoardExternalReferenceType, BoardLayout, BoardNodeFactory, Column, ColumnBoard } from '../domain'; +import { BoardNodePermissionService, BoardNodeService, ColumnBoardService } from '../service'; +import { columnBoardFactory, columnFactory } from '../testing'; import { BoardUc } from './board.uc'; describe(BoardUc.name, () => { let module: TestingModule; let uc: BoardUc; let authorizationService: DeepMocked; - let boardDoAuthorizableService: DeepMocked; + let boardPermissionService: DeepMocked; + let boardNodeService: DeepMocked; let columnBoardService: DeepMocked; - let columnBoardCopyService: DeepMocked; - let columnService: DeepMocked; + let courseRepo: DeepMocked; + let boardNodeFactory: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -30,46 +32,39 @@ describe(BoardUc.name, () => { useValue: createMock(), }, { - provide: BoardDoAuthorizableService, - useValue: createMock(), - }, - { - provide: ColumnBoardService, - useValue: createMock(), + provide: BoardNodePermissionService, + useValue: createMock(), }, { - provide: ColumnBoardCopyService, - useValue: createMock(), + provide: BoardNodeService, + useValue: createMock(), }, { - provide: ColumnService, - useValue: createMock(), + provide: ColumnBoardService, + useValue: createMock(), }, { provide: CourseRepo, useValue: createMock(), }, { - provide: LegacyLogger, - useValue: createMock(), + provide: BoardNodeFactory, + useValue: createMock(), }, { - provide: ContentElementService, - useValue: createMock(), - }, - { - provide: CourseRepo, - useValue: createMock(), + provide: LegacyLogger, + useValue: createMock(), }, ], }).compile(); uc = module.get(BoardUc); authorizationService = module.get(AuthorizationService); - boardDoAuthorizableService = module.get(BoardDoAuthorizableService); + boardPermissionService = module.get(BoardNodePermissionService); + boardNodeService = module.get(BoardNodeService); columnBoardService = module.get(ColumnBoardService); - columnBoardCopyService = module.get(ColumnBoardCopyService); - columnService = module.get(ColumnService); + courseRepo = module.get(CourseRepo); + boardNodeFactory = module.get(BoardNodeFactory); await setupEntities(); }); @@ -87,74 +82,197 @@ describe(BoardUc.name, () => { const board = columnBoardFactory.build(); const boardId = board.id; const column = columnFactory.build(); - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - const authorizableMock: BoardDoAuthorizable = new BoardDoAuthorizable({ - users: [{ userId: user.id, roles: [BoardRoles.EDITOR] }], - id: board.id, - boardDo: column, - rootDo: board, - }); - const createCardBodyParams = { - requiredEmptyElements: [ContentElementType.FILE, ContentElementType.RICH_TEXT], + return { user, board, boardId, column }; // createCardBodyParams + }; + + describe('createBoard', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const course = courseFactory.build(); + + return { user, course }; }; + describe('when creating a board', () => { + it('should call the service to find the user', async () => { + const { user } = setup(); + + await uc.createBoard(user.id, { + title: 'new board', + layout: BoardLayout.COLUMNS, + parentId: new ObjectId().toHexString(), + parentType: BoardExternalReferenceType.Course, + }); - boardDoAuthorizableService.findById.mockResolvedValueOnce(authorizableMock); + expect(authorizationService.getUserWithPermissions).toHaveBeenCalledWith(user.id); + }); - return { user, board, boardId, column, createCardBodyParams }; - }; + it('should call the service to find the course', async () => { + const { user } = setup(); - describe('findBoard', () => { - describe('when loading a board and having required permission', () => { - it('should call the service', async () => { - const { user, boardId } = globalSetup(); + const courseId = new ObjectId().toHexString(); - await uc.findBoard(user.id, boardId); + await uc.createBoard(user.id, { + title: 'new board', + layout: BoardLayout.COLUMNS, + parentId: courseId, + parentType: BoardExternalReferenceType.Course, + }); - expect(columnBoardService.findById).toHaveBeenCalledWith(boardId); + expect(courseRepo.findById).toHaveBeenCalledWith(courseId); }); - it('should return the column board object', async () => { - const { user, board } = globalSetup(); - columnBoardService.findById.mockResolvedValueOnce(board); + it('should call the authorization service to check the permissions', async () => { + const { user, course } = setup(); - const result = await uc.findBoard(user.id, board.id); + courseRepo.findById.mockResolvedValueOnce(course); - expect(result).toEqual(board); + await uc.createBoard(user.id, { + title: 'new board', + layout: BoardLayout.COLUMNS, + parentId: course.id, + parentType: BoardExternalReferenceType.Course, + }); + + expect(authorizationService.checkPermission).toHaveBeenCalledWith(user, course, { + action: Action.write, + requiredPermissions: [Permission.COURSE_EDIT], + }); + }); + + it('should call factory to build board', async () => { + const { user, course } = setup(); + + await uc.createBoard(user.id, { + title: 'new board', + layout: BoardLayout.COLUMNS, + parentId: course.id, + parentType: BoardExternalReferenceType.Course, + }); + + expect(boardNodeFactory.buildColumnBoard).toHaveBeenCalledWith({ + context: { type: BoardExternalReferenceType.Course, id: course.id }, + layout: BoardLayout.COLUMNS, + title: 'new board', + }); + }); + + it('should call the service to create the board', async () => { + const { user } = setup(); + + const board = columnBoardFactory.build(); + boardNodeFactory.buildColumnBoard.mockReturnValueOnce(board); + + await uc.createBoard(user.id, { + title: 'new board', + layout: BoardLayout.COLUMNS, + parentId: new ObjectId().toHexString(), + parentType: BoardExternalReferenceType.Course, + }); + + expect(boardNodeService.addRoot).toHaveBeenCalledWith(board); }); - }); - describe('when loading a board without having permissions', () => { it('should return the column board object', async () => { - const { board } = globalSetup(); - columnBoardService.findById.mockResolvedValueOnce(board); + const { user } = setup(); + + const board = columnBoardFactory.build(); + boardNodeFactory.buildColumnBoard.mockReturnValueOnce(board); - const fakeUserId = new ObjectId().toHexString(); - authorizationService.checkPermission.mockImplementationOnce(() => { - throw new ForbiddenException(); + const result = await uc.createBoard(user.id, { + title: 'new board', + layout: BoardLayout.COLUMNS, + parentId: new ObjectId().toHexString(), + parentType: BoardExternalReferenceType.Course, }); - await expect(uc.findBoard(fakeUserId, board.id)).rejects.toThrow(ForbiddenException); + expect(result).toEqual(board); }); }); }); + describe('findBoard', () => { + it('should call the Board Node Service to find board ', async () => { + const { user, boardId } = globalSetup(); + + await uc.findBoard(user.id, boardId); + + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(ColumnBoard, boardId); + }); + + it('should call Board Permission Service to check permission', async () => { + const { user, board } = globalSetup(); + boardNodeService.findByClassAndId.mockResolvedValueOnce(board); + + await uc.findBoard(user.id, board.id); + + expect(boardPermissionService.checkPermission).toHaveBeenCalledWith(user.id, board, Action.read); + }); + + it('should return the column board object', async () => { + const { user, board } = globalSetup(); + boardNodeService.findByClassAndId.mockResolvedValueOnce(board); + + const result = await uc.findBoard(user.id, board.id); + + expect(result).toEqual(board); + }); + }); + + describe('findBoardContext', () => { + it('should call the Board Node Service to find board ', async () => { + const { user, boardId } = globalSetup(); + + await uc.findBoardContext(user.id, boardId); + + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(ColumnBoard, boardId); + }); + + it('should call Board Permission Service to check permission', async () => { + const { user, board } = globalSetup(); + boardNodeService.findByClassAndId.mockResolvedValueOnce(board); + + await uc.findBoardContext(user.id, board.id); + + expect(boardPermissionService.checkPermission).toHaveBeenCalledWith(user.id, board, Action.read); + }); + + it('should return the context object', async () => { + const { user, board } = globalSetup(); + boardNodeService.findByClassAndId.mockResolvedValueOnce(board); + + const result = await uc.findBoardContext(user.id, board.id); + + expect(result).toEqual(board.context); + }); + }); + describe('deleteBoard', () => { describe('when deleting a board', () => { - it('should call the service to find the board', async () => { + it('should call the Board Node Service to find board ', async () => { + const { user, boardId } = globalSetup(); + + await uc.deleteBoard(user.id, boardId); + + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(ColumnBoard, boardId); + }); + + it('should call Board Permission Service to check permission', async () => { const { user, board } = globalSetup(); + boardNodeService.findByClassAndId.mockResolvedValueOnce(board); await uc.deleteBoard(user.id, board.id); - expect(columnBoardService.findById).toHaveBeenCalledWith(board.id); + expect(boardPermissionService.checkPermission).toHaveBeenCalledWith(user.id, board, Action.write); }); it('should call the service to delete the board', async () => { const { user, board } = globalSetup(); + boardNodeService.findByClassAndId.mockResolvedValueOnce(board); await uc.deleteBoard(user.id, board.id); - expect(columnBoardService.delete).toHaveBeenCalledWith(board); + expect(boardNodeService.delete).toHaveBeenCalledWith(board); }); }); }); @@ -166,16 +284,26 @@ describe(BoardUc.name, () => { await uc.updateBoardTitle(user.id, board.id, 'new title'); - expect(columnBoardService.findById).toHaveBeenCalledWith(board.id); + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(ColumnBoard, board.id); + }); + + it('should call the service to check the permissions', async () => { + const { user, board } = globalSetup(); + boardNodeService.findByClassAndId.mockResolvedValueOnce(board); + + await uc.updateBoardTitle(user.id, board.id, 'new title'); + + expect(boardPermissionService.checkPermission).toHaveBeenCalledWith(user.id, board, Action.write); }); it('should call the service to update the board title', async () => { const { user, board } = globalSetup(); const newTitle = 'new title'; + boardNodeService.findByClassAndId.mockResolvedValueOnce(board); await uc.updateBoardTitle(user.id, board.id, newTitle); - expect(columnBoardService.updateTitle).toHaveBeenCalledWith(board, newTitle); + expect(boardNodeService.updateTitle).toHaveBeenCalledWith(board, newTitle); }); }); }); @@ -187,21 +315,39 @@ describe(BoardUc.name, () => { await uc.createColumn(user.id, board.id); - expect(columnBoardService.findById).toHaveBeenCalledWith(board.id); + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(ColumnBoard, board.id); + }); + + it('should call the service to check the permissions', async () => { + const { user, board } = globalSetup(); + boardNodeService.findByClassAndId.mockResolvedValueOnce(board); + + await uc.createColumn(user.id, board.id); + + expect(boardPermissionService.checkPermission).toHaveBeenCalledWith(user.id, board, Action.write); }); - it('should call the service to create the column', async () => { + it('should call the factory to build column', async () => { const { user, board } = globalSetup(); - columnBoardService.findById.mockResolvedValueOnce(board); await uc.createColumn(user.id, board.id); - expect(columnService.create).toHaveBeenCalledWith(board); + expect(boardNodeFactory.buildColumn).toHaveBeenCalled(); + }); + + it('should call the board node service to add the column to board', async () => { + const { user, board, column } = globalSetup(); + boardNodeService.findByClassAndId.mockResolvedValueOnce(board); + boardNodeFactory.buildColumn.mockReturnValueOnce(column); + + await uc.createColumn(user.id, board.id); + + expect(boardNodeService.addToParent).toHaveBeenCalledWith(board, column); }); it('should return the column board object', async () => { const { user, board, column } = globalSetup(); - columnService.create.mockResolvedValueOnce(column); + boardNodeFactory.buildColumn.mockReturnValueOnce(column); const result = await uc.createColumn(user.id, board.id); @@ -217,7 +363,7 @@ describe(BoardUc.name, () => { await uc.moveColumn(user.id, column.id, board.id, 7); - expect(columnService.findById).toHaveBeenCalledWith(column.id); + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(Column, column.id); }); it('should call the service to find the target board', async () => { @@ -225,31 +371,114 @@ describe(BoardUc.name, () => { await uc.moveColumn(user.id, column.id, board.id, 7); - expect(columnBoardService.findById).toHaveBeenCalledWith(board.id); + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(ColumnBoard, board.id); + }); + + it('should call the service to check the permissions for column', async () => { + const { user, board, column } = globalSetup(); + boardNodeService.findByClassAndId.mockResolvedValueOnce(board).mockResolvedValueOnce(column); + + await uc.moveColumn(user.id, column.id, board.id, 1); + + expect(boardPermissionService.checkPermission).toHaveBeenCalledWith(user.id, column, Action.write); + }); + + it('should call the service to check the permissions for target board', async () => { + const { user, board, column } = globalSetup(); + boardNodeService.findByClassAndId.mockResolvedValueOnce(board).mockResolvedValueOnce(column); + + await uc.moveColumn(user.id, column.id, board.id, 1); + + expect(boardPermissionService.checkPermission).toHaveBeenCalledWith(user.id, board, Action.write); }); it('should call the service to move the column', async () => { const { user, board, column } = globalSetup(); - columnService.findById.mockResolvedValueOnce(column); - columnBoardService.findById.mockResolvedValueOnce(board); + // TODO think about using jest-when + @types/jest-when + // https://github.com/timkindberg/jest-when + // https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/jest-when + boardNodeService.findByClassAndId.mockResolvedValueOnce(column); + boardNodeService.findByClassAndId.mockResolvedValueOnce(board); - await uc.moveColumn(user.id, board.id, column.id, 7); + await uc.moveColumn(user.id, column.id, board.id, 7); - expect(columnService.move).toHaveBeenCalledWith(column, board, 7); + expect(boardNodeService.move).toHaveBeenCalledWith(column, board, 7); }); }); }); describe('copyBoard', () => { + it('should call the service to find the user', async () => { + const { user, boardId } = globalSetup(); + + await uc.copyBoard(user.id, boardId); + + expect(authorizationService.getUserWithPermissions).toHaveBeenCalledWith(user.id); + }); + + it('should call the service to find the board', async () => { + const { user, boardId } = globalSetup(); + + await uc.copyBoard(user.id, boardId); + + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(ColumnBoard, boardId); + }); + + it('[deprecated] should call course repo to find the course', async () => { + const { user, boardId } = globalSetup(); + + await uc.copyBoard(user.id, boardId); + + expect(courseRepo.findById).toHaveBeenCalled(); + }); + + it('should call Board Permission Service to check permission', async () => { + const { user, board } = globalSetup(); + boardNodeService.findByClassAndId.mockResolvedValueOnce(board); + + await uc.copyBoard(user.id, board.id); + + expect(boardPermissionService.checkPermission).toHaveBeenCalledWith(user.id, board, Action.read); + }); + + it('should call authorization to check course permissions', async () => { + const { user, boardId } = globalSetup(); + + const course = courseFactory.build(); + // TODO should not use course repo + courseRepo.findById.mockResolvedValueOnce(course); + + await uc.copyBoard(user.id, boardId); + + expect(authorizationService.checkPermission).toHaveBeenCalledWith(user, course, { + action: Action.write, + requiredPermissions: [], + }); + }); + it('should call the service to copy the board', async () => { const { user, boardId } = globalSetup(); await uc.copyBoard(user.id, boardId); - expect(columnBoardCopyService.copyColumnBoard).toHaveBeenCalledWith( + expect(columnBoardService.copyColumnBoard).toHaveBeenCalledWith( expect.objectContaining({ userId: user.id, originalColumnBoardId: boardId }) ); }); + + it('should return the copy status', async () => { + const { user, boardId } = globalSetup(); + + const copyStatus: CopyStatus = { + type: CopyElementType.BOARD, + status: CopyStatusEnum.SUCCESS, + }; + columnBoardService.copyColumnBoard.mockResolvedValueOnce(copyStatus); + + const result = await uc.copyBoard(user.id, boardId); + + expect(result).toEqual(copyStatus); + }); }); describe('updateVisibility', () => { @@ -265,26 +494,16 @@ describe(BoardUc.name, () => { await uc.updateVisibility(user.id, board.id, true); - expect(columnBoardService.findById).toHaveBeenCalledWith(board.id); + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(ColumnBoard, board.id); }); - it('should authorize', async () => { + it('should call the service to check the permissions', async () => { const { user, board } = setup(); - - columnBoardService.findById.mockResolvedValueOnce(board); - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - const mockAuthorizable = new BoardDoAuthorizable({ - users: [{ userId: user.id, roles: [BoardRoles.EDITOR] }], - id: board.id, - boardDo: board, - rootDo: board, - }); - boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce(mockAuthorizable); + boardNodeService.findByClassAndId.mockResolvedValueOnce(board); await uc.updateVisibility(user.id, board.id, true); - const context = { action: Action.write, requiredPermissions: [] }; - expect(authorizationService.checkPermission).toBeCalledWith(user, mockAuthorizable, context); + expect(boardPermissionService.checkPermission).toHaveBeenCalledWith(user.id, board, Action.write); }); it('should call the service to update the board visibility', async () => { @@ -292,7 +511,7 @@ describe(BoardUc.name, () => { await uc.updateVisibility(user.id, board.id, true); - expect(columnBoardService.updateBoardVisibility).toHaveBeenCalledWith(board.id, true); + expect(boardNodeService.updateVisibility).toHaveBeenCalledWith(board.id, true); }); }); }); diff --git a/apps/server/src/modules/board/uc/board.uc.ts b/apps/server/src/modules/board/uc/board.uc.ts index 70fd510991c..4490d54f206 100644 --- a/apps/server/src/modules/board/uc/board.uc.ts +++ b/apps/server/src/modules/board/uc/board.uc.ts @@ -1,30 +1,26 @@ import { Action, AuthorizationService } from '@modules/authorization'; +import { CopyStatus } from '@modules/copy-helper'; import { forwardRef, Inject, Injectable } from '@nestjs/common'; -import { BoardExternalReference, Column, ColumnBoard } from '@shared/domain/domainobject'; import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { CourseRepo } from '@shared/repo'; import { LegacyLogger } from '@src/core/logger'; -import { CopyStatus } from '@src/modules/copy-helper'; import { CreateBoardBodyParams } from '../controller/dto'; -import { ColumnBoardService, ColumnService } from '../service'; -import { BoardDoAuthorizableService } from '../service/board-do-authorizable.service'; -import { ColumnBoardCopyService } from '../service/column-board-copy.service'; -import { BaseUc } from './base.uc'; +import { BoardExternalReference, BoardNodeFactory, Column, ColumnBoard } from '../domain'; +import { BoardNodePermissionService, BoardNodeService, ColumnBoardService } from '../service'; @Injectable() -export class BoardUc extends BaseUc { +export class BoardUc { constructor( - @Inject(forwardRef(() => AuthorizationService)) - protected readonly authorizationService: AuthorizationService, - protected readonly boardDoAuthorizableService: BoardDoAuthorizableService, + @Inject(forwardRef(() => AuthorizationService)) // TODO is this needed? + private readonly authorizationService: AuthorizationService, + private readonly boardPermissionService: BoardNodePermissionService, + private readonly boardNodeService: BoardNodeService, private readonly columnBoardService: ColumnBoardService, - private readonly columnBoardCopyService: ColumnBoardCopyService, - private readonly columnService: ColumnService, private readonly logger: LegacyLogger, - private readonly courseRepo: CourseRepo + private readonly courseRepo: CourseRepo, + private readonly boardNodeFactory: BoardNodeFactory ) { - super(authorizationService, boardDoAuthorizableService); this.logger.setContext(BoardUc.name); } @@ -39,9 +35,13 @@ export class BoardUc extends BaseUc { requiredPermissions: [Permission.COURSE_EDIT], }); - const context = { type: params.parentType, id: params.parentId }; + const board = this.boardNodeFactory.buildColumnBoard({ + context: { type: params.parentType, id: params.parentId }, + title: params.title, + layout: params.layout, + }); - const board = await this.columnBoardService.create(context, params.layout, params.title); + await this.boardNodeService.addRoot(board); return board; } @@ -49,8 +49,9 @@ export class BoardUc extends BaseUc { async findBoard(userId: EntityId, boardId: EntityId): Promise { this.logger.debug({ action: 'findBoard', userId, boardId }); - const board = await this.columnBoardService.findById(boardId); - await this.checkPermission(userId, board, Action.read); + // TODO set depth=2 to reduce data? + const board = await this.boardNodeService.findByClassAndId(ColumnBoard, boardId); + await this.boardPermissionService.checkPermission(userId, board, Action.read); return board; } @@ -58,40 +59,40 @@ export class BoardUc extends BaseUc { async findBoardContext(userId: EntityId, boardId: EntityId): Promise { this.logger.debug({ action: 'findBoardContext', userId, boardId }); - const board = await this.columnBoardService.findById(boardId); - await this.checkPermission(userId, board, Action.read); + const board = await this.boardNodeService.findByClassAndId(ColumnBoard, boardId); + await this.boardPermissionService.checkPermission(userId, board, Action.read); return board.context; } - async deleteBoard(userId: EntityId, boardId: EntityId): Promise { + async deleteBoard(userId: EntityId, boardId: EntityId): Promise { this.logger.debug({ action: 'deleteBoard', userId, boardId }); - const board = await this.columnBoardService.findById(boardId); - await this.checkPermission(userId, board, Action.write); - - await this.columnBoardService.delete(board); + const board = await this.boardNodeService.findByClassAndId(ColumnBoard, boardId); + await this.boardPermissionService.checkPermission(userId, board, Action.write); - return board; + await this.boardNodeService.delete(board); } - async updateBoardTitle(userId: EntityId, boardId: EntityId, title: string): Promise { + async updateBoardTitle(userId: EntityId, boardId: EntityId, title: string): Promise { this.logger.debug({ action: 'updateBoardTitle', userId, boardId, title }); - const board = await this.columnBoardService.findById(boardId); - await this.checkPermission(userId, board, Action.write); + const board = await this.boardNodeService.findByClassAndId(ColumnBoard, boardId); + await this.boardPermissionService.checkPermission(userId, board, Action.write); - await this.columnBoardService.updateTitle(board, title); - return board; + await this.boardNodeService.updateTitle(board, title); } async createColumn(userId: EntityId, boardId: EntityId): Promise { this.logger.debug({ action: 'createColumn', userId, boardId }); - const board = await this.columnBoardService.findById(boardId); - await this.checkPermission(userId, board, Action.write); + const board = await this.boardNodeService.findByClassAndId(ColumnBoard, boardId); + await this.boardPermissionService.checkPermission(userId, board, Action.write); + + const column = this.boardNodeFactory.buildColumn(); + + await this.boardNodeService.addToParent(board, column); - const column = await this.columnService.create(board); return column; } @@ -100,33 +101,34 @@ export class BoardUc extends BaseUc { columnId: EntityId, targetBoardId: EntityId, targetPosition: number - ): Promise { + ): Promise { this.logger.debug({ action: 'moveColumn', userId, columnId, targetBoardId, targetPosition }); - const column = await this.columnService.findById(columnId); - const targetBoard = await this.columnBoardService.findById(targetBoardId); + const column = await this.boardNodeService.findByClassAndId(Column, columnId); + const targetBoard = await this.boardNodeService.findByClassAndId(ColumnBoard, targetBoardId); - await this.checkPermission(userId, column, Action.write); - await this.checkPermission(userId, targetBoard, Action.write); + await this.boardPermissionService.checkPermission(userId, column, Action.write); + await this.boardPermissionService.checkPermission(userId, targetBoard, Action.write); - await this.columnService.move(column, targetBoard, targetPosition); - return column; + await this.boardNodeService.move(column, targetBoard, targetPosition); } async copyBoard(userId: EntityId, boardId: EntityId): Promise { this.logger.debug({ action: 'copyBoard', userId, boardId }); const user = await this.authorizationService.getUserWithPermissions(userId); - const board = await this.columnBoardService.findById(boardId); + const board = await this.boardNodeService.findByClassAndId(ColumnBoard, boardId); + + // TODO - should not use course repo const course = await this.courseRepo.findById(board.context.id); - await this.checkPermission(userId, board, Action.read); + await this.boardPermissionService.checkPermission(userId, board, Action.read); this.authorizationService.checkPermission(user, course, { action: Action.write, - requiredPermissions: [], + requiredPermissions: [], // TODO - what permissions are required? COURSE_EDIT? }); - const copyStatus = await this.columnBoardCopyService.copyColumnBoard({ + const copyStatus = await this.columnBoardService.copyColumnBoard({ userId, originalColumnBoardId: boardId, destinationExternalReference: board.context, @@ -135,11 +137,10 @@ export class BoardUc extends BaseUc { return copyStatus; } - async updateVisibility(userId: EntityId, boardId: EntityId, isVisible: boolean): Promise { - const board = await this.columnBoardService.findById(boardId); - await this.checkPermission(userId, board, Action.write); + async updateVisibility(userId: EntityId, boardId: EntityId, isVisible: boolean): Promise { + const board = await this.boardNodeService.findByClassAndId(ColumnBoard, boardId); + await this.boardPermissionService.checkPermission(userId, board, Action.write); - await this.columnBoardService.updateBoardVisibility(board, isVisible); - return board; + await this.boardNodeService.updateVisibility(board, isVisible); } } diff --git a/apps/server/src/modules/board/uc/card.uc.spec.ts b/apps/server/src/modules/board/uc/card.uc.spec.ts index da42e9ec4aa..5d232081d92 100644 --- a/apps/server/src/modules/board/uc/card.uc.spec.ts +++ b/apps/server/src/modules/board/uc/card.uc.spec.ts @@ -1,22 +1,22 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { AuthorizationService } from '@modules/authorization'; -import { HttpService } from '@nestjs/axios'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { Action, AuthorizationService } from '@modules/authorization'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardDoAuthorizable, BoardRoles, ContentElementType } from '@shared/domain/domainobject'; -import { columnBoardFactory, columnFactory, setupEntities, userFactory } from '@shared/testing'; -import { cardFactory, richTextElementFactory } from '@shared/testing/factory/domainobject'; +import { setupEntities, userFactory } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; -import { ObjectId } from '@mikro-orm/mongodb'; -import { BoardDoAuthorizableService, CardService, ContentElementService } from '../service'; +import { BoardNodeAuthorizable, BoardNodeFactory, Card, ContentElementType } from '../domain'; +import { BoardNodeAuthorizableService, BoardNodePermissionService, BoardNodeService } from '../service'; +import { cardFactory, columnBoardFactory, richTextElementFactory } from '../testing'; import { CardUc } from './card.uc'; describe(CardUc.name, () => { let module: TestingModule; let uc: CardUc; let authorizationService: DeepMocked; - let boardDoAuthorizableService: DeepMocked; - let cardService: DeepMocked; - let elementService: DeepMocked; + let boardNodeAuthorizableService: DeepMocked; + let boardNodePermissionService: DeepMocked; + let boardNodeService: DeepMocked; + let boardNodeFactory: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -27,34 +27,34 @@ describe(CardUc.name, () => { useValue: createMock(), }, { - provide: BoardDoAuthorizableService, - useValue: createMock(), + provide: BoardNodeAuthorizableService, + useValue: createMock(), }, { - provide: CardService, - useValue: createMock(), + provide: BoardNodePermissionService, + useValue: createMock(), }, { - provide: ContentElementService, - useValue: createMock(), + provide: BoardNodeService, + useValue: createMock(), }, { - provide: LegacyLogger, - useValue: createMock(), + provide: BoardNodeFactory, + useValue: createMock(), }, { - provide: HttpService, - useValue: createMock(), + provide: LegacyLogger, + useValue: createMock(), }, ], }).compile(); uc = module.get(CardUc); authorizationService = module.get(AuthorizationService); - boardDoAuthorizableService = module.get(BoardDoAuthorizableService); - - cardService = module.get(CardService); - elementService = module.get(ContentElementService); + boardNodeAuthorizableService = module.get(BoardNodeAuthorizableService); + boardNodePermissionService = module.get(BoardNodePermissionService); + boardNodeService = module.get(BoardNodeService); + boardNodeFactory = module.get(BoardNodeFactory); await setupEntities(); }); @@ -73,29 +73,57 @@ describe(CardUc.name, () => { const cards = cardFactory.buildList(3); const cardIds = cards.map((c) => c.id); - boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValue( - new BoardDoAuthorizable({ + boardNodeAuthorizableService.getBoardAuthorizable.mockResolvedValue( + new BoardNodeAuthorizable({ users: [], id: new ObjectId().toHexString(), - boardDo: cards[0], - rootDo: columnBoardFactory.build(), + boardNode: cards[0], + rootNode: columnBoardFactory.build(), }) ); + authorizationService.hasPermission.mockReturnValue(true); return { user, cards, cardIds }; }; - it('should call the service', async () => { + it('should call boardNodeService service to find cards', async () => { const { user, cardIds } = setup(); await uc.findCards(user.id, cardIds); - expect(cardService.findByIds).toHaveBeenCalledWith(cardIds); + expect(boardNodeService.findByClassAndIds).toHaveBeenCalledWith(Card, cardIds); + }); + + it('should call the service to get the user with permissions', async () => { + const { user, cards, cardIds } = setup(); + boardNodeService.findByClassAndIds.mockResolvedValueOnce(cards); + + await uc.findCards(user.id, cardIds); + + expect(authorizationService.getUserWithPermissions).toHaveBeenCalledWith(user.id); + }); + + it('should call the service to filter by authorization', async () => { + const { user, cards, cardIds } = setup(); + boardNodeService.findByClassAndIds.mockResolvedValueOnce(cards); + + await uc.findCards(user.id, cardIds); + + expect(boardNodeAuthorizableService.getBoardAuthorizable).toHaveBeenCalledTimes(3); + }); + + it('should call the service to check the user permission', async () => { + const { user, cards, cardIds } = setup(); + boardNodeService.findByClassAndIds.mockResolvedValueOnce(cards); + + await uc.findCards(user.id, cardIds); + + expect(authorizationService.hasPermission).toHaveBeenCalledTimes(3); }); it('should return the card objects', async () => { const { user, cards, cardIds } = setup(); - cardService.findByIds.mockResolvedValueOnce(cards); + boardNodeService.findByClassAndIds.mockResolvedValueOnce(cards); const result = await uc.findCards(user.id, cardIds); @@ -107,25 +135,9 @@ describe(CardUc.name, () => { describe('deleteCard', () => { const setup = () => { const user = userFactory.buildWithId(); - const board = columnBoardFactory.build(); - const boardId = board.id; - const column = columnFactory.build(); const card = cardFactory.build(); - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - const authorizableMock: BoardDoAuthorizable = new BoardDoAuthorizable({ - users: [{ userId: user.id, roles: [BoardRoles.EDITOR] }], - id: board.id, - boardDo: card, - rootDo: board, - }); - const createCardBodyParams = { - requiredEmptyElements: [ContentElementType.FILE, ContentElementType.RICH_TEXT], - }; - - boardDoAuthorizableService.findById.mockResolvedValueOnce(authorizableMock); - - return { user, board, boardId, column, card, createCardBodyParams }; + return { user, card }; }; describe('when deleting a card', () => { @@ -134,16 +146,25 @@ describe(CardUc.name, () => { await uc.deleteCard(user.id, card.id); - expect(cardService.findById).toHaveBeenCalledWith(card.id); + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(Card, card.id); + }); + + it('should call the Board Permission Service to check the user permission', async () => { + const { user, card } = setup(); + boardNodeService.findByClassAndId.mockResolvedValueOnce(card); + + await uc.deleteCard(user.id, card.id); + + expect(boardNodePermissionService.checkPermission).toHaveBeenCalledWith(user.id, card, Action.write); }); it('should call the service to delete the card', async () => { const { user, card } = setup(); - cardService.findById.mockResolvedValueOnce(card); + boardNodeService.findByClassAndId.mockResolvedValueOnce(card); await uc.deleteCard(user.id, card.id); - expect(cardService.delete).toHaveBeenCalledWith(card); + expect(boardNodeService.delete).toHaveBeenCalledWith(card); }); }); }); @@ -151,25 +172,9 @@ describe(CardUc.name, () => { describe('updateCardHeight', () => { const setup = () => { const user = userFactory.buildWithId(); - const board = columnBoardFactory.build(); - const boardId = board.id; - const column = columnFactory.build(); const card = cardFactory.build(); - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - const authorizableMock: BoardDoAuthorizable = new BoardDoAuthorizable({ - users: [{ userId: user.id, roles: [BoardRoles.EDITOR] }], - id: board.id, - boardDo: card, - rootDo: board, - }); - const createCardBodyParams = { - requiredEmptyElements: [ContentElementType.FILE, ContentElementType.RICH_TEXT], - }; - - boardDoAuthorizableService.findById.mockResolvedValueOnce(authorizableMock); - - return { user, board, boardId, column, card, createCardBodyParams }; + return { user, card }; }; describe('when updating a card height', () => { @@ -179,26 +184,27 @@ describe(CardUc.name, () => { await uc.updateCardHeight(user.id, card.id, cardHeight); - expect(cardService.findById).toHaveBeenCalledWith(card.id); + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(Card, card.id); }); - it('should check the permission', async () => { + it('should call the Board Permission Service to check the user permission', async () => { const { user, card } = setup(); const cardHeight = 200; + boardNodeService.findByClassAndId.mockResolvedValueOnce(card); await uc.updateCardHeight(user.id, card.id, cardHeight); - expect(authorizationService.checkPermission).toHaveBeenCalled(); + expect(boardNodePermissionService.checkPermission).toHaveBeenCalledWith(user.id, card, Action.write); }); it('should call the service to update the card height', async () => { const { user, card } = setup(); - cardService.findById.mockResolvedValueOnce(card); const newHeight = 250; + boardNodeService.findByClassAndId.mockResolvedValueOnce(card); await uc.updateCardHeight(user.id, card.id, newHeight); - expect(cardService.updateHeight).toHaveBeenCalledWith(card, newHeight); + expect(boardNodeService.updateHeight).toHaveBeenCalledWith(card, newHeight); }); }); }); @@ -206,25 +212,9 @@ describe(CardUc.name, () => { describe('updateCardTitle', () => { const setup = () => { const user = userFactory.buildWithId(); - const board = columnBoardFactory.build(); - const boardId = board.id; - const column = columnFactory.build(); const card = cardFactory.build(); - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - - const authorizableMock: BoardDoAuthorizable = new BoardDoAuthorizable({ - users: [{ userId: user.id, roles: [BoardRoles.EDITOR] }], - id: card.id, - boardDo: card, - rootDo: board, - }); - const createCardBodyParams = { - requiredEmptyElements: [ContentElementType.FILE, ContentElementType.RICH_TEXT], - }; - - boardDoAuthorizableService.findById.mockResolvedValueOnce(authorizableMock); - return { user, board, boardId, column, card, createCardBodyParams }; + return { user, card }; }; describe('when updating a card title', () => { @@ -233,17 +223,26 @@ describe(CardUc.name, () => { await uc.updateCardTitle(user.id, card.id, 'new title'); - expect(cardService.findById).toHaveBeenCalledWith(card.id); + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(Card, card.id); + }); + + it('should call the service to check the user permission', async () => { + const { user, card } = setup(); + boardNodeService.findByClassAndId.mockResolvedValueOnce(card); + + await uc.updateCardTitle(user.id, card.id, 'new title'); + + expect(boardNodePermissionService.checkPermission).toHaveBeenCalledWith(user.id, card, Action.write); }); it('should call the service to update the card title', async () => { const { user, card } = setup(); - cardService.findById.mockResolvedValueOnce(card); const newTitle = 'new title'; + boardNodeService.findByClassAndId.mockResolvedValueOnce(card); await uc.updateCardTitle(user.id, card.id, newTitle); - expect(cardService.updateTitle).toHaveBeenCalledWith(card, newTitle); + expect(boardNodeService.updateTitle).toHaveBeenCalledWith(card, newTitle); }); }); }); @@ -255,18 +254,6 @@ describe(CardUc.name, () => { const card = cardFactory.build(); const element = richTextElementFactory.build(); - cardService.findById.mockResolvedValueOnce(card); - elementService.create.mockResolvedValueOnce(element); - - boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValue( - new BoardDoAuthorizable({ - users: [], - id: new ObjectId().toHexString(), - boardDo: card, - rootDo: columnBoardFactory.build(), - }) - ); - return { user, card, element }; }; @@ -275,33 +262,39 @@ describe(CardUc.name, () => { await uc.createElement(user.id, card.id, ContentElementType.RICH_TEXT); - expect(cardService.findById).toHaveBeenCalledWith(card.id); + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(Card, card.id); }); - it('should call the service to create the content element', async () => { + it('should call the service to check the user permission', async () => { const { user, card } = setup(); + boardNodeService.findByClassAndId.mockResolvedValueOnce(card); await uc.createElement(user.id, card.id, ContentElementType.RICH_TEXT); - expect(elementService.create).toHaveBeenCalledWith(card, ContentElementType.RICH_TEXT); + expect(boardNodePermissionService.checkPermission).toHaveBeenCalledWith(user.id, card, Action.write); }); - it('should call the service to move the element', async () => { - const { user, card, element } = setup(); - await uc.createElement(user.id, card.id, ContentElementType.RICH_TEXT, 3); + it('should call the factory to build element', async () => { + const { user, card } = setup(); + + await uc.createElement(user.id, card.id, ContentElementType.RICH_TEXT); - expect(elementService.move).toHaveBeenCalledWith(element, card, 3); + expect(boardNodeFactory.buildContentElement).toHaveBeenCalledWith(ContentElementType.RICH_TEXT); }); - it('should not call the service to move the element if position is not a number', async () => { - const { user, card } = setup(); - await uc.createElement(user.id, card.id, ContentElementType.RICH_TEXT, 'not a number' as unknown as number); + it('should call the service to create add the content element to the card', async () => { + const { user, card, element } = setup(); + boardNodeService.findByClassAndId.mockResolvedValueOnce(card); + boardNodeFactory.buildContentElement.mockReturnValueOnce(element); + + await uc.createElement(user.id, card.id, ContentElementType.RICH_TEXT, 3); - expect(elementService.move).not.toHaveBeenCalled(); + expect(boardNodeService.addToParent).toHaveBeenCalledWith(card, element, 3); }); it('should return new content element', async () => { const { user, card, element } = setup(); + boardNodeFactory.buildContentElement.mockReturnValueOnce(element); const result = await uc.createElement(user.id, card.id, ContentElementType.RICH_TEXT); @@ -317,17 +310,6 @@ describe(CardUc.name, () => { const element = richTextElementFactory.build(); const card = cardFactory.build(); - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - - const authorizableMock: BoardDoAuthorizable = new BoardDoAuthorizable({ - users: [{ userId: user.id, roles: [BoardRoles.EDITOR] }], - id: element.id, - boardDo: element, - rootDo: columnBoardFactory.build(), - }); - - boardDoAuthorizableService.findById.mockResolvedValueOnce(authorizableMock); - return { user, card, element }; }; @@ -336,7 +318,7 @@ describe(CardUc.name, () => { await uc.moveElement(user.id, element.id, card.id, 3); - expect(elementService.findById).toHaveBeenCalledWith(element.id); + expect(boardNodeService.findContentElementById).toHaveBeenCalledWith(element.id); }); it('should call the service to find the target card', async () => { @@ -344,17 +326,35 @@ describe(CardUc.name, () => { await uc.moveElement(user.id, element.id, card.id, 3); - expect(cardService.findById).toHaveBeenCalledWith(card.id); + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(Card, card.id); + }); + + it('should call the service to check the user permission for the element', async () => { + const { user, element, card } = setup(); + boardNodeService.findContentElementById.mockResolvedValueOnce(element); + + await uc.moveElement(user.id, element.id, card.id, 3); + + expect(boardNodePermissionService.checkPermission).toHaveBeenCalledWith(user.id, element, Action.write); + }); + + it('should call the service to check the user permission for the target card', async () => { + const { user, element, card } = setup(); + boardNodeService.findByClassAndId.mockResolvedValueOnce(card); + + await uc.moveElement(user.id, element.id, card.id, 3); + + expect(boardNodePermissionService.checkPermission).toHaveBeenCalledWith(user.id, card, Action.write); }); it('should call the service to move the element', async () => { const { user, element, card } = setup(); - elementService.findById.mockResolvedValueOnce(element); - cardService.findById.mockResolvedValueOnce(card); + boardNodeService.findContentElementById.mockResolvedValueOnce(element); + boardNodeService.findByClassAndId.mockResolvedValueOnce(card); await uc.moveElement(user.id, element.id, card.id, 3); - expect(elementService.move).toHaveBeenCalledWith(element, card, 3); + expect(boardNodeService.move).toHaveBeenCalledWith(element, card, 3); }); }); }); diff --git a/apps/server/src/modules/board/uc/card.uc.ts b/apps/server/src/modules/board/uc/card.uc.ts index c8751b570d8..eac9f9c00a3 100644 --- a/apps/server/src/modules/board/uc/card.uc.ts +++ b/apps/server/src/modules/board/uc/card.uc.ts @@ -1,30 +1,47 @@ -import { Action, AuthorizationService } from '@modules/authorization'; +import { Action, AuthorizationContext, AuthorizationService } from '@modules/authorization'; import { forwardRef, Inject, Injectable } from '@nestjs/common'; -import { AnyBoardDo, AnyContentElementDo, Card, ContentElementType } from '@shared/domain/domainobject'; import { EntityId } from '@shared/domain/types'; import { LegacyLogger } from '@src/core/logger'; -import { BoardDoAuthorizableService, CardService, ContentElementService } from '../service'; -import { BaseUc } from './base.uc'; + +import { AnyContentElement, BoardNodeFactory, Card, ContentElementType } from '../domain'; +import { BoardNodeAuthorizableService, BoardNodePermissionService, BoardNodeService } from '../service'; @Injectable() -export class CardUc extends BaseUc { +export class CardUc { constructor( @Inject(forwardRef(() => AuthorizationService)) - protected readonly authorizationService: AuthorizationService, - protected readonly boardDoAuthorizableService: BoardDoAuthorizableService, - private readonly cardService: CardService, - private readonly elementService: ContentElementService, + private readonly authorizationService: AuthorizationService, + private readonly boardNodeAuthorizableService: BoardNodeAuthorizableService, + private readonly boardNodePermissionService: BoardNodePermissionService, + private readonly boardNodeService: BoardNodeService, + private readonly boardNodeFactory: BoardNodeFactory, private readonly logger: LegacyLogger ) { - super(authorizationService, boardDoAuthorizableService); this.logger.setContext(CardUc.name); } + // TODO reactor: No reason to check permission for all cards; this is only cards from same board async findCards(userId: EntityId, cardIds: EntityId[]): Promise { this.logger.debug({ action: 'findCards', userId, cardIds }); - const cards = await this.cardService.findByIds(cardIds); - const allowedCards = await this.filterAllowed(userId, cards, Action.read); + const cards = await this.boardNodeService.findByClassAndIds(Card, cardIds); + + const user = await this.authorizationService.getUserWithPermissions(userId); + + const context: AuthorizationContext = { action: Action.read, requiredPermissions: [] }; + const promises = cards.map((card) => + this.boardNodeAuthorizableService.getBoardAuthorizable(card).then((boardNodeAuthorizable) => { + return { boardNodeAuthorizable, boardNode: card }; + }) + ); + const result = await Promise.all(promises); + + const allowedCards = result.reduce((allowedNodes: Card[], { boardNodeAuthorizable, boardNode }) => { + if (this.authorizationService.hasPermission(user, boardNodeAuthorizable, context)) { + allowedNodes.push(boardNode); + } + return allowedNodes; + }, []); return allowedCards; } @@ -32,28 +49,28 @@ export class CardUc extends BaseUc { async updateCardHeight(userId: EntityId, cardId: EntityId, height: number): Promise { this.logger.debug({ action: 'updateCardHeight', userId, cardId, height }); - const card = await this.cardService.findById(cardId); - await this.checkPermission(userId, card, Action.write); + const card = await this.boardNodeService.findByClassAndId(Card, cardId); + await this.boardNodePermissionService.checkPermission(userId, card, Action.write); - await this.cardService.updateHeight(card, height); + await this.boardNodeService.updateHeight(card, height); } async updateCardTitle(userId: EntityId, cardId: EntityId, title: string): Promise { this.logger.debug({ action: 'updateCardTitle', userId, cardId, title }); - const card = await this.cardService.findById(cardId); - await this.checkPermission(userId, card, Action.write); + const card = await this.boardNodeService.findByClassAndId(Card, cardId); + await this.boardNodePermissionService.checkPermission(userId, card, Action.write); - await this.cardService.updateTitle(card, title); + await this.boardNodeService.updateTitle(card, title); } async deleteCard(userId: EntityId, cardId: EntityId): Promise { this.logger.debug({ action: 'deleteCard', userId, cardId }); - const card = await this.cardService.findById(cardId); - await this.checkPermission(userId, card, Action.write); + const card = await this.boardNodeService.findByClassAndId(Card, cardId); + await this.boardNodePermissionService.checkPermission(userId, card, Action.write); - await this.cardService.delete(card); + await this.boardNodeService.delete(card); } // --- elements --- @@ -63,16 +80,15 @@ export class CardUc extends BaseUc { cardId: EntityId, type: ContentElementType, toPosition?: number - ): Promise { + ): Promise { this.logger.debug({ action: 'createElement', userId, cardId, type }); - const card = await this.cardService.findById(cardId); - await this.checkPermission(userId, card, Action.write); + const card = await this.boardNodeService.findByClassAndId(Card, cardId); + await this.boardNodePermissionService.checkPermission(userId, card, Action.write); + + const element = this.boardNodeFactory.buildContentElement(type); - const element = await this.elementService.create(card, type); - if (toPosition !== undefined && typeof toPosition === 'number') { - await this.elementService.move(element, card, toPosition); - } + await this.boardNodeService.addToParent(card, element, toPosition); return element; } @@ -85,33 +101,12 @@ export class CardUc extends BaseUc { ): Promise { this.logger.debug({ action: 'moveElement', userId, elementId, targetCardId, targetPosition }); - const element = await this.elementService.findById(elementId); - const targetCard = await this.cardService.findById(targetCardId); + const element = await this.boardNodeService.findContentElementById(elementId); + const targetCard = await this.boardNodeService.findByClassAndId(Card, targetCardId); - await this.checkPermission(userId, element, Action.write); - await this.checkPermission(userId, targetCard, Action.write); - - await this.elementService.move(element, targetCard, targetPosition); - } - - private async filterAllowed(userId: EntityId, boardDos: T[], action: Action): Promise { - const user = await this.authorizationService.getUserWithPermissions(userId); - - const context = { action, requiredPermissions: [] }; - const promises = boardDos.map((boardDo) => - this.boardDoAuthorizableService.getBoardAuthorizable(boardDo).then((boardDoAuthorizable) => { - return { boardDoAuthorizable, boardDo }; - }) - ); - const result = await Promise.all(promises); - - const allowed = result.reduce((allowedDos: T[], { boardDoAuthorizable, boardDo }) => { - if (this.authorizationService.hasPermission(user, boardDoAuthorizable, context)) { - allowedDos.push(boardDo); - } - return allowedDos; - }, []); + await this.boardNodePermissionService.checkPermission(userId, element, Action.write); + await this.boardNodePermissionService.checkPermission(userId, targetCard, Action.write); - return allowed; + await this.boardNodeService.move(element, targetCard, targetPosition); } } diff --git a/apps/server/src/modules/board/uc/column.uc.spec.ts b/apps/server/src/modules/board/uc/column.uc.spec.ts index 67111077911..1c39a2fa57e 100644 --- a/apps/server/src/modules/board/uc/column.uc.spec.ts +++ b/apps/server/src/modules/board/uc/column.uc.spec.ts @@ -1,57 +1,49 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { AuthorizationService } from '@modules/authorization'; +import { Action } from '@modules/authorization'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardDoAuthorizable, BoardRoles, ContentElementType } from '@shared/domain/domainobject'; import { setupEntities, userFactory } from '@shared/testing'; -import { cardFactory, columnBoardFactory, columnFactory } from '@shared/testing/factory/domainobject'; import { LegacyLogger } from '@src/core/logger'; -import { BoardDoAuthorizableService, CardService, ColumnService, ContentElementService } from '../service'; +import { BoardNodeFactory, Card, Column, ContentElementType } from '../domain'; +import { BoardNodeService } from '../service'; +import { BoardNodePermissionService } from '../service/board-node-permission.service'; +import { cardFactory, columnBoardFactory, columnFactory } from '../testing'; import { ColumnUc } from './column.uc'; describe(ColumnUc.name, () => { let module: TestingModule; let uc: ColumnUc; - let authorizationService: DeepMocked; - let boardDoAuthorizableService: DeepMocked; - let columnService: DeepMocked; - let cardService: DeepMocked; + + let boardNodePermissionService: DeepMocked; + let boardNodeService: DeepMocked; + let boardNodeFactory: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ providers: [ ColumnUc, { - provide: AuthorizationService, - useValue: createMock(), - }, - { - provide: BoardDoAuthorizableService, - useValue: createMock(), + provide: BoardNodePermissionService, + useValue: createMock(), }, { - provide: CardService, - useValue: createMock(), + provide: BoardNodeService, + useValue: createMock(), }, { - provide: ColumnService, - useValue: createMock(), + provide: BoardNodeFactory, + useValue: createMock(), }, { provide: LegacyLogger, useValue: createMock(), }, - { - provide: ContentElementService, - useValue: createMock(), - }, ], }).compile(); uc = module.get(ColumnUc); - authorizationService = module.get(AuthorizationService); - boardDoAuthorizableService = module.get(BoardDoAuthorizableService); - columnService = module.get(ColumnService); - cardService = module.get(CardService); + boardNodePermissionService = module.get(BoardNodePermissionService); + boardNodeService = module.get(BoardNodeService); + boardNodeFactory = module.get(BoardNodeFactory); await setupEntities(); }); @@ -64,26 +56,16 @@ describe(ColumnUc.name, () => { }); const setup = () => { - jest.clearAllMocks(); const user = userFactory.buildWithId(); const board = columnBoardFactory.build(); const boardId = board.id; const column = columnFactory.build(); const card = cardFactory.build(); - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - const authorizableMock: BoardDoAuthorizable = new BoardDoAuthorizable({ - users: [{ userId: user.id, roles: [BoardRoles.EDITOR] }], - id: board.id, - boardDo: board, - rootDo: columnBoardFactory.build(), - }); const createCardBodyParams = { requiredEmptyElements: [ContentElementType.FILE, ContentElementType.RICH_TEXT], }; - boardDoAuthorizableService.findById.mockResolvedValueOnce(authorizableMock); - return { user, board, boardId, column, card, createCardBodyParams }; }; @@ -94,16 +76,25 @@ describe(ColumnUc.name, () => { await uc.deleteColumn(user.id, column.id); - expect(columnService.findById).toHaveBeenCalledWith(column.id); + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(Column, column.id); + }); + + it('should call the Board Permission Service to check the user permission', async () => { + const { user, column } = setup(); + boardNodeService.findByClassAndId.mockResolvedValueOnce(column); + + await uc.deleteColumn(user.id, column.id); + + expect(boardNodePermissionService.checkPermission).toHaveBeenCalledWith(user.id, column, Action.write); }); it('should call the service to delete the column', async () => { const { user, column } = setup(); - columnService.findById.mockResolvedValueOnce(column); + boardNodeService.findByClassAndId.mockResolvedValueOnce(column); await uc.deleteColumn(user.id, column.id); - expect(columnService.delete).toHaveBeenCalledWith(column); + expect(boardNodeService.delete).toHaveBeenCalledWith(column); }); }); }); @@ -115,35 +106,72 @@ describe(ColumnUc.name, () => { await uc.updateColumnTitle(user.id, column.id, 'new title'); - expect(columnService.findById).toHaveBeenCalledWith(column.id); + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(Column, column.id); + }); + + it('should call the Board Permission Service to check the user permission', async () => { + const { user, column } = setup(); + boardNodeService.findByClassAndId.mockResolvedValueOnce(column); + + await uc.updateColumnTitle(user.id, column.id, 'new title'); + + expect(boardNodePermissionService.checkPermission).toHaveBeenCalledWith(user.id, column, Action.write); }); it('should call the service to update the column title', async () => { const { user, column } = setup(); - columnService.findById.mockResolvedValueOnce(column); + boardNodeService.findByClassAndId.mockResolvedValueOnce(column); const newTitle = 'new title'; await uc.updateColumnTitle(user.id, column.id, newTitle); - expect(columnService.updateTitle).toHaveBeenCalledWith(column, newTitle); + expect(boardNodeService.updateTitle).toHaveBeenCalledWith(column, newTitle); }); }); }); describe('createCard', () => { describe('when creating a card', () => { + it('should call the service to find the column', async () => { + const { user, column } = setup(); + + await uc.createCard(user.id, column.id); + + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(Column, column.id); + }); + + it('should call the service to check the permissions', async () => { + const { user, column } = setup(); + boardNodeService.findByClassAndId.mockResolvedValueOnce(column); + + await uc.createCard(user.id, column.id); + + expect(boardNodePermissionService.checkPermission).toHaveBeenCalledWith(user.id, column, Action.write); + }); + + it('should call the factory to build card', async () => { + const { user, column, card } = setup(); + boardNodeFactory.buildCard.mockReturnValueOnce(card); + + await uc.createCard(user.id, column.id); + + expect(boardNodeFactory.buildCard).toHaveBeenCalled(); + }); + it('should call the service to create the card', async () => { - const { user, column, createCardBodyParams } = setup(); + const { user, column, card, createCardBodyParams } = setup(); const { requiredEmptyElements } = createCardBodyParams; + boardNodeFactory.buildCard.mockReturnValueOnce(card); + boardNodeService.findByClassAndId.mockResolvedValueOnce(column); await uc.createCard(user.id, column.id, requiredEmptyElements); - expect(cardService.create).toHaveBeenCalledWith(column, requiredEmptyElements); + expect(boardNodeService.addToParent).toHaveBeenCalledWith(column, card); }); it('should return the card object', async () => { const { user, column, card } = setup(); - cardService.create.mockResolvedValueOnce(card); + boardNodeFactory.buildCard.mockReturnValueOnce(card); const result = await uc.createCard(user.id, column.id); @@ -159,7 +187,7 @@ describe(ColumnUc.name, () => { await uc.moveCard(user.id, card.id, column.id, 5); - expect(cardService.findById).toHaveBeenCalledWith(card.id); + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(Card, card.id); }); it('should call the service to find the target column', async () => { @@ -167,17 +195,35 @@ describe(ColumnUc.name, () => { await uc.moveCard(user.id, card.id, column.id, 5); - expect(columnService.findById).toHaveBeenCalledWith(column.id); + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(Column, column.id); + }); + + it('should call the service to check the permissions for card', async () => { + const { user, column, card } = setup(); + boardNodeService.findByClassAndId.mockResolvedValueOnce(card); + + await uc.moveCard(user.id, card.id, column.id, 5); + + expect(boardNodePermissionService.checkPermission).toHaveBeenCalledWith(user.id, card, Action.write); + }); + + it('should call the service to check the user permission for the target column', async () => { + const { user, column, card } = setup(); + boardNodeService.findByClassAndId.mockResolvedValueOnce(column); + + await uc.moveCard(user.id, card.id, column.id, 5); + + expect(boardNodePermissionService.checkPermission).toHaveBeenCalledWith(user.id, column, Action.write); }); it('should call the service to move the card', async () => { const { user, column, card } = setup(); - cardService.findById.mockResolvedValueOnce(card); - columnService.findById.mockResolvedValueOnce(column); + boardNodeService.findByClassAndId.mockResolvedValueOnce(card); + boardNodeService.findByClassAndId.mockResolvedValueOnce(column); await uc.moveCard(user.id, card.id, column.id, 5); - expect(cardService.move).toHaveBeenCalledWith(card, column, 5); + expect(boardNodeService.move).toHaveBeenCalledWith(card, column, 5); }); }); }); diff --git a/apps/server/src/modules/board/uc/column.uc.ts b/apps/server/src/modules/board/uc/column.uc.ts index f3b6a84ef7f..8e8a0c1a06e 100644 --- a/apps/server/src/modules/board/uc/column.uc.ts +++ b/apps/server/src/modules/board/uc/column.uc.ts @@ -1,68 +1,67 @@ -import { Action, AuthorizationService } from '@modules/authorization'; -import { forwardRef, Inject, Injectable } from '@nestjs/common'; -import { Card, Column, ContentElementType } from '@shared/domain/domainobject'; +import { Action } from '@modules/authorization'; +import { Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain/types'; import { LegacyLogger } from '@src/core/logger'; -import { BoardDoAuthorizableService, CardService, ColumnService } from '../service'; -import { BaseUc } from './base.uc'; +import { BoardNodeFactory, Card, Column, ContentElementType } from '../domain'; +import { BoardNodePermissionService, BoardNodeService } from '../service'; @Injectable() -export class ColumnUc extends BaseUc { +export class ColumnUc { constructor( - @Inject(forwardRef(() => AuthorizationService)) - protected readonly authorizationService: AuthorizationService, - protected readonly boardDoAuthorizableService: BoardDoAuthorizableService, - private readonly cardService: CardService, - private readonly columnService: ColumnService, + private readonly boardNodePermissionService: BoardNodePermissionService, + private readonly boardNodeService: BoardNodeService, + private readonly boardNodeFactory: BoardNodeFactory, + private readonly logger: LegacyLogger ) { - super(authorizationService, boardDoAuthorizableService); this.logger.setContext(ColumnUc.name); } - async deleteColumn(userId: EntityId, columnId: EntityId): Promise { + async deleteColumn(userId: EntityId, columnId: EntityId): Promise { this.logger.debug({ action: 'deleteColumn', userId, columnId }); - const column = await this.columnService.findById(columnId); - await this.checkPermission(userId, column, Action.write); - - await this.columnService.delete(column); + const column = await this.boardNodeService.findByClassAndId(Column, columnId); + await this.boardNodePermissionService.checkPermission(userId, column, Action.write); - return column; + await this.boardNodeService.delete(column); } - async updateColumnTitle(userId: EntityId, columnId: EntityId, title: string): Promise { + async updateColumnTitle(userId: EntityId, columnId: EntityId, title: string): Promise { this.logger.debug({ action: 'updateColumnTitle', userId, columnId, title }); - const column = await this.columnService.findById(columnId); - await this.checkPermission(userId, column, Action.write); - - await this.columnService.updateTitle(column, title); + const column = await this.boardNodeService.findByClassAndId(Column, columnId); + await this.boardNodePermissionService.checkPermission(userId, column, Action.write); - return column; + await this.boardNodeService.updateTitle(column, title); } - async createCard(userId: EntityId, columnId: EntityId, requiredEmptyElements?: ContentElementType[]): Promise { + async createCard( + userId: EntityId, + columnId: EntityId, + requiredEmptyElements: ContentElementType[] = [] + ): Promise { this.logger.debug({ action: 'createCard', userId, columnId }); - const column = await this.columnService.findById(columnId); - await this.checkPermission(userId, column, Action.write); + const column = await this.boardNodeService.findByClassAndId(Column, columnId); + await this.boardNodePermissionService.checkPermission(userId, column, Action.write); - const card = await this.cardService.create(column, requiredEmptyElements); + const elements = requiredEmptyElements.map((type) => this.boardNodeFactory.buildContentElement(type)); + const card = this.boardNodeFactory.buildCard(elements); + + await this.boardNodeService.addToParent(column, card); return card; } - async moveCard(userId: EntityId, cardId: EntityId, targetColumnId: EntityId, targetPosition: number): Promise { + async moveCard(userId: EntityId, cardId: EntityId, targetColumnId: EntityId, targetPosition: number): Promise { this.logger.debug({ action: 'moveCard', userId, cardId, targetColumnId, toPosition: targetPosition }); - const card = await this.cardService.findById(cardId); - const targetColumn = await this.columnService.findById(targetColumnId); + const card = await this.boardNodeService.findByClassAndId(Card, cardId); + const targetColumn = await this.boardNodeService.findByClassAndId(Column, targetColumnId); - await this.checkPermission(userId, card, Action.write); - await this.checkPermission(userId, targetColumn, Action.write); + await this.boardNodePermissionService.checkPermission(userId, card, Action.write); + await this.boardNodePermissionService.checkPermission(userId, targetColumn, Action.write); - await this.cardService.move(card, targetColumn, targetPosition); - return card; + await this.boardNodeService.move(card, targetColumn, targetPosition); } } diff --git a/apps/server/src/modules/board/uc/element.uc.spec.ts b/apps/server/src/modules/board/uc/element.uc.spec.ts index c3607fb0312..2315b3788d0 100644 --- a/apps/server/src/modules/board/uc/element.uc.spec.ts +++ b/apps/server/src/modules/board/uc/element.uc.spec.ts @@ -1,52 +1,50 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { AuthorizationService } from '@modules/authorization'; import { HttpService } from '@nestjs/axios'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardDoAuthorizable, BoardRoles } from '@shared/domain/domainobject'; import { InputFormat } from '@shared/domain/types'; +import { setupEntities, userFactory } from '@shared/testing'; +import { Logger } from '@src/core/logger'; +import { Action } from '@modules/authorization'; +import { ElementUc } from './element.uc'; +import { BoardNodePermissionService, BoardNodeAuthorizableService, BoardNodeService } from '../service'; +import { BoardNodeFactory } from '../domain'; + import { - cardFactory, - columnBoardFactory, - drawingElementFactory, - fileElementFactory, + boardNodeAuthorizableFactory, richTextElementFactory, - setupEntities, + drawingElementFactory, submissionContainerElementFactory, submissionItemFactory, - userFactory, -} from '@shared/testing'; -import { Logger } from '@src/core/logger'; -import { ObjectId } from '@mikro-orm/mongodb'; -import { boardDoAuthorizableFactory } from '@shared/testing/factory/domainobject/board/board-do-authorizable.factory'; -import { BoardDoAuthorizableService, ContentElementService, SubmissionItemService } from '../service'; -import { ElementUc } from './element.uc'; +} from '../testing'; +import { RichTextContentBody } from '../controller/dto'; describe(ElementUc.name, () => { let module: TestingModule; let uc: ElementUc; - let authorizationService: DeepMocked; - let boardDoAuthorizableService: DeepMocked; - let elementService: DeepMocked; + let boardNodeAuthorizableService: DeepMocked; + let boardNodeService: DeepMocked; + let boardNodeFactory: DeepMocked; + let boardPermissionService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ providers: [ ElementUc, { - provide: AuthorizationService, - useValue: createMock(), + provide: BoardNodeAuthorizableService, + useValue: createMock(), }, { - provide: BoardDoAuthorizableService, - useValue: createMock(), + provide: BoardNodePermissionService, + useValue: createMock(), }, { - provide: ContentElementService, - useValue: createMock(), + provide: BoardNodeService, + useValue: createMock(), }, { - provide: SubmissionItemService, - useValue: createMock(), + provide: BoardNodeFactory, + useValue: createMock(), }, { provide: Logger, @@ -60,11 +58,10 @@ describe(ElementUc.name, () => { }).compile(); uc = module.get(ElementUc); - authorizationService = module.get(AuthorizationService); - authorizationService.checkPermission.mockImplementation(() => {}); - boardDoAuthorizableService = module.get(BoardDoAuthorizableService); - boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValue(boardDoAuthorizableFactory.build()); - elementService = module.get(ContentElementService); + boardNodeAuthorizableService = module.get(BoardNodeAuthorizableService); + boardPermissionService = module.get(BoardNodePermissionService); + boardNodeService = module.get(BoardNodeService); + boardNodeFactory = module.get(BoardNodeFactory); await setupEntities(); }); @@ -72,81 +69,68 @@ describe(ElementUc.name, () => { await module.close(); }); - describe('updateElementContent', () => { - describe('update rich text element', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('updateElement', () => { + describe('when element is rich text', () => { const setup = () => { const user = userFactory.build(); - const richTextElement = richTextElementFactory.build(); - const content = { text: 'this has been updated', inputFormat: InputFormat.RICH_TEXT_CK5 }; + const element = richTextElementFactory.build(); + + const content: RichTextContentBody = { text: 'this has been updated', inputFormat: InputFormat.RICH_TEXT_CK5 }; - const elementSpy = elementService.findById.mockResolvedValueOnce(richTextElement); + boardNodeService.findContentElementById.mockResolvedValueOnce(element); - return { richTextElement, user, content, elementSpy }; + return { element, user, content }; }; - it('should get element', async () => { - const { richTextElement, user, content, elementSpy } = setup(); + it('should call service to find the elemebt element', async () => { + const { element, user, content } = setup(); - await uc.updateElement(user.id, richTextElement.id, content); + await uc.updateElement(user.id, element.id, content); - expect(elementSpy).toHaveBeenCalledWith(richTextElement.id); + expect(boardNodeService.findContentElementById).toHaveBeenCalledWith(element.id); }); - it('should call the service', async () => { - const { richTextElement, user, content } = setup(); + it('should call the Board Permission Service to check the user permission', async () => { + const { user, element, content } = setup(); - await uc.updateElement(user.id, richTextElement.id, content); + await uc.updateElement(user.id, element.id, content); - expect(elementService.update).toHaveBeenCalledWith(richTextElement, content); + expect(boardPermissionService.checkPermission).toHaveBeenCalledWith(user.id, element, Action.write); }); - }); - describe('update file element', () => { - const setup = () => { - const user = userFactory.build(); - const fileElement = fileElementFactory.build(); - const content = { caption: 'this has been updated', alternativeText: 'this altText has been updated' }; - - const elementSpy = elementService.findById.mockResolvedValueOnce(fileElement); - - return { fileElement, user, content, elementSpy }; - }; - - it('should get element', async () => { - const { fileElement, user, content, elementSpy } = setup(); + it('should call the boardNodeService service to update content', async () => { + const { element, user, content } = setup(); - await uc.updateElement(user.id, fileElement.id, content); + await uc.updateElement(user.id, element.id, content); - expect(elementSpy).toHaveBeenCalledWith(fileElement.id); + expect(boardNodeService.updateContent).toHaveBeenCalledWith(element, content); }); - it('should call the service', async () => { - const { fileElement, user, content } = setup(); + it('should return the updated element', async () => { + const { element, user, content } = setup(); - await uc.updateElement(user.id, fileElement.id, content); + const updatedElement = element; + updatedElement.text = content.text; + updatedElement.inputFormat = content.inputFormat; - expect(elementService.update).toHaveBeenCalledWith(fileElement, content); + const result = await uc.updateElement(user.id, element.id, content); + + expect(result).toBe(updatedElement); }); }); }); describe('deleteElement', () => { - describe('when deleting a content element', () => { + describe('when deleting an element', () => { const setup = () => { const user = userFactory.build(); const element = richTextElementFactory.build(); - const drawingElement = drawingElementFactory.build(); - - boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValue( - new BoardDoAuthorizable({ - users: [], - id: new ObjectId().toHexString(), - boardDo: element, - rootDo: columnBoardFactory.build(), - }) - ); - return { user, element, drawingElement }; + return { user, element }; }; it('should call the service to find the element', async () => { @@ -154,69 +138,66 @@ describe(ElementUc.name, () => { await uc.deleteElement(user.id, element.id); - expect(elementService.findById).toHaveBeenCalledWith(element.id); + expect(boardNodeService.findContentElementById).toHaveBeenCalledWith(element.id); + }); + + it('should call the Board Permission Service to check the user permission', async () => { + const { user, element } = setup(); + boardNodeService.findContentElementById.mockResolvedValueOnce(element); + + await uc.deleteElement(user.id, element.id); + + expect(boardPermissionService.checkPermission).toHaveBeenCalledWith(user.id, element, Action.write); }); it('should call the service to delete the element', async () => { const { user, element } = setup(); - elementService.findById.mockResolvedValueOnce(element); + boardNodeService.findContentElementById.mockResolvedValueOnce(element); await uc.deleteElement(user.id, element.id); - expect(elementService.delete).toHaveBeenCalledWith(element); + expect(boardNodeService.delete).toHaveBeenCalledWith(element); }); }); }); - describe('createSubmissionItem', () => { - describe('with non SubmissionContainerElement parent', () => { - const setup = () => { - const user = userFactory.build(); - const fileElement = fileElementFactory.build(); + describe('checkElementReadPermission', () => { + const setup = () => { + const user = userFactory.build(); + const drawingElement = drawingElementFactory.build(); - elementService.findById.mockResolvedValueOnce(fileElement); + return { drawingElement, user }; + }; - return { fileElement, user }; - }; + it('should properly find the element', async () => { + const { drawingElement, user } = setup(); + boardNodeService.findContentElementById.mockResolvedValueOnce(drawingElement); - it('should throw', async () => { - const { fileElement, user } = setup(); + await uc.checkElementReadPermission(user.id, drawingElement.id); - await expect(uc.createSubmissionItem(user.id, fileElement.id, true)).rejects.toThrowError( - 'Cannot create submission-item for non submission-container-element' - ); - }); + expect(boardNodeService.findContentElementById).toHaveBeenCalledWith(drawingElement.id); }); - describe('with non SubmissionContainerElement containing non SubmissionItem children', () => { - const setup = () => { - const user = userFactory.build(); - const fileElement = fileElementFactory.build(); - - const submissionContainer = submissionContainerElementFactory.build({ children: [fileElement] }); - - elementService.findById.mockResolvedValueOnce(submissionContainer); - - return { submissionContainer, fileElement, user }; - }; + it('should check the Board Permission Service for user read permission', async () => { + const { drawingElement, user } = setup(); + boardNodeService.findContentElementById.mockResolvedValueOnce(drawingElement); - it('should throw', async () => { - const { submissionContainer, user } = setup(); + await uc.checkElementReadPermission(user.id, drawingElement.id); - await expect(uc.createSubmissionItem(user.id, submissionContainer.id, true)).rejects.toThrowError( - 'Children of submission-container-element must be of type submission-item' - ); - }); + expect(boardPermissionService.checkPermission).toHaveBeenCalledWith(user.id, drawingElement, Action.read); }); + }); + describe('createSubmissionItem', () => { describe('with user already has a submission-item in the submission-container-element set', () => { const setup = () => { - const user = userFactory.build(); + const user = userFactory.buildWithId(); const submissionItem = submissionItemFactory.build({ userId: user.id }); const submissionContainer = submissionContainerElementFactory.build({ children: [submissionItem] }); - elementService.findById.mockResolvedValueOnce(submissionContainer); + boardNodeService.findByClassAndId.mockResolvedValueOnce(submissionContainer); + boardPermissionService.isUserBoardEditor.mockReturnValueOnce(false); return { submissionContainer, submissionItem, user }; }; @@ -229,59 +210,81 @@ describe(ElementUc.name, () => { ); }); }); - }); - describe('checkElementReadPermission', () => { - const setup = () => { - const user = userFactory.build(); - const drawingElement = drawingElementFactory.build(); - const card = cardFactory.build({ children: [drawingElement] }); - const columnBoard = columnBoardFactory.build({ children: [card] }); - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - - const authorizableMock: BoardDoAuthorizable = new BoardDoAuthorizable({ - users: [{ userId: user.id, roles: [BoardRoles.EDITOR] }], - id: columnBoard.id, - boardDo: card, - rootDo: columnBoard, + describe('with user being board editor', () => { + const sestup = () => { + const user = userFactory.build(); + const submissionContainer = submissionContainerElementFactory.build(); + boardNodeService.findContentElementById.mockResolvedValueOnce(submissionContainer); + boardPermissionService.isUserBoardEditor.mockReturnValueOnce(true); + + return { user, submissionContainer }; + }; + + it('should throw', async () => { + const { user, submissionContainer } = sestup(); + + await expect(uc.createSubmissionItem(user.id, submissionContainer.id, true)).rejects.toThrowError(); }); + }); - boardDoAuthorizableService.findById.mockResolvedValueOnce(authorizableMock); + describe('with user being board reader', () => { + const setup = () => { + const user = userFactory.build(); + const submissionItem = submissionItemFactory.build(); + const submissionContainer = submissionContainerElementFactory.build(); - return { drawingElement, user }; - }; + boardNodeService.findByClassAndId.mockResolvedValueOnce(submissionContainer); + boardPermissionService.isUserBoardEditor.mockReturnValueOnce(false); + boardNodeAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce(boardNodeAuthorizableFactory.build()); + boardNodeFactory.buildSubmissionItem.mockReturnValueOnce(submissionItem); - it('should properly find the element', async () => { - const { drawingElement, user } = setup(); - elementService.findById.mockResolvedValueOnce(drawingElement); + return { user, submissionContainer, submissionItem }; + }; - await uc.checkElementReadPermission(user.id, drawingElement.id); + it('should call Board Permission Service to check user *read* permission on submission container', async () => { + const { user, submissionContainer } = setup(); - expect(elementService.findById).toHaveBeenCalledWith(drawingElement.id); - }); + await uc.createSubmissionItem(user.id, submissionContainer.id, true); - it('should properly check element permission and not throw', async () => { - const { drawingElement, user } = setup(); - elementService.findById.mockResolvedValueOnce(drawingElement); + expect(boardPermissionService.checkPermission).toHaveBeenCalledWith(user.id, submissionContainer, Action.read); + }); - await expect(uc.checkElementReadPermission(user.id, drawingElement.id)).resolves.not.toThrow(); - }); + it('should call BoardNodeAuthorizableService to get board authorizable', async () => { + const { user, submissionContainer } = setup(); - it('should throw at find element by Id', async () => { - const { drawingElement, user } = setup(); - elementService.findById.mockRejectedValueOnce(new Error()); + await uc.createSubmissionItem(user.id, submissionContainer.id, true); - await expect(uc.checkElementReadPermission(user.id, drawingElement.id)).rejects.toThrow(); - }); + expect(boardNodeAuthorizableService.getBoardAuthorizable).toHaveBeenCalledWith(submissionContainer); + }); + + it('should call Board Permission Service to check user *editor* permission', async () => { + const { user, submissionContainer } = setup(); + // boardNodeAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce(boardNodeAuthorizableFactory.build()); + + await uc.createSubmissionItem(user.id, submissionContainer.id, true); - it('should throw at check permission', async () => { - const { user } = setup(); - const testElementId = 'wrongTestId123'; - authorizationService.checkPermission.mockImplementationOnce(() => { - throw new Error(); + expect(boardPermissionService.isUserBoardEditor).toHaveBeenCalledWith(user.id, expect.anything()); }); - await expect(uc.checkElementReadPermission(user.id, testElementId)).rejects.toThrow(); + it('should create submission item', async () => { + const { user, submissionContainer, submissionItem } = setup(); + + await uc.createSubmissionItem(user.id, submissionContainer.id, true); + + expect(boardNodeFactory.buildSubmissionItem).toHaveBeenCalledWith({ completed: true, userId: user.id }); + expect(boardNodeService.addToParent).toHaveBeenCalledWith(submissionContainer, submissionItem); + }); + + it('should return the created submission item', async () => { + const { user, submissionContainer, submissionItem } = setup(); + + boardNodeFactory.buildSubmissionItem.mockReturnValueOnce(submissionItem); + + const result = await uc.createSubmissionItem(user.id, submissionContainer.id, true); + + expect(result).toBe(submissionItem); + }); }); }); }); diff --git a/apps/server/src/modules/board/uc/element.uc.ts b/apps/server/src/modules/board/uc/element.uc.ts index 521eb825480..cff947cd92a 100644 --- a/apps/server/src/modules/board/uc/element.uc.ts +++ b/apps/server/src/modules/board/uc/element.uc.ts @@ -1,29 +1,26 @@ -import { Action, AuthorizationService } from '@modules/authorization'; -import { ForbiddenException, forwardRef, Inject, Injectable, UnprocessableEntityException } from '@nestjs/common'; -import { - AnyContentElementDo, - isSubmissionContainerElement, - isSubmissionItem, - SubmissionItem, -} from '@shared/domain/domainobject'; +import { Action } from '@modules/authorization'; +import { ForbiddenException, Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain/types'; import { Logger } from '@src/core/logger'; import { AnyElementContentBody } from '../controller/dto'; -import { BoardDoAuthorizableService, ContentElementService } from '../service'; -import { SubmissionItemService } from '../service/submission-item.service'; -import { BaseUc } from './base.uc'; +import { + AnyContentElement, + BoardNodeFactory, + isSubmissionItem, + SubmissionContainerElement, + SubmissionItem, +} from '../domain'; +import { BoardNodeAuthorizableService, BoardNodePermissionService, BoardNodeService } from '../service'; @Injectable() -export class ElementUc extends BaseUc { +export class ElementUc { constructor( - @Inject(forwardRef(() => AuthorizationService)) - protected readonly authorizationService: AuthorizationService, - protected readonly boardDoAuthorizableService: BoardDoAuthorizableService, - private readonly elementService: ContentElementService, - private readonly submissionItemService: SubmissionItemService, + private readonly boardNodeAuthorizableService: BoardNodeAuthorizableService, + private readonly boardNodeService: BoardNodeService, + private readonly boardNodeFactory: BoardNodeFactory, + private readonly boardPermissionService: BoardNodePermissionService, private readonly logger: Logger ) { - super(authorizationService, boardDoAuthorizableService); this.logger.setContext(ElementUc.name); } @@ -31,24 +28,25 @@ export class ElementUc extends BaseUc { userId: EntityId, elementId: EntityId, content: AnyElementContentBody - ): Promise { - const element = await this.elementService.findById(elementId); - await this.checkPermission(userId, element, Action.write); + ): Promise { + const element = await this.boardNodeService.findContentElementById(elementId); + await this.boardPermissionService.checkPermission(userId, element, Action.write); + + await this.boardNodeService.updateContent(element, content); - await this.elementService.update(element, content); return element; } async deleteElement(userId: EntityId, elementId: EntityId): Promise { - const element = await this.elementService.findById(elementId); - await this.checkPermission(userId, element, Action.write); + const element = await this.boardNodeService.findContentElementById(elementId); + await this.boardPermissionService.checkPermission(userId, element, Action.write); - await this.elementService.delete(element); + await this.boardNodeService.delete(element); } async checkElementReadPermission(userId: EntityId, elementId: EntityId): Promise { - const element = await this.elementService.findById(elementId); - await this.checkPermission(userId, element, Action.read); + const element = await this.boardNodeService.findContentElementById(elementId); + await this.boardPermissionService.checkPermission(userId, element, Action.read); } async createSubmissionItem( @@ -56,17 +54,10 @@ export class ElementUc extends BaseUc { contentElementId: EntityId, completed: boolean ): Promise { - const submissionContainerElement = await this.elementService.findById(contentElementId); - - if (!isSubmissionContainerElement(submissionContainerElement)) { - throw new UnprocessableEntityException('Cannot create submission-item for non submission-container-element'); - } - - if (!submissionContainerElement.children.every((child) => isSubmissionItem(child))) { - throw new UnprocessableEntityException( - 'Children of submission-container-element must be of type submission-item' - ); - } + const submissionContainerElement = await this.boardNodeService.findByClassAndId( + SubmissionContainerElement, + contentElementId + ); const userSubmissionExists = submissionContainerElement.children .filter(isSubmissionItem) @@ -77,14 +68,19 @@ export class ElementUc extends BaseUc { ); } - await this.checkPermission(userId, submissionContainerElement, Action.read); + await this.boardPermissionService.checkPermission(userId, submissionContainerElement, Action.read); - const boardDoAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable(submissionContainerElement); - if (this.isUserBoardEditor(userId, boardDoAuthorizable.users)) { + // TODO move this in service + const boardNodeAuthorizable = await this.boardNodeAuthorizableService.getBoardAuthorizable( + submissionContainerElement + ); + if (this.boardPermissionService.isUserBoardEditor(userId, boardNodeAuthorizable.users)) { throw new ForbiddenException(); } - const submissionItem = await this.submissionItemService.create(userId, submissionContainerElement, { completed }); + const submissionItem = this.boardNodeFactory.buildSubmissionItem({ completed, userId }); + + await this.boardNodeService.addToParent(submissionContainerElement, submissionItem); return submissionItem; } diff --git a/apps/server/src/modules/board/uc/index.ts b/apps/server/src/modules/board/uc/index.ts index 2be241703c4..03b1407b56e 100644 --- a/apps/server/src/modules/board/uc/index.ts +++ b/apps/server/src/modules/board/uc/index.ts @@ -1,4 +1,3 @@ -export * from './base.uc'; export * from './board.uc'; export * from './card.uc'; export * from './column.uc'; diff --git a/apps/server/src/modules/board/uc/media-board/media-available-line.uc.spec.ts b/apps/server/src/modules/board/uc/media-board/media-available-line.uc.spec.ts index 8a261ed4e60..1a361ff9547 100644 --- a/apps/server/src/modules/board/uc/media-board/media-available-line.uc.spec.ts +++ b/apps/server/src/modules/board/uc/media-board/media-available-line.uc.spec.ts @@ -1,8 +1,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; -import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { Action, AuthorizationService } from '@modules/authorization'; import { ExternalTool } from '@modules/tool/external-tool/domain'; -import { externalToolFactory } from '@modules/tool/external-tool/testing'; import { SchoolExternalTool } from '@modules/tool/school-external-tool/domain'; import { schoolExternalToolFactory } from '@modules/tool/school-external-tool/testing'; import { MediaUserLicense, mediaUserLicenseFactory, UserLicenseService } from '@modules/user-license'; @@ -10,35 +9,39 @@ import { MediaUserLicenseService } from '@modules/user-license/service'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { FeatureDisabledLoggableException } from '@shared/common/loggable-exception'; +import { User } from '@shared/domain/entity'; +import { setupEntities, userFactory as userEntityFactory, userFactory } from '@shared/testing'; +import { externalToolFactory } from '@modules/tool/external-tool/testing'; import { - BoardDoAuthorizable, MediaAvailableLine, MediaAvailableLineElement, MediaBoard, MediaExternalToolElement, -} from '@shared/domain/domainobject'; -import { User } from '@shared/domain/entity'; + MediaBoardColors, +} from '../../domain'; +import type { MediaBoardConfig } from '../../media-board.config'; +import { + BoardNodePermissionService, + BoardNodeService, + MediaAvailableLineService, + MediaBoardService, +} from '../../service'; +import { MediaAvailableLineUc } from './media-available-line.uc'; + import { - boardDoAuthorizableFactory, - mediaAvailableLineElementFactory, mediaAvailableLineFactory, + mediaAvailableLineElementFactory, mediaBoardFactory, mediaExternalToolElementFactory, - setupEntities, - userFactory as userEntityFactory, - userFactory, -} from '@shared/testing'; -import { MediaBoardColors } from '../../domain'; -import type { MediaBoardConfig } from '../../media-board.config'; -import { BoardDoAuthorizableService, MediaAvailableLineService, MediaBoardService } from '../../service'; -import { MediaAvailableLineUc } from './media-available-line.uc'; +} from '../../testing'; describe(MediaAvailableLineUc.name, () => { let module: TestingModule; let uc: MediaAvailableLineUc; let authorizationService: DeepMocked; - let boardDoAuthorizableService: DeepMocked; + let boardNodePermissionService: DeepMocked; + let boardNodeService: DeepMocked; let mediaAvailableLineService: DeepMocked; let configService: DeepMocked>; let mediaBoardService: DeepMocked; @@ -56,8 +59,12 @@ describe(MediaAvailableLineUc.name, () => { useValue: createMock(), }, { - provide: BoardDoAuthorizableService, - useValue: createMock(), + provide: BoardNodePermissionService, + useValue: createMock(), + }, + { + provide: BoardNodeService, + useValue: createMock(), }, { provide: MediaAvailableLineService, @@ -84,7 +91,8 @@ describe(MediaAvailableLineUc.name, () => { uc = module.get(MediaAvailableLineUc); authorizationService = module.get(AuthorizationService); - boardDoAuthorizableService = module.get(BoardDoAuthorizableService); + boardNodePermissionService = module.get(BoardNodePermissionService); + boardNodeService = module.get(BoardNodeService); mediaAvailableLineService = module.get(MediaAvailableLineService); configService = module.get(ConfigService); mediaBoardService = module.get(MediaBoardService); @@ -107,21 +115,23 @@ describe(MediaAvailableLineUc.name, () => { configService.get.mockReturnValueOnce(false); const user: User = userFactory.build(); + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + const mediaExternalToolElement: MediaExternalToolElement = mediaExternalToolElementFactory.build(); - const mediaBoard: MediaBoard = mediaBoardFactory.addChild(mediaExternalToolElement).build(); - const mediaAvailableLineElement: MediaAvailableLineElement = mediaAvailableLineElementFactory.build(); + + const mediaBoard = mediaBoardFactory.build({ children: [mediaExternalToolElement] }); + const mediaAvailableLineElement = mediaAvailableLineElementFactory.build(); const mediaAvailableLine: MediaAvailableLine = mediaAvailableLineFactory .withElement(mediaAvailableLineElement) .build(); + const externalTool1: ExternalTool = externalToolFactory.build(); const externalTool2: ExternalTool = externalToolFactory.build(); const schoolExternalTool1: SchoolExternalTool = schoolExternalToolFactory.build({ toolId: externalTool1.id }); const schoolExternalTool2: SchoolExternalTool = schoolExternalToolFactory.build({ toolId: externalTool2.id }); - const boardDoAuthorizable: BoardDoAuthorizable = boardDoAuthorizableFactory.build(); - mediaBoardService.findById.mockResolvedValueOnce(mediaBoard); - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce(boardDoAuthorizable); + boardNodeService.findByClassAndId.mockResolvedValueOnce(mediaBoard); + mediaAvailableLineService.getUnusedAvailableSchoolExternalTools.mockResolvedValueOnce([ schoolExternalTool1, schoolExternalTool2, @@ -153,16 +163,23 @@ describe(MediaAvailableLineUc.name, () => { await uc.getMediaAvailableLine(user.id, mediaBoard.id); - expect(mediaBoardService.findById).toHaveBeenCalledWith(mediaBoard.id); + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(MediaBoard, mediaBoard.id); }); - it('should check the authorization', async () => { + it('should check the permissions', async () => { + const { user, mediaBoard } = setup(); + + await uc.getMediaAvailableLine(user.id, mediaBoard.id); + + expect(boardNodePermissionService.checkPermission).toHaveBeenCalledWith(user.id, mediaBoard, Action.read); + }); + + it('should get the user from authrorization service', async () => { const { user, mediaBoard } = setup(); await uc.getMediaAvailableLine(user.id, mediaBoard.id); expect(authorizationService.getUserWithPermissions).toHaveBeenCalledWith(user.id); - expect(boardDoAuthorizableService.getBoardAuthorizable).toHaveBeenCalledWith(mediaBoard); }); it('should call the service to get the unused available school external tools', async () => { @@ -173,7 +190,7 @@ describe(MediaAvailableLineUc.name, () => { expect(mediaAvailableLineService.getUnusedAvailableSchoolExternalTools).toHaveBeenCalledWith(user, mediaBoard); }); - it('should call the service to get the unused available school external tools', async () => { + it('should call the service to get the unused available external tools for school', async () => { const { user, mediaBoard, schoolExternalTool1, schoolExternalTool2 } = setup(); await uc.getMediaAvailableLine(user.id, mediaBoard.id); @@ -209,8 +226,8 @@ describe(MediaAvailableLineUc.name, () => { const line: MediaAvailableLine = await uc.getMediaAvailableLine(user.id, mediaBoard.id); expect(line).toEqual({ - collapsed: mediaBoard.mediaAvailableLineCollapsed, - backgroundColor: mediaBoard.mediaAvailableLineBackgroundColor, + collapsed: mediaBoard.collapsed, + backgroundColor: mediaBoard.backgroundColor, elements: [ { schoolExternalToolId: mediaAvailableLineElement.schoolExternalToolId, @@ -229,8 +246,7 @@ describe(MediaAvailableLineUc.name, () => { configService.get.mockReturnValue(true); const user: User = userFactory.build(); - const mediaExternalToolElement: MediaExternalToolElement = mediaExternalToolElementFactory.build(); - const mediaBoard: MediaBoard = mediaBoardFactory.addChild(mediaExternalToolElement).build(); + const mediaBoard: MediaBoard = mediaBoardFactory.build(); const mediaAvailableLineElement: MediaAvailableLineElement = mediaAvailableLineElementFactory.build(); const mediaAvailableLine: MediaAvailableLine = mediaAvailableLineFactory .withElement(mediaAvailableLineElement) @@ -239,13 +255,10 @@ describe(MediaAvailableLineUc.name, () => { const externalTool2: ExternalTool = externalToolFactory.build(); const schoolExternalTool1: SchoolExternalTool = schoolExternalToolFactory.build({ toolId: externalTool1.id }); const schoolExternalTool2: SchoolExternalTool = schoolExternalToolFactory.build({ toolId: externalTool2.id }); - const boardDoAuthorizable: BoardDoAuthorizable = boardDoAuthorizableFactory.build(); userLicenseService.getMediaUserLicensesForUser.mockResolvedValue([]); - mediaBoardService.findById.mockResolvedValueOnce(mediaBoard); - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce(boardDoAuthorizable); + boardNodeService.findByClassAndId.mockResolvedValueOnce(mediaBoard); mediaAvailableLineService.getUnusedAvailableSchoolExternalTools.mockResolvedValueOnce([ schoolExternalTool1, schoolExternalTool2, @@ -281,8 +294,8 @@ describe(MediaAvailableLineUc.name, () => { const line: MediaAvailableLine = await uc.getMediaAvailableLine(user.id, mediaBoard.id); expect(line).toEqual({ - collapsed: mediaBoard.mediaAvailableLineCollapsed, - backgroundColor: mediaBoard.mediaAvailableLineBackgroundColor, + collapsed: mediaBoard.collapsed, + backgroundColor: mediaBoard.backgroundColor, elements: [ { schoolExternalToolId: mediaAvailableLineElement.schoolExternalToolId, @@ -300,8 +313,7 @@ describe(MediaAvailableLineUc.name, () => { configService.get.mockReturnValue(true); const user: User = userFactory.build(); - const mediaExternalToolElement: MediaExternalToolElement = mediaExternalToolElementFactory.build(); - const mediaBoard: MediaBoard = mediaBoardFactory.addChild(mediaExternalToolElement).build(); + const mediaBoard: MediaBoard = mediaBoardFactory.build(); const mediaAvailableLineElement: MediaAvailableLineElement = mediaAvailableLineElementFactory.build(); const mediaAvailableLine: MediaAvailableLine = mediaAvailableLineFactory .withElement(mediaAvailableLineElement) @@ -310,7 +322,6 @@ describe(MediaAvailableLineUc.name, () => { const externalTool2: ExternalTool = externalToolFactory.build(); const schoolExternalTool1: SchoolExternalTool = schoolExternalToolFactory.build({ toolId: externalTool1.id }); const schoolExternalTool2: SchoolExternalTool = schoolExternalToolFactory.build({ toolId: externalTool2.id }); - const boardDoAuthorizable: BoardDoAuthorizable = boardDoAuthorizableFactory.build(); const mediaUserlicense: MediaUserLicense = mediaUserLicenseFactory.build(); mediaUserlicense.mediumId = 'mediumId'; @@ -318,9 +329,8 @@ describe(MediaAvailableLineUc.name, () => { userLicenseService.getMediaUserLicensesForUser.mockResolvedValue([mediaUserlicense]); mediaUserLicenseService.hasLicenseForExternalTool.mockReturnValue(true); - mediaBoardService.findById.mockResolvedValueOnce(mediaBoard); - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce(boardDoAuthorizable); + boardNodeService.findByClassAndId.mockResolvedValueOnce(mediaBoard); + mediaAvailableLineService.getUnusedAvailableSchoolExternalTools.mockResolvedValueOnce([ schoolExternalTool1, schoolExternalTool2, @@ -356,8 +366,8 @@ describe(MediaAvailableLineUc.name, () => { const line: MediaAvailableLine = await uc.getMediaAvailableLine(user.id, mediaBoard.id); expect(line).toEqual({ - collapsed: mediaBoard.mediaAvailableLineCollapsed, - backgroundColor: mediaBoard.mediaAvailableLineBackgroundColor, + collapsed: mediaBoard.collapsed, + backgroundColor: mediaBoard.backgroundColor, elements: [ { schoolExternalToolId: mediaAvailableLineElement.schoolExternalToolId, @@ -375,21 +385,18 @@ describe(MediaAvailableLineUc.name, () => { configService.get.mockReturnValue(true); const user: User = userFactory.build(); - const mediaExternalToolElement: MediaExternalToolElement = mediaExternalToolElementFactory.build(); - const mediaBoard: MediaBoard = mediaBoardFactory.addChild(mediaExternalToolElement).build(); + const mediaBoard: MediaBoard = mediaBoardFactory.build(); const mediaAvailableLine: MediaAvailableLine = mediaAvailableLineFactory.build(); const externalTool1: ExternalTool = externalToolFactory.build({ medium: { mediumId: 'mediumId' } }); const schoolExternalTool1: SchoolExternalTool = schoolExternalToolFactory.build({ toolId: externalTool1.id }); - const boardDoAuthorizable: BoardDoAuthorizable = boardDoAuthorizableFactory.build(); const mediaUserlicense: MediaUserLicense = mediaUserLicenseFactory.build(); userLicenseService.getMediaUserLicensesForUser.mockResolvedValue([mediaUserlicense]); mediaUserLicenseService.hasLicenseForExternalTool.mockReturnValue(false); - mediaBoardService.findById.mockResolvedValueOnce(mediaBoard); - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce(boardDoAuthorizable); + boardNodeService.findByClassAndId.mockResolvedValueOnce(mediaBoard); + mediaAvailableLineService.getUnusedAvailableSchoolExternalTools.mockResolvedValueOnce([schoolExternalTool1]); mediaAvailableLineService.getAvailableExternalToolsForSchool.mockResolvedValueOnce([externalTool1]); mediaAvailableLineService.matchTools.mockReturnValueOnce([[externalTool1, schoolExternalTool1]]); @@ -407,8 +414,8 @@ describe(MediaAvailableLineUc.name, () => { const line: MediaAvailableLine = await uc.getMediaAvailableLine(user.id, mediaBoard.id); expect(line).toEqual({ - collapsed: mediaBoard.mediaAvailableLineCollapsed, - backgroundColor: mediaBoard.mediaAvailableLineBackgroundColor, + collapsed: mediaBoard.collapsed, + backgroundColor: mediaBoard.backgroundColor, elements: [], }); }); @@ -443,30 +450,22 @@ describe(MediaAvailableLineUc.name, () => { const setup = () => { const user = userEntityFactory.build(); const mediaBoard = mediaBoardFactory.build(); - const boardDoAuthorizable = boardDoAuthorizableFactory.build(); configService.get.mockReturnValueOnce(true); - mediaBoardService.findById.mockResolvedValueOnce(mediaBoard); - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce(boardDoAuthorizable); + boardNodeService.findByClassAndId.mockResolvedValueOnce(mediaBoard); return { user, mediaBoard, - boardDoAuthorizable, }; }; - it('should check the authorization', async () => { - const { user, mediaBoard, boardDoAuthorizable } = setup(); + it('should check the permission', async () => { + const { user, mediaBoard } = setup(); await uc.updateAvailableLineColor(user.id, mediaBoard.id, MediaBoardColors.RED); - expect(authorizationService.checkPermission).toHaveBeenCalledWith( - user, - boardDoAuthorizable, - AuthorizationContextBuilder.write([]) - ); + expect(boardNodePermissionService.checkPermission).toHaveBeenCalledWith(user.id, mediaBoard, Action.write); }); it('should collapse the line', async () => { @@ -474,7 +473,7 @@ describe(MediaAvailableLineUc.name, () => { await uc.updateAvailableLineColor(user.id, mediaBoard.id, MediaBoardColors.RED); - expect(mediaBoardService.updateAvailableLineColor).toHaveBeenCalledWith(mediaBoard, MediaBoardColors.RED); + expect(mediaBoardService.updateBackgroundColor).toHaveBeenCalledWith(mediaBoard, MediaBoardColors.RED); }); }); @@ -506,30 +505,22 @@ describe(MediaAvailableLineUc.name, () => { const setup = () => { const user = userEntityFactory.build(); const mediaBoard = mediaBoardFactory.build(); - const boardDoAuthorizable = boardDoAuthorizableFactory.build(); configService.get.mockReturnValueOnce(true); - mediaBoardService.findById.mockResolvedValueOnce(mediaBoard); - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce(boardDoAuthorizable); + boardNodeService.findByClassAndId.mockResolvedValueOnce(mediaBoard); return { user, mediaBoard, - boardDoAuthorizable, }; }; it('should check the authorization', async () => { - const { user, mediaBoard, boardDoAuthorizable } = setup(); + const { user, mediaBoard } = setup(); await uc.collapseAvailableLine(user.id, mediaBoard.id, true); - expect(authorizationService.checkPermission).toHaveBeenCalledWith( - user, - boardDoAuthorizable, - AuthorizationContextBuilder.write([]) - ); + expect(boardNodePermissionService.checkPermission).toHaveBeenCalledWith(user.id, mediaBoard, Action.write); }); it('should collapse the line', async () => { @@ -537,7 +528,7 @@ describe(MediaAvailableLineUc.name, () => { await uc.collapseAvailableLine(user.id, mediaBoard.id, true); - expect(mediaBoardService.collapseAvailableLine).toHaveBeenCalledWith(mediaBoard, true); + expect(mediaBoardService.updateCollapsed).toHaveBeenCalledWith(mediaBoard, true); }); }); diff --git a/apps/server/src/modules/board/uc/media-board/media-available-line.uc.ts b/apps/server/src/modules/board/uc/media-board/media-available-line.uc.ts index 275fd859134..b2dc2dd0716 100644 --- a/apps/server/src/modules/board/uc/media-board/media-available-line.uc.ts +++ b/apps/server/src/modules/board/uc/media-board/media-available-line.uc.ts @@ -1,26 +1,31 @@ -import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { Action, AuthorizationService } from '@modules/authorization'; +import { ExternalTool } from '@modules/tool/external-tool/domain'; import { SchoolExternalTool } from '@modules/tool/school-external-tool/domain'; import { MediaUserLicense, UserLicenseService } from '@modules/user-license'; import { MediaUserLicenseService } from '@modules/user-license/service'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { FeatureDisabledLoggableException } from '@shared/common/loggable-exception'; -import { BoardDoAuthorizable, MediaAvailableLine, type MediaBoard } from '@shared/domain/domainobject'; -import { User } from '@shared/domain/entity'; import { EntityId } from '@shared/domain/types'; -import { ExternalTool } from '@src/modules/tool/external-tool/domain'; -import { MediaBoardColors } from '../../domain'; +import { MediaAvailableLine, MediaBoard } from '../../domain'; import type { MediaBoardConfig } from '../../media-board.config'; -import { BoardDoAuthorizableService, MediaAvailableLineService, MediaBoardService } from '../../service'; +import { + BoardNodePermissionService, + BoardNodeService, + MediaAvailableLineService, + MediaBoardService, +} from '../../service'; +import { MediaBoardColors } from '../../domain/media-board/types'; @Injectable() export class MediaAvailableLineUc { constructor( private readonly authorizationService: AuthorizationService, - private readonly boardDoAuthorizableService: BoardDoAuthorizableService, + private readonly boardNodePermissionService: BoardNodePermissionService, + private readonly boardNodeService: BoardNodeService, private readonly mediaAvailableLineService: MediaAvailableLineService, - private readonly configService: ConfigService, private readonly mediaBoardService: MediaBoardService, + private readonly configService: ConfigService, private readonly userLicenseService: UserLicenseService, private readonly mediaUserLicenseService: MediaUserLicenseService ) {} @@ -28,14 +33,11 @@ export class MediaAvailableLineUc { public async getMediaAvailableLine(userId: EntityId, boardId: EntityId): Promise { this.checkFeatureEnabled(); - const mediaBoard: MediaBoard = await this.mediaBoardService.findById(boardId); + const mediaBoard: MediaBoard = await this.boardNodeService.findByClassAndId(MediaBoard, boardId); - const user: User = await this.authorizationService.getUserWithPermissions(userId); - const boardDoAuthorizable: BoardDoAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable( - mediaBoard - ); - this.authorizationService.checkPermission(user, boardDoAuthorizable, AuthorizationContextBuilder.read([])); + await this.boardNodePermissionService.checkPermission(userId, mediaBoard, Action.read); + const user = await this.authorizationService.getUserWithPermissions(userId); const schoolExternalToolsForAvailableMediaLine: SchoolExternalTool[] = await this.mediaAvailableLineService.getUnusedAvailableSchoolExternalTools(user, mediaBoard); @@ -62,13 +64,11 @@ export class MediaAvailableLineUc { public async updateAvailableLineColor(userId: EntityId, boardId: EntityId, color: MediaBoardColors): Promise { this.checkFeatureEnabled(); - const board: MediaBoard = await this.mediaBoardService.findById(boardId); + const board: MediaBoard = await this.boardNodeService.findByClassAndId(MediaBoard, boardId); - const user: User = await this.authorizationService.getUserWithPermissions(userId); - const boardDoAuthorizable: BoardDoAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable(board); - this.authorizationService.checkPermission(user, boardDoAuthorizable, AuthorizationContextBuilder.write([])); + await this.boardNodePermissionService.checkPermission(userId, board, Action.write); - await this.mediaBoardService.updateAvailableLineColor(board, color); + await this.mediaBoardService.updateBackgroundColor(board, color); } public async collapseAvailableLine( @@ -78,13 +78,11 @@ export class MediaAvailableLineUc { ): Promise { this.checkFeatureEnabled(); - const board: MediaBoard = await this.mediaBoardService.findById(boardId); + const board: MediaBoard = await this.boardNodeService.findByClassAndId(MediaBoard, boardId); - const user: User = await this.authorizationService.getUserWithPermissions(userId); - const boardDoAuthorizable: BoardDoAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable(board); - this.authorizationService.checkPermission(user, boardDoAuthorizable, AuthorizationContextBuilder.write([])); + await this.boardNodePermissionService.checkPermission(userId, board, Action.write); - await this.mediaBoardService.collapseAvailableLine(board, mediaAvailableLineCollapsed); + await this.mediaBoardService.updateCollapsed(board, mediaAvailableLineCollapsed); } private checkFeatureEnabled(): void { diff --git a/apps/server/src/modules/board/uc/media-board/media-board.uc.spec.ts b/apps/server/src/modules/board/uc/media-board/media-board.uc.spec.ts index 8904af6fac2..8d1b231cb98 100644 --- a/apps/server/src/modules/board/uc/media-board/media-board.uc.spec.ts +++ b/apps/server/src/modules/board/uc/media-board/media-board.uc.spec.ts @@ -1,19 +1,14 @@ import { createMock, type DeepMocked } from '@golevelup/ts-jest'; -import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { Action, AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { FeatureDisabledLoggableException } from '@shared/common/loggable-exception'; -import { - boardDoAuthorizableFactory, - mediaBoardFactory, - mediaLineFactory, - setupEntities, - userFactory as userEntityFactory, -} from '@shared/testing'; -import { MediaBoardLayoutType } from '../../domain'; +import { setupEntities, userFactory as userEntityFactory } from '@shared/testing'; import type { MediaBoardConfig } from '../../media-board.config'; -import { BoardDoAuthorizableService, MediaBoardService, MediaLineService } from '../../service'; +import { BoardNodePermissionService, BoardNodeService, MediaBoardService } from '../../service'; +import { mediaBoardFactory, mediaLineFactory } from '../../testing'; import { MediaBoardUc } from './media-board.uc'; +import { BoardLayout, MediaBoardNodeFactory } from '../../domain'; describe(MediaBoardUc.name, () => { let module: TestingModule; @@ -21,9 +16,10 @@ describe(MediaBoardUc.name, () => { let authorizationService: DeepMocked; let mediaBoardService: DeepMocked; - let mediaLineService: DeepMocked; - let boardDoAuthorizableService: DeepMocked; + let boardNodeService: DeepMocked; + let boardNodePermissionService: DeepMocked; let configService: DeepMocked>; + let mediaBoardNodeFactory: DeepMocked; beforeAll(async () => { await setupEntities(); @@ -40,26 +36,31 @@ describe(MediaBoardUc.name, () => { useValue: createMock(), }, { - provide: MediaLineService, - useValue: createMock(), + provide: BoardNodeService, + useValue: createMock(), }, { - provide: BoardDoAuthorizableService, - useValue: createMock(), + provide: BoardNodePermissionService, + useValue: createMock(), }, { provide: ConfigService, useValue: createMock(), }, + { + provide: MediaBoardNodeFactory, + useValue: createMock(), + }, ], }).compile(); uc = module.get(MediaBoardUc); authorizationService = module.get(AuthorizationService); mediaBoardService = module.get(MediaBoardService); - mediaLineService = module.get(MediaLineService); - boardDoAuthorizableService = module.get(BoardDoAuthorizableService); + boardNodeService = module.get(BoardNodeService); + boardNodePermissionService = module.get(BoardNodePermissionService); configService = module.get(ConfigService); + mediaBoardNodeFactory = module.get(MediaBoardNodeFactory); }); afterAll(async () => { @@ -78,8 +79,8 @@ describe(MediaBoardUc.name, () => { configService.get.mockReturnValueOnce(true); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - mediaBoardService.findIdsByExternalReference.mockResolvedValueOnce([]); - mediaBoardService.create.mockResolvedValueOnce(mediaBoard); + mediaBoardService.findByExternalReference.mockResolvedValueOnce([]); + mediaBoardNodeFactory.buildMediaBoard.mockReturnValueOnce(mediaBoard); return { user, @@ -115,8 +116,7 @@ describe(MediaBoardUc.name, () => { configService.get.mockReturnValueOnce(true); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - mediaBoardService.findIdsByExternalReference.mockResolvedValueOnce([mediaBoard.id]); - mediaBoardService.findById.mockResolvedValueOnce(mediaBoard); + mediaBoardService.findByExternalReference.mockResolvedValueOnce([mediaBoard]); return { user, @@ -170,32 +170,25 @@ describe(MediaBoardUc.name, () => { const user = userEntityFactory.build(); const mediaBoard = mediaBoardFactory.build(); const mediaLine = mediaLineFactory.build(); - const boardDoAuthorizable = boardDoAuthorizableFactory.build(); configService.get.mockReturnValueOnce(true); - boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce(boardDoAuthorizable); - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - mediaBoardService.findById.mockResolvedValueOnce(mediaBoard); - mediaLineService.create.mockResolvedValueOnce(mediaLine); + + boardNodeService.findByClassAndId.mockResolvedValueOnce(mediaBoard); + mediaBoardNodeFactory.buildMediaLine.mockReturnValueOnce(mediaLine); return { user, mediaBoard, mediaLine, - boardDoAuthorizable, }; }; it('should check the authorization', async () => { - const { user, mediaBoard, boardDoAuthorizable } = setup(); + const { user, mediaBoard } = setup(); await uc.createLine(user.id, mediaBoard.id); - expect(authorizationService.checkPermission).toHaveBeenCalledWith( - user, - boardDoAuthorizable, - AuthorizationContextBuilder.write([]) - ); + expect(boardNodePermissionService.checkPermission).toHaveBeenCalledWith(user.id, mediaBoard, Action.write); }); it('should return a new media line', async () => { @@ -233,40 +226,32 @@ describe(MediaBoardUc.name, () => { const setup = () => { const user = userEntityFactory.build(); const mediaBoard = mediaBoardFactory.build({ - layout: MediaBoardLayoutType.LIST, + layout: BoardLayout.LIST, }); - const boardDoAuthorizable = boardDoAuthorizableFactory.build(); configService.get.mockReturnValueOnce(true); - mediaBoardService.findById.mockResolvedValueOnce(mediaBoard); - boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce(boardDoAuthorizable); - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + boardNodeService.findByClassAndId.mockResolvedValueOnce(mediaBoard); return { user, mediaBoard, - boardDoAuthorizable, }; }; it('should check the authorization', async () => { - const { user, mediaBoard, boardDoAuthorizable } = setup(); + const { user, mediaBoard } = setup(); - await uc.setLayout(user.id, mediaBoard.id, MediaBoardLayoutType.GRID); + await uc.setLayout(user.id, mediaBoard.id, BoardLayout.GRID); - expect(authorizationService.checkPermission).toHaveBeenCalledWith( - user, - boardDoAuthorizable, - AuthorizationContextBuilder.write([]) - ); + expect(boardNodePermissionService.checkPermission).toHaveBeenCalledWith(user.id, mediaBoard, Action.write); }); it('should change the layout', async () => { const { user, mediaBoard } = setup(); - await uc.setLayout(user.id, mediaBoard.id, MediaBoardLayoutType.GRID); + await uc.setLayout(user.id, mediaBoard.id, BoardLayout.GRID); - expect(mediaBoardService.setLayout).toHaveBeenCalledWith(mediaBoard, MediaBoardLayoutType.GRID); + expect(mediaBoardService.updateLayout).toHaveBeenCalledWith(mediaBoard, BoardLayout.GRID); }); }); @@ -286,7 +271,7 @@ describe(MediaBoardUc.name, () => { it('should throw an exception', async () => { const { user, mediaBoard } = setup(); - await expect(uc.setLayout(user.id, mediaBoard.id, MediaBoardLayoutType.GRID)).rejects.toThrow( + await expect(uc.setLayout(user.id, mediaBoard.id, BoardLayout.GRID)).rejects.toThrow( FeatureDisabledLoggableException ); }); diff --git a/apps/server/src/modules/board/uc/media-board/media-board.uc.ts b/apps/server/src/modules/board/uc/media-board/media-board.uc.ts index 557fa27ed1f..6912c7cd369 100644 --- a/apps/server/src/modules/board/uc/media-board/media-board.uc.ts +++ b/apps/server/src/modules/board/uc/media-board/media-board.uc.ts @@ -1,26 +1,29 @@ -import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { Action, AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { FeatureDisabledLoggableException } from '@shared/common/loggable-exception'; -import { - BoardDoAuthorizable, - BoardExternalReferenceType, - type MediaBoard, - type MediaLine, -} from '@shared/domain/domainobject'; import { User } from '@shared/domain/entity'; import type { EntityId } from '@shared/domain/types'; -import { MediaBoardLayoutType } from '../../domain'; +import { + BoardExternalReference, + BoardExternalReferenceType, + BoardLayout, + MediaBoard, + MediaBoardColors, + MediaBoardNodeFactory, + MediaLine, +} from '../../domain'; import type { MediaBoardConfig } from '../../media-board.config'; -import { BoardDoAuthorizableService, MediaBoardService, MediaLineService } from '../../service'; +import { BoardNodePermissionService, BoardNodeService, MediaBoardService } from '../../service'; @Injectable() export class MediaBoardUc { constructor( private readonly authorizationService: AuthorizationService, + private readonly boardNodeService: BoardNodeService, + private readonly boardNodePermissionService: BoardNodePermissionService, + private readonly mediaBoardNodeFactory: MediaBoardNodeFactory, private readonly mediaBoardService: MediaBoardService, - private readonly mediaLineService: MediaLineService, - private readonly boardDoAuthorizableService: BoardDoAuthorizableService, private readonly configService: ConfigService ) {} @@ -30,19 +33,24 @@ export class MediaBoardUc { const user: User = await this.authorizationService.getUserWithPermissions(userId); this.authorizationService.checkPermission(user, user, AuthorizationContextBuilder.read([])); - const boardIds: EntityId[] = await this.mediaBoardService.findIdsByExternalReference({ + const context: BoardExternalReference = { type: BoardExternalReferenceType.User, id: user.id, - }); + }; + + const existingBoards: MediaBoard[] = await this.mediaBoardService.findByExternalReference(context); let board: MediaBoard; - if (!boardIds.length) { - board = await this.mediaBoardService.create({ - type: BoardExternalReferenceType.User, - id: user.id, + if (!existingBoards.length) { + board = this.mediaBoardNodeFactory.buildMediaBoard({ + context, + layout: BoardLayout.LIST, + backgroundColor: MediaBoardColors.TRANSPARENT, + collapsed: false, }); + await this.boardNodeService.addRoot(board); } else { - board = await this.mediaBoardService.findById(boardIds[0]); + board = existingBoards[0]; } return board; @@ -51,27 +59,28 @@ export class MediaBoardUc { public async createLine(userId: EntityId, boardId: EntityId): Promise { this.checkFeatureEnabled(); - const board: MediaBoard = await this.mediaBoardService.findById(boardId); + const board: MediaBoard = await this.boardNodeService.findByClassAndId(MediaBoard, boardId); - const user: User = await this.authorizationService.getUserWithPermissions(userId); - const boardDoAuthorizable: BoardDoAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable(board); - this.authorizationService.checkPermission(user, boardDoAuthorizable, AuthorizationContextBuilder.write([])); + await this.boardNodePermissionService.checkPermission(userId, board, Action.write); - const line: MediaLine = await this.mediaLineService.create(board); + const line = this.mediaBoardNodeFactory.buildMediaLine({ + title: '', + backgroundColor: MediaBoardColors.TRANSPARENT, + collapsed: false, + }); + await this.boardNodeService.addToParent(board, line); return line; } - public async setLayout(userId: EntityId, boardId: EntityId, layout: MediaBoardLayoutType): Promise { + public async setLayout(userId: EntityId, boardId: EntityId, layout: BoardLayout): Promise { this.checkFeatureEnabled(); - const board: MediaBoard = await this.mediaBoardService.findById(boardId); + const board: MediaBoard = await this.boardNodeService.findByClassAndId(MediaBoard, boardId); - const user: User = await this.authorizationService.getUserWithPermissions(userId); - const boardDoAuthorizable: BoardDoAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable(board); - this.authorizationService.checkPermission(user, boardDoAuthorizable, AuthorizationContextBuilder.write([])); + await this.boardNodePermissionService.checkPermission(userId, board, Action.write); - await this.mediaBoardService.setLayout(board, layout); + await this.mediaBoardService.updateLayout(board, layout); } private checkFeatureEnabled(): void { diff --git a/apps/server/src/modules/board/uc/media-board/media-element.uc.spec.ts b/apps/server/src/modules/board/uc/media-board/media-element.uc.spec.ts index 12588e46c60..b74483cc06a 100644 --- a/apps/server/src/modules/board/uc/media-board/media-element.uc.spec.ts +++ b/apps/server/src/modules/board/uc/media-board/media-element.uc.spec.ts @@ -1,25 +1,19 @@ import { createMock, type DeepMocked } from '@golevelup/ts-jest'; -import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { ContextExternalTool } from '@modules/tool/context-external-tool/domain'; -import { contextExternalToolFactory } from '@modules/tool/context-external-tool/testing'; +import { Action, AuthorizationService } from '@modules/authorization'; import { SchoolExternalToolService } from '@modules/tool/school-external-tool'; import { SchoolExternalTool } from '@modules/tool/school-external-tool/domain'; import { schoolExternalToolFactory } from '@modules/tool/school-external-tool/testing'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { FeatureDisabledLoggableException } from '@shared/common/loggable-exception'; -import { MediaExternalToolElement } from '@shared/domain/domainobject'; -import { - boardDoAuthorizableFactory, - mediaBoardFactory, - mediaExternalToolElementFactory, - mediaLineFactory, - setupEntities, - userFactory as userEntityFactory, -} from '@shared/testing'; +import { setupEntities, userFactory as userEntityFactory } from '@shared/testing'; +import { contextExternalToolFactory } from '@modules/tool/context-external-tool/testing'; +import { MediaBoard, MediaBoardNodeFactory, MediaExternalToolElement, MediaLine } from '../../domain'; import { MediaBoardElementAlreadyExistsLoggableException } from '../../loggable'; import type { MediaBoardConfig } from '../../media-board.config'; -import { BoardDoAuthorizableService, MediaBoardService, MediaElementService, MediaLineService } from '../../service'; +import { BoardNodePermissionService, BoardNodeService, MediaBoardService } from '../../service'; +import { mediaBoardFactory, mediaExternalToolElementFactory, mediaLineFactory } from '../../testing'; import { MediaElementUc } from './media-element.uc'; describe(MediaElementUc.name, () => { @@ -27,11 +21,11 @@ describe(MediaElementUc.name, () => { let uc: MediaElementUc; let authorizationService: DeepMocked; - let mediaLineService: DeepMocked; - let mediaElementService: DeepMocked; - let boardDoAuthorizableService: DeepMocked; + let boardNodeService: DeepMocked; + let boardNodePermissionService: DeepMocked; let configService: DeepMocked>; let mediaBoardService: DeepMocked; + let mediaBoardNodeFactory: DeepMocked; let schoolExternalToolService: DeepMocked; beforeAll(async () => { @@ -45,16 +39,12 @@ describe(MediaElementUc.name, () => { useValue: createMock(), }, { - provide: MediaLineService, - useValue: createMock(), - }, - { - provide: MediaElementService, - useValue: createMock(), + provide: BoardNodeService, + useValue: createMock(), }, { - provide: BoardDoAuthorizableService, - useValue: createMock(), + provide: BoardNodePermissionService, + useValue: createMock(), }, { provide: ConfigService, @@ -64,6 +54,10 @@ describe(MediaElementUc.name, () => { provide: MediaBoardService, useValue: createMock(), }, + { + provide: MediaBoardNodeFactory, + useValue: createMock(), + }, { provide: SchoolExternalToolService, useValue: createMock(), @@ -73,11 +67,11 @@ describe(MediaElementUc.name, () => { uc = module.get(MediaElementUc); authorizationService = module.get(AuthorizationService); - mediaLineService = module.get(MediaLineService); - mediaElementService = module.get(MediaElementService); - boardDoAuthorizableService = module.get(BoardDoAuthorizableService); + boardNodeService = module.get(BoardNodeService); + boardNodePermissionService = module.get(BoardNodePermissionService); configService = module.get(ConfigService); mediaBoardService = module.get(MediaBoardService); + mediaBoardNodeFactory = module.get(MediaBoardNodeFactory); schoolExternalToolService = module.get(SchoolExternalToolService); }); @@ -95,32 +89,32 @@ describe(MediaElementUc.name, () => { const user = userEntityFactory.build(); const mediaLine = mediaLineFactory.build(); const mediaElement = mediaExternalToolElementFactory.build(); - const boardDoAuthorizable = boardDoAuthorizableFactory.build(); configService.get.mockReturnValueOnce(true); - boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce(boardDoAuthorizable); - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - mediaLineService.findById.mockResolvedValueOnce(mediaLine); - mediaElementService.findById.mockResolvedValueOnce(mediaElement); + boardNodeService.findByClassAndId.mockResolvedValueOnce(mediaElement).mockResolvedValueOnce(mediaLine); return { user, mediaElement, mediaLine, - boardDoAuthorizable, }; }; + it('should find element and target line', async () => { + const { user, mediaLine, mediaElement } = setup(); + + await uc.moveElement(user.id, mediaElement.id, mediaLine.id, 1); + + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(MediaExternalToolElement, mediaElement.id); + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(MediaLine, mediaLine.id); + }); + it('should check the authorization', async () => { - const { user, mediaLine, mediaElement, boardDoAuthorizable } = setup(); + const { user, mediaLine, mediaElement } = setup(); await uc.moveElement(user.id, mediaElement.id, mediaLine.id, 1); - expect(authorizationService.checkPermission).toHaveBeenCalledWith( - user, - boardDoAuthorizable, - AuthorizationContextBuilder.write([]) - ); + expect(boardNodePermissionService.checkPermission).toHaveBeenCalledWith(user.id, mediaLine, Action.write); }); it('should move the element', async () => { @@ -128,7 +122,7 @@ describe(MediaElementUc.name, () => { await uc.moveElement(user.id, mediaElement.id, mediaLine.id, 1); - expect(mediaElementService.move).toHaveBeenCalledWith(mediaElement, mediaLine, 1); + expect(boardNodeService.move).toHaveBeenCalledWith(mediaElement, mediaLine, 1); }); }); @@ -168,39 +162,57 @@ describe(MediaElementUc.name, () => { const contextExternalTool: ContextExternalTool = contextExternalToolFactory .withSchoolExternalToolRef(schoolExternalTool.id, user.school.id) .buildWithId(); - const boardDoAuthorizable = boardDoAuthorizableFactory.build(); configService.get.mockReturnValueOnce(true); - mediaLineService.findById.mockResolvedValueOnce(mediaLine); - boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce(boardDoAuthorizable); + boardNodeService.findByClassAndId.mockResolvedValueOnce(mediaLine).mockResolvedValueOnce(mediaBoard); + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - mediaBoardService.findByDescendant.mockResolvedValueOnce(mediaBoard); schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); - mediaElementService.checkElementExists.mockResolvedValueOnce(false); - mediaElementService.createContextExternalToolForMediaBoard.mockResolvedValueOnce(contextExternalTool); - mediaElementService.createExternalToolElement.mockResolvedValueOnce(mediaElement); + mediaBoardService.checkElementExists.mockResolvedValueOnce(false); + + mediaBoardService.createContextExternalToolForMediaBoard.mockResolvedValueOnce(contextExternalTool); + mediaBoardNodeFactory.buildExternalToolElement.mockReturnValueOnce(mediaElement); return { user, mediaElement, mediaLine, - boardDoAuthorizable, mediaBoard, schoolExternalTool, contextExternalTool, }; }; + it('should find the line', async () => { + const { user, mediaLine, mediaElement } = setup(); + + await uc.createElement(user.id, mediaElement.id, mediaLine.id, 1); + + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(MediaLine, mediaLine.id); + }); + it('should check the authorization', async () => { - const { user, mediaLine, mediaElement, boardDoAuthorizable } = setup(); + const { user, mediaLine, mediaElement } = setup(); await uc.createElement(user.id, mediaElement.id, mediaLine.id, 1); - expect(authorizationService.checkPermission).toHaveBeenCalledWith( - user, - boardDoAuthorizable, - AuthorizationContextBuilder.write([]) - ); + expect(boardNodePermissionService.checkPermission).toHaveBeenCalledWith(user.id, mediaLine, Action.write); + }); + + it('should find the board', async () => { + const { user, mediaLine, mediaElement } = setup(); + + await uc.createElement(user.id, mediaElement.id, mediaLine.id, 1); + + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(MediaBoard, mediaLine.rootId); + }); + + it('should find the school external tool', async () => { + const { user, mediaLine, mediaElement } = setup(); + + await uc.createElement(user.id, mediaElement.id, mediaLine.id, 1); + + expect(schoolExternalToolService.findById).toHaveBeenCalledWith(mediaElement.id); }); it('should check if element exists already on board', async () => { @@ -208,7 +220,7 @@ describe(MediaElementUc.name, () => { await uc.createElement(user.id, mediaElement.id, mediaLine.id, 1); - expect(mediaElementService.checkElementExists).toHaveBeenCalledWith(mediaBoard, schoolExternalTool); + expect(mediaBoardService.checkElementExists).toHaveBeenCalledWith(mediaBoard, schoolExternalTool); }); it('should create the element', async () => { @@ -216,7 +228,17 @@ describe(MediaElementUc.name, () => { await uc.createElement(user.id, mediaElement.id, mediaLine.id, 1); - expect(mediaElementService.createExternalToolElement).toHaveBeenCalledWith(mediaLine, 1, contextExternalTool); + expect(mediaBoardNodeFactory.buildExternalToolElement).toHaveBeenCalledWith({ + contextExternalToolId: contextExternalTool.id, + }); + }); + + it('should add the element to the line', async () => { + const { user, mediaLine, mediaElement } = setup(); + + await uc.createElement(user.id, mediaElement.id, mediaLine.id, 1); + + expect(boardNodeService.addToParent).toHaveBeenCalledWith(mediaLine, mediaElement, 1); }); it('should return the created element', async () => { @@ -238,21 +260,19 @@ describe(MediaElementUc.name, () => { const contextExternalTool: ContextExternalTool = contextExternalToolFactory .withSchoolExternalToolRef(schoolExternalTool.id, user.school.id) .buildWithId(); - const boardDoAuthorizable = boardDoAuthorizableFactory.build(); configService.get.mockReturnValueOnce(true); - mediaLineService.findById.mockResolvedValueOnce(mediaLine); - boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce(boardDoAuthorizable); + boardNodeService.findByClassAndId.mockResolvedValueOnce(mediaLine).mockResolvedValueOnce(mediaBoard); + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - mediaBoardService.findByDescendant.mockResolvedValueOnce(mediaBoard); + schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); - mediaElementService.checkElementExists.mockResolvedValueOnce(true); + mediaBoardService.checkElementExists.mockResolvedValueOnce(true); return { user, mediaElement, mediaLine, - boardDoAuthorizable, mediaBoard, schoolExternalTool, contextExternalTool, @@ -298,30 +318,22 @@ describe(MediaElementUc.name, () => { const setup = () => { const user = userEntityFactory.build(); const mediaElement = mediaExternalToolElementFactory.build(); - const boardDoAuthorizable = boardDoAuthorizableFactory.build(); configService.get.mockReturnValueOnce(true); - boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce(boardDoAuthorizable); - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - mediaElementService.findById.mockResolvedValueOnce(mediaElement); + boardNodeService.findByClassAndId.mockResolvedValueOnce(mediaElement); return { user, mediaElement, - boardDoAuthorizable, }; }; it('should check the authorization', async () => { - const { user, mediaElement, boardDoAuthorizable } = setup(); + const { user, mediaElement } = setup(); await uc.deleteElement(user.id, mediaElement.id); - expect(authorizationService.checkPermission).toHaveBeenCalledWith( - user, - boardDoAuthorizable, - AuthorizationContextBuilder.write([]) - ); + expect(boardNodePermissionService.checkPermission).toHaveBeenCalledWith(user.id, mediaElement, Action.write); }); it('should delete the element', async () => { @@ -329,7 +341,7 @@ describe(MediaElementUc.name, () => { await uc.deleteElement(user.id, mediaElement.id); - expect(mediaElementService.delete).toHaveBeenCalledWith(mediaElement); + expect(boardNodeService.delete).toHaveBeenCalledWith(mediaElement); }); }); diff --git a/apps/server/src/modules/board/uc/media-board/media-element.uc.ts b/apps/server/src/modules/board/uc/media-board/media-element.uc.ts index 960f3b9f400..af8830bae38 100644 --- a/apps/server/src/modules/board/uc/media-board/media-element.uc.ts +++ b/apps/server/src/modules/board/uc/media-board/media-element.uc.ts @@ -1,32 +1,25 @@ -import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { Action, AuthorizationService } from '@modules/authorization'; import { ContextExternalTool } from '@modules/tool/context-external-tool/domain'; import { SchoolExternalToolService } from '@modules/tool/school-external-tool'; import { SchoolExternalTool } from '@modules/tool/school-external-tool/domain'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { FeatureDisabledLoggableException } from '@shared/common/loggable-exception'; -import { - type AnyMediaContentElementDo, - BoardDoAuthorizable, - MediaBoard, - MediaExternalToolElement, - type MediaLine, -} from '@shared/domain/domainobject'; -import { User as UserEntity } from '@shared/domain/entity'; import type { EntityId } from '@shared/domain/types'; +import { MediaBoard, MediaBoardNodeFactory, MediaExternalToolElement, MediaLine } from '../../domain'; import { MediaBoardElementAlreadyExistsLoggableException } from '../../loggable'; import type { MediaBoardConfig } from '../../media-board.config'; -import { BoardDoAuthorizableService, MediaBoardService, MediaElementService, MediaLineService } from '../../service'; +import { BoardNodePermissionService, BoardNodeService, MediaBoardService } from '../../service'; @Injectable() export class MediaElementUc { constructor( private readonly authorizationService: AuthorizationService, - private readonly mediaLineService: MediaLineService, - private readonly mediaElementService: MediaElementService, - private readonly boardDoAuthorizableService: BoardDoAuthorizableService, + private readonly boardNodeService: BoardNodeService, + private readonly boardNodePermissionService: BoardNodePermissionService, private readonly configService: ConfigService, private readonly mediaBoardService: MediaBoardService, + private readonly mediaBoardNodeFactory: MediaBoardNodeFactory, private readonly schoolExternalToolService: SchoolExternalToolService ) {} @@ -38,23 +31,12 @@ export class MediaElementUc { ): Promise { this.checkFeatureEnabled(); - const targetLine: MediaLine = await this.mediaLineService.findById(targetLineId); + const element = await this.boardNodeService.findByClassAndId(MediaExternalToolElement, elementId); + const targetLine = await this.boardNodeService.findByClassAndId(MediaLine, targetLineId); - const user: UserEntity = await this.authorizationService.getUserWithPermissions(userId); - const boardDoAuthorizable: BoardDoAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable( - targetLine - ); - this.authorizationService.checkPermission(user, boardDoAuthorizable, AuthorizationContextBuilder.write([])); + await this.boardNodePermissionService.checkPermission(userId, targetLine, Action.write); - const element: AnyMediaContentElementDo = await this.mediaElementService.findById(elementId); - - await this.mediaElementService.move(element, targetLine, targetPosition); - } - - private checkFeatureEnabled() { - if (!this.configService.get('FEATURE_MEDIA_SHELF_ENABLED')) { - throw new FeatureDisabledLoggableException('FEATURE_MEDIA_SHELF_ENABLED'); - } + await this.boardNodeService.move(element, targetLine, targetPosition); } public async createElement( @@ -65,52 +47,56 @@ export class MediaElementUc { ): Promise { this.checkFeatureEnabled(); - const line: MediaLine = await this.mediaLineService.findById(lineId); + const line = await this.boardNodeService.findByClassAndId(MediaLine, lineId); - const user: UserEntity = await this.authorizationService.getUserWithPermissions(userId); - const boardDoAuthorizable: BoardDoAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable(line); - this.authorizationService.checkPermission(user, boardDoAuthorizable, AuthorizationContextBuilder.write([])); + await this.boardNodePermissionService.checkPermission(userId, line, Action.write); - const mediaBoard: MediaBoard = await this.mediaBoardService.findByDescendant(line); + const mediaBoard = await this.boardNodeService.findByClassAndId(MediaBoard, line.rootId); const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.findById(schoolExternalToolId); await this.checkElementExistsAlreadyOnBoardAndThrow(mediaBoard, schoolExternalTool); - const createdContexExternalTool: ContextExternalTool = - await this.mediaElementService.createContextExternalToolForMediaBoard(user, schoolExternalTool, mediaBoard); + const user = await this.authorizationService.getUserWithPermissions(userId); + const createdContextExternalTool: ContextExternalTool = + await this.mediaBoardService.createContextExternalToolForMediaBoard( + user.school.id, + schoolExternalTool, + mediaBoard + ); - const createdElement: AnyMediaContentElementDo = await this.mediaElementService.createExternalToolElement( - line, - position, - createdContexExternalTool - ); + const createdElement: MediaExternalToolElement = this.mediaBoardNodeFactory.buildExternalToolElement({ + contextExternalToolId: createdContextExternalTool.id, + }); + await this.boardNodeService.addToParent(line, createdElement, position); return createdElement; } + public async deleteElement(userId: EntityId, elementId: EntityId): Promise { + this.checkFeatureEnabled(); + // TODO in case you have more than one element, implement and use findContentElementById in media-board.service.ts + const element = await this.boardNodeService.findByClassAndId(MediaExternalToolElement, elementId); + + await this.boardNodePermissionService.checkPermission(userId, element, Action.write); + + await this.boardNodeService.delete(element); + } + + private checkFeatureEnabled() { + if (!this.configService.get('FEATURE_MEDIA_SHELF_ENABLED')) { + throw new FeatureDisabledLoggableException('FEATURE_MEDIA_SHELF_ENABLED'); + } + } + private async checkElementExistsAlreadyOnBoardAndThrow( mediaBoard: MediaBoard, schoolExternalTool: SchoolExternalTool ): Promise { - const exists = await this.mediaElementService.checkElementExists(mediaBoard, schoolExternalTool); + const exists = await this.mediaBoardService.checkElementExists(mediaBoard, schoolExternalTool); if (exists) { throw new MediaBoardElementAlreadyExistsLoggableException(mediaBoard.id, schoolExternalTool.id); } } - - public async deleteElement(userId: EntityId, elementId: EntityId): Promise { - this.checkFeatureEnabled(); - - const element: AnyMediaContentElementDo = await this.mediaElementService.findById(elementId); - - const user: UserEntity = await this.authorizationService.getUserWithPermissions(userId); - const boardDoAuthorizable: BoardDoAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable( - element - ); - this.authorizationService.checkPermission(user, boardDoAuthorizable, AuthorizationContextBuilder.write([])); - - await this.mediaElementService.delete(element); - } } diff --git a/apps/server/src/modules/board/uc/media-board/media-line.uc.spec.ts b/apps/server/src/modules/board/uc/media-board/media-line.uc.spec.ts index a6fc0a6bf9c..96732a1e4dd 100644 --- a/apps/server/src/modules/board/uc/media-board/media-line.uc.spec.ts +++ b/apps/server/src/modules/board/uc/media-board/media-line.uc.spec.ts @@ -1,28 +1,23 @@ import { createMock, type DeepMocked } from '@golevelup/ts-jest'; -import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { Action } from '@modules/authorization'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { FeatureDisabledLoggableException } from '@shared/common/loggable-exception'; -import { - boardDoAuthorizableFactory, - mediaBoardFactory, - mediaLineFactory, - setupEntities, - userFactory as userEntityFactory, -} from '@shared/testing'; -import { MediaBoardColors } from '../../domain'; +import { setupEntities, userFactory as userEntityFactory } from '@shared/testing'; import type { MediaBoardConfig } from '../../media-board.config'; -import { BoardDoAuthorizableService, MediaBoardService, MediaLineService } from '../../service'; +import { BoardNodePermissionService, BoardNodeService, MediaBoardService } from '../../service'; +import { mediaBoardFactory, mediaLineFactory } from '../../testing'; import { MediaLineUc } from './media-line.uc'; +import { MediaBoard, MediaLine } from '../../domain'; +import { MediaBoardColors } from '../../domain/media-board/types'; describe(MediaLineUc.name, () => { let module: TestingModule; let uc: MediaLineUc; - let authorizationService: DeepMocked; let mediaBoardService: DeepMocked; - let mediaLineService: DeepMocked; - let boardDoAuthorizableService: DeepMocked; + let boardNodeService: DeepMocked; + let boardNodePermissionService: DeepMocked; let configService: DeepMocked>; beforeAll(async () => { @@ -31,21 +26,17 @@ describe(MediaLineUc.name, () => { module = await Test.createTestingModule({ providers: [ MediaLineUc, - { - provide: AuthorizationService, - useValue: createMock(), - }, { provide: MediaBoardService, useValue: createMock(), }, { - provide: MediaLineService, - useValue: createMock(), + provide: BoardNodeService, + useValue: createMock(), }, { - provide: BoardDoAuthorizableService, - useValue: createMock(), + provide: BoardNodePermissionService, + useValue: createMock(), }, { provide: ConfigService, @@ -55,10 +46,9 @@ describe(MediaLineUc.name, () => { }).compile(); uc = module.get(MediaLineUc); - authorizationService = module.get(AuthorizationService); mediaBoardService = module.get(MediaBoardService); - mediaLineService = module.get(MediaLineService); - boardDoAuthorizableService = module.get(BoardDoAuthorizableService); + boardNodeService = module.get(BoardNodeService); + boardNodePermissionService = module.get(BoardNodePermissionService); configService = module.get(ConfigService); }); @@ -74,34 +64,34 @@ describe(MediaLineUc.name, () => { describe('when the user moves a media line', () => { const setup = () => { const user = userEntityFactory.build(); - const mediaBoard = mediaBoardFactory.build(); + const mediaBoard: MediaBoard = mediaBoardFactory.build(); const mediaLine = mediaLineFactory.build(); - const boardDoAuthorizable = boardDoAuthorizableFactory.build(); configService.get.mockReturnValueOnce(true); - boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce(boardDoAuthorizable); - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - mediaBoardService.findById.mockResolvedValueOnce(mediaBoard); - mediaLineService.findById.mockResolvedValueOnce(mediaLine); + + boardNodeService.findByClassAndId.mockResolvedValueOnce(mediaLine).mockResolvedValueOnce(mediaBoard); return { user, mediaBoard, mediaLine, - boardDoAuthorizable, }; }; + it('should call boardNodeService to find line, target board', async () => { + const { user, mediaLine, mediaBoard } = setup(); + + await uc.moveLine(user.id, mediaLine.id, mediaBoard.id, 1); + + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(MediaLine, mediaLine.id); + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(MediaBoard, mediaBoard.id); + }); it('should check the authorization', async () => { - const { user, mediaLine, mediaBoard, boardDoAuthorizable } = setup(); + const { user, mediaLine, mediaBoard } = setup(); await uc.moveLine(user.id, mediaLine.id, mediaBoard.id, 1); - expect(authorizationService.checkPermission).toHaveBeenCalledWith( - user, - boardDoAuthorizable, - AuthorizationContextBuilder.write([]) - ); + expect(boardNodePermissionService.checkPermission).toHaveBeenCalledWith(user.id, mediaBoard, Action.write); }); it('should move the line', async () => { @@ -109,7 +99,7 @@ describe(MediaLineUc.name, () => { await uc.moveLine(user.id, mediaLine.id, mediaBoard.id, 1); - expect(mediaLineService.move).toHaveBeenCalledWith(mediaLine, mediaBoard, 1); + expect(boardNodeService.move).toHaveBeenCalledWith(mediaLine, mediaBoard, 1); }); }); @@ -143,30 +133,23 @@ describe(MediaLineUc.name, () => { const setup = () => { const user = userEntityFactory.build(); const mediaLine = mediaLineFactory.build(); - const boardDoAuthorizable = boardDoAuthorizableFactory.build(); configService.get.mockReturnValueOnce(true); - boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce(boardDoAuthorizable); - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - mediaLineService.findById.mockResolvedValueOnce(mediaLine); + + boardNodeService.findByClassAndId.mockResolvedValueOnce(mediaLine); return { user, mediaLine, - boardDoAuthorizable, }; }; it('should check the authorization', async () => { - const { user, mediaLine, boardDoAuthorizable } = setup(); + const { user, mediaLine } = setup(); await uc.updateLineTitle(user.id, mediaLine.id, 'newTitle'); - expect(authorizationService.checkPermission).toHaveBeenCalledWith( - user, - boardDoAuthorizable, - AuthorizationContextBuilder.write([]) - ); + expect(boardNodePermissionService.checkPermission).toHaveBeenCalledWith(user.id, mediaLine, Action.write); }); it('should rename the line', async () => { @@ -174,7 +157,7 @@ describe(MediaLineUc.name, () => { await uc.updateLineTitle(user.id, mediaLine.id, 'newTitle'); - expect(mediaLineService.updateTitle).toHaveBeenCalledWith(mediaLine, 'newTitle'); + expect(boardNodeService.updateTitle).toHaveBeenCalledWith(mediaLine, 'newTitle'); }); }); @@ -206,30 +189,19 @@ describe(MediaLineUc.name, () => { const setup = () => { const user = userEntityFactory.build(); const mediaLine = mediaLineFactory.build(); - const boardDoAuthorizable = boardDoAuthorizableFactory.build(); configService.get.mockReturnValueOnce(true); - boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce(boardDoAuthorizable); - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - mediaLineService.findById.mockResolvedValueOnce(mediaLine); + boardNodeService.findByClassAndId.mockResolvedValueOnce(mediaLine); - return { - user, - mediaLine, - boardDoAuthorizable, - }; + return { user, mediaLine }; }; it('should check the authorization', async () => { - const { user, mediaLine, boardDoAuthorizable } = setup(); + const { user, mediaLine } = setup(); await uc.deleteLine(user.id, mediaLine.id); - expect(authorizationService.checkPermission).toHaveBeenCalledWith( - user, - boardDoAuthorizable, - AuthorizationContextBuilder.write([]) - ); + expect(boardNodePermissionService.checkPermission).toHaveBeenCalledWith(user.id, mediaLine, Action.write); }); it('should delete the line', async () => { @@ -237,7 +209,7 @@ describe(MediaLineUc.name, () => { await uc.deleteLine(user.id, mediaLine.id); - expect(mediaLineService.delete).toHaveBeenCalledWith(mediaLine); + expect(boardNodeService.delete).toHaveBeenCalledWith(mediaLine); }); }); @@ -267,30 +239,23 @@ describe(MediaLineUc.name, () => { const setup = () => { const user = userEntityFactory.build(); const mediaLine = mediaLineFactory.build(); - const boardDoAuthorizable = boardDoAuthorizableFactory.build(); configService.get.mockReturnValueOnce(true); - boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce(boardDoAuthorizable); - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - mediaLineService.findById.mockResolvedValueOnce(mediaLine); + + boardNodeService.findByClassAndId.mockResolvedValueOnce(mediaLine); return { user, mediaLine, - boardDoAuthorizable, }; }; it('should check the authorization', async () => { - const { user, mediaLine, boardDoAuthorizable } = setup(); + const { user, mediaLine } = setup(); await uc.updateLineColor(user.id, mediaLine.id, MediaBoardColors.BLUE); - expect(authorizationService.checkPermission).toHaveBeenCalledWith( - user, - boardDoAuthorizable, - AuthorizationContextBuilder.write([]) - ); + expect(boardNodePermissionService.checkPermission).toHaveBeenCalledWith(user.id, mediaLine, Action.write); }); it('should set background color', async () => { @@ -298,7 +263,7 @@ describe(MediaLineUc.name, () => { await uc.updateLineColor(user.id, mediaLine.id, MediaBoardColors.BLUE); - expect(mediaLineService.updateColor).toHaveBeenCalledWith(mediaLine, 'blue'); + expect(mediaBoardService.updateBackgroundColor).toHaveBeenCalledWith(mediaLine, 'blue'); }); }); @@ -330,30 +295,23 @@ describe(MediaLineUc.name, () => { const setup = () => { const user = userEntityFactory.build(); const mediaLine = mediaLineFactory.build(); - const boardDoAuthorizable = boardDoAuthorizableFactory.build(); configService.get.mockReturnValueOnce(true); - boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce(boardDoAuthorizable); - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - mediaLineService.findById.mockResolvedValueOnce(mediaLine); + + boardNodeService.findByClassAndId.mockResolvedValueOnce(mediaLine); return { user, mediaLine, - boardDoAuthorizable, }; }; it('should check the authorization', async () => { - const { user, mediaLine, boardDoAuthorizable } = setup(); + const { user, mediaLine } = setup(); await uc.collapseLine(user.id, mediaLine.id, true); - expect(authorizationService.checkPermission).toHaveBeenCalledWith( - user, - boardDoAuthorizable, - AuthorizationContextBuilder.write([]) - ); + expect(boardNodePermissionService.checkPermission).toHaveBeenCalledWith(user.id, mediaLine, Action.write); }); it('should collapse the line', async () => { @@ -361,7 +319,7 @@ describe(MediaLineUc.name, () => { await uc.collapseLine(user.id, mediaLine.id, true); - expect(mediaLineService.collapse).toHaveBeenCalledWith(mediaLine, true); + expect(mediaBoardService.updateCollapsed).toHaveBeenCalledWith(mediaLine, true); }); }); diff --git a/apps/server/src/modules/board/uc/media-board/media-line.uc.ts b/apps/server/src/modules/board/uc/media-board/media-line.uc.ts index 91671636b14..f12627fb987 100644 --- a/apps/server/src/modules/board/uc/media-board/media-line.uc.ts +++ b/apps/server/src/modules/board/uc/media-board/media-line.uc.ts @@ -1,22 +1,21 @@ -import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { Action } from '@modules/authorization'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { FeatureDisabledLoggableException } from '@shared/common/loggable-exception'; -import { BoardDoAuthorizable, type MediaBoard, type MediaLine } from '@shared/domain/domainobject'; -import type { User as UserEntity } from '@shared/domain/entity'; import type { EntityId } from '@shared/domain/types'; -import { MediaBoardColors } from '../../domain'; +import { MediaBoard, MediaLine } from '../../domain'; import type { MediaBoardConfig } from '../../media-board.config'; -import { BoardDoAuthorizableService, MediaBoardService, MediaLineService } from '../../service'; +import { BoardNodePermissionService, BoardNodeService } from '../../service'; +import { MediaBoardService } from '../../service/media-board'; +import { MediaBoardColors } from '../../domain/media-board/types'; @Injectable() export class MediaLineUc { constructor( - private readonly authorizationService: AuthorizationService, - private readonly mediaBoardService: MediaBoardService, - private readonly mediaLineService: MediaLineService, - private readonly boardDoAuthorizableService: BoardDoAuthorizableService, - private readonly configService: ConfigService + private readonly boardNodeService: BoardNodeService, + private readonly boardNodePermissionService: BoardNodePermissionService, + private readonly configService: ConfigService, + private readonly mediaBoardService: MediaBoardService ) {} public async moveLine( @@ -27,65 +26,52 @@ export class MediaLineUc { ): Promise { this.checkFeatureEnabled(); - const targetBoard: MediaBoard = await this.mediaBoardService.findById(targetBoardId); + const line = await this.boardNodeService.findByClassAndId(MediaLine, lineId); + const targetBoard = await this.boardNodeService.findByClassAndId(MediaBoard, targetBoardId); - const user: UserEntity = await this.authorizationService.getUserWithPermissions(userId); - const boardDoAuthorizable: BoardDoAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable( - targetBoard - ); - this.authorizationService.checkPermission(user, boardDoAuthorizable, AuthorizationContextBuilder.write([])); + await this.boardNodePermissionService.checkPermission(userId, targetBoard, Action.write); - const line: MediaLine = await this.mediaLineService.findById(lineId); - - await this.mediaLineService.move(line, targetBoard, targetPosition); + await this.boardNodeService.move(line, targetBoard, targetPosition); } public async updateLineTitle(userId: EntityId, lineId: EntityId, title: string): Promise { this.checkFeatureEnabled(); - const line: MediaLine = await this.mediaLineService.findById(lineId); + const line: MediaLine = await this.boardNodeService.findByClassAndId(MediaLine, lineId); - const user: UserEntity = await this.authorizationService.getUserWithPermissions(userId); - const boardDoAuthorizable: BoardDoAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable(line); - this.authorizationService.checkPermission(user, boardDoAuthorizable, AuthorizationContextBuilder.write([])); + await this.boardNodePermissionService.checkPermission(userId, line, Action.write); - await this.mediaLineService.updateTitle(line, title); + await this.boardNodeService.updateTitle(line, title); } public async updateLineColor(userId: EntityId, lineId: EntityId, color: MediaBoardColors): Promise { this.checkFeatureEnabled(); - const line: MediaLine = await this.mediaLineService.findById(lineId); + const line: MediaLine = await this.boardNodeService.findByClassAndId(MediaLine, lineId); - const user: UserEntity = await this.authorizationService.getUserWithPermissions(userId); - const boardDoAuthorizable: BoardDoAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable(line); - this.authorizationService.checkPermission(user, boardDoAuthorizable, AuthorizationContextBuilder.write([])); + await this.boardNodePermissionService.checkPermission(userId, line, Action.write); - await this.mediaLineService.updateColor(line, color); + await this.mediaBoardService.updateBackgroundColor(line, color); } public async collapseLine(userId: EntityId, lineId: EntityId, collapsed: boolean): Promise { this.checkFeatureEnabled(); - const line: MediaLine = await this.mediaLineService.findById(lineId); + const line: MediaLine = await this.boardNodeService.findByClassAndId(MediaLine, lineId); - const user: UserEntity = await this.authorizationService.getUserWithPermissions(userId); - const boardDoAuthorizable: BoardDoAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable(line); - this.authorizationService.checkPermission(user, boardDoAuthorizable, AuthorizationContextBuilder.write([])); + await this.boardNodePermissionService.checkPermission(userId, line, Action.write); - await this.mediaLineService.collapse(line, collapsed); + await this.mediaBoardService.updateCollapsed(line, collapsed); } public async deleteLine(userId: EntityId, lineId: EntityId): Promise { this.checkFeatureEnabled(); - const line: MediaLine = await this.mediaLineService.findById(lineId); + const line = await this.boardNodeService.findByClassAndId(MediaLine, lineId); - const user: UserEntity = await this.authorizationService.getUserWithPermissions(userId); - const boardDoAuthorizable: BoardDoAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable(line); - this.authorizationService.checkPermission(user, boardDoAuthorizable, AuthorizationContextBuilder.write([])); + await this.boardNodePermissionService.checkPermission(userId, line, Action.write); - await this.mediaLineService.delete(line); + await this.boardNodeService.delete(line); } private checkFeatureEnabled() { diff --git a/apps/server/src/modules/board/uc/submission-item.uc.spec.ts b/apps/server/src/modules/board/uc/submission-item.uc.spec.ts index 4fe5da487c5..a516784b6fe 100644 --- a/apps/server/src/modules/board/uc/submission-item.uc.spec.ts +++ b/apps/server/src/modules/board/uc/submission-item.uc.spec.ts @@ -1,67 +1,63 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { Action, AuthorizationService } from '@modules/authorization'; -import { - BadRequestException, - ForbiddenException, - NotFoundException, - UnprocessableEntityException, -} from '@nestjs/common'; +import { Action } from '@modules/authorization'; +import { BadRequestException, ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardDoAuthorizable, BoardRoles, ContentElementType } from '@shared/domain/domainobject'; +import { setupEntities, userFactory } from '@shared/testing'; +import { + BoardNodeAuthorizable, + BoardNodeFactory, + BoardRoles, + ContentElementType, + SubmissionContainerElement, + SubmissionItem, + UserWithBoardRoles, +} from '../domain'; +import { BoardNodeAuthorizableService, BoardNodePermissionService, BoardNodeService } from '../service'; import { columnBoardFactory, - fileElementFactory, + linkElementFactory, richTextElementFactory, - setupEntities, submissionContainerElementFactory, submissionItemFactory, - userFactory, -} from '@shared/testing'; -import { Logger } from '@src/core/logger'; -import { BoardDoAuthorizableService, ContentElementService, SubmissionItemService } from '../service'; +} from '../testing'; import { SubmissionItemUc } from './submission-item.uc'; describe(SubmissionItemUc.name, () => { let module: TestingModule; let uc: SubmissionItemUc; - let authorizationService: DeepMocked; - let boardDoAuthorizableService: DeepMocked; - let elementService: DeepMocked; - let submissionItemService: DeepMocked; + let boardNodeAuthorizableService: DeepMocked; + let boardPermissionService: DeepMocked; + let boardNodeService: DeepMocked; + let boardNodeFactory: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ providers: [ SubmissionItemUc, { - provide: AuthorizationService, - useValue: createMock(), + provide: BoardNodeAuthorizableService, + useValue: createMock(), }, { - provide: BoardDoAuthorizableService, - useValue: createMock(), + provide: BoardNodePermissionService, + useValue: createMock(), }, { - provide: ContentElementService, - useValue: createMock(), + provide: BoardNodeService, + useValue: createMock(), }, { - provide: Logger, - useValue: createMock(), - }, - { - provide: SubmissionItemService, - useValue: createMock(), + provide: BoardNodeFactory, + useValue: createMock(), }, ], }).compile(); uc = module.get(SubmissionItemUc); - authorizationService = module.get(AuthorizationService); - authorizationService.checkPermission.mockImplementation(() => {}); - boardDoAuthorizableService = module.get(BoardDoAuthorizableService); - elementService = module.get(ContentElementService); - submissionItemService = module.get(SubmissionItemService); + boardNodeAuthorizableService = module.get(BoardNodeAuthorizableService); + boardPermissionService = module.get(BoardNodePermissionService); + boardNodeService = module.get(BoardNodeService); + boardNodeFactory = module.get(BoardNodeFactory); await setupEntities(); }); @@ -70,114 +66,125 @@ describe(SubmissionItemUc.name, () => { }); afterEach(() => { - jest.clearAllMocks(); + jest.resetAllMocks(); }); describe('findSubmissionItems', () => { - describe('user is student', () => { + describe('when finding submission items', () => { const setup = () => { - const user1 = userFactory.buildWithId(); - const user2 = userFactory.buildWithId(); - const submissionItem1 = submissionItemFactory.build({ - userId: user1.id, - }); - const submissionItem2 = submissionItemFactory.build({ - userId: user2.id, + const user = userFactory.buildWithId(); + const submissionItem = submissionItemFactory.build({ + userId: user.id, }); + const submissionContainerEl = submissionContainerElementFactory.build({ - children: [submissionItem1, submissionItem2], + children: [submissionItem], }); + boardNodeService.findByClassAndId.mockResolvedValueOnce(submissionContainerEl); - boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValue( - new BoardDoAuthorizable({ - users: [ - { userId: user1.id, roles: [BoardRoles.READER] }, - { userId: user2.id, roles: [BoardRoles.READER] }, - ], + boardNodeAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce( + new BoardNodeAuthorizable({ + users: [{ userId: user.id, roles: [BoardRoles.READER] }], id: submissionContainerEl.id, - boardDo: submissionContainerEl, - rootDo: columnBoardFactory.build(), + boardNode: submissionContainerEl, + rootNode: columnBoardFactory.build(), }) ); - const elementSpy = elementService.findById.mockResolvedValueOnce(submissionContainerEl); - - return { submissionContainerEl, submissionItem1, user1, elementSpy }; + return { submissionContainerEl, submissionItem, user }; }; - it('student1 should only get their own submission item', async () => { - const { user1, submissionContainerEl, submissionItem1 } = setup(); - const { submissionItems } = await uc.findSubmissionItems(user1.id, submissionContainerEl.id); - expect(submissionItems.length).toBe(1); - expect(submissionItems[0]).toStrictEqual(submissionItem1); + it('should call service to find the submission container ', async () => { + const { submissionContainerEl, user } = setup(); + + await uc.findSubmissionItems(user.id, submissionContainerEl.id); + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith( + SubmissionContainerElement, + submissionContainerEl.id + ); }); - it('student should not get a list of users', async () => { - const { user1, submissionContainerEl } = setup(); - const { users } = await uc.findSubmissionItems(user1.id, submissionContainerEl.id); - expect(users.length).toBe(0); + + it('should call Board Permission service to check user permission', async () => { + const { submissionContainerEl, user } = setup(); + + await uc.findSubmissionItems(user.id, submissionContainerEl.id); + expect(boardPermissionService.checkPermission).toBeCalledWith(user.id, submissionContainerEl, Action.read); }); }); - describe('when user is a teacher', () => { + + describe('when submission container contains several submission items', () => { const setup = () => { - const teacher = userFactory.buildWithId(); - const student1 = userFactory.buildWithId(); - const student2 = userFactory.buildWithId(); + const boardReaderUser1 = userFactory.buildWithId(); + const boardReaderUser2 = userFactory.buildWithId(); + const boardEditorUser = userFactory.buildWithId(); + const submissionItem1 = submissionItemFactory.build({ - userId: student1.id, + userId: boardReaderUser1.id, }); const submissionItem2 = submissionItemFactory.build({ - userId: student2.id, + userId: boardReaderUser2.id, }); const submissionContainerEl = submissionContainerElementFactory.build({ children: [submissionItem1, submissionItem2], }); - boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValue( - new BoardDoAuthorizable({ - users: [ - { userId: teacher.id, roles: [BoardRoles.EDITOR] }, - { userId: student1.id, roles: [BoardRoles.READER] }, - { userId: student2.id, roles: [BoardRoles.READER] }, - ], + const users: UserWithBoardRoles[] = [ + { userId: boardEditorUser.id, roles: [BoardRoles.EDITOR] }, + { userId: boardReaderUser1.id, roles: [BoardRoles.READER] }, + { userId: boardReaderUser2.id, roles: [BoardRoles.READER] }, + ]; + + boardNodeAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce( + new BoardNodeAuthorizable({ + users, id: submissionContainerEl.id, - boardDo: submissionContainerEl, - rootDo: columnBoardFactory.build(), + boardNode: submissionContainerEl, + rootNode: columnBoardFactory.build(), }) ); - const elementSpy = elementService.findById.mockResolvedValue(submissionContainerEl); - - return { submissionContainerEl, submissionItem1, submissionItem2, teacher, elementSpy }; + const elementSpy = boardNodeService.findByClassAndId.mockResolvedValueOnce(submissionContainerEl); + return { + submissionContainerEl, + submissionItem1, + submissionItem2, + boardReaderUser1, + boardReaderUser2, + boardEditorUser, + elementSpy, + }; }; - it('teacher should get all submission items', async () => { - const { teacher, submissionContainerEl, submissionItem1, submissionItem2 } = setup(); - const { submissionItems } = await uc.findSubmissionItems(teacher.id, submissionContainerEl.id); + it('board editor should get all submission items', async () => { + const { boardEditorUser, submissionContainerEl, submissionItem1, submissionItem2 } = setup(); + boardPermissionService.isUserBoardReader.mockReturnValueOnce(false); + const { submissionItems } = await uc.findSubmissionItems(boardEditorUser.id, submissionContainerEl.id); expect(submissionItems.length).toBe(2); expect(submissionItems.map((item) => item.id)).toContain(submissionItem1.id); expect(submissionItems.map((item) => item.id)).toContain(submissionItem2.id); }); - it('teacher should get list of students', async () => { - const { teacher, submissionContainerEl } = setup(); - const { users } = await uc.findSubmissionItems(teacher.id, submissionContainerEl.id); + + it('board editor gets list of students', async () => { + const { boardEditorUser, submissionContainerEl } = setup(); + boardPermissionService.isUserBoardReader.mockReturnValueOnce(false); + const { users } = await uc.findSubmissionItems(boardEditorUser.id, submissionContainerEl.id); expect(users.length).toBe(2); }); - }); - describe('when called with wrong board node', () => { - const setup = () => { - const teacher = userFactory.buildWithId(); - const fileEl = fileElementFactory.build(); - elementService.findById.mockResolvedValue(fileEl); - return { teacher, fileEl }; - }; + it('board reader only get his own submission item', async () => { + const { boardReaderUser1, submissionContainerEl, submissionItem1 } = setup(); + boardPermissionService.isUserBoardReader.mockReturnValueOnce(true); - it('should throw HttpException', async () => { - const { teacher, fileEl } = setup(); + const { submissionItems } = await uc.findSubmissionItems(boardReaderUser1.id, submissionContainerEl.id); + expect(submissionItems.length).toBe(1); + expect(submissionItems[0]).toStrictEqual(submissionItem1); + }); - await expect(uc.findSubmissionItems(teacher.id, fileEl.id)).rejects.toThrow( - new NotFoundException('Could not find a submission container with this id') - ); + it('board reader not get a list of users', async () => { + const { boardReaderUser1, submissionContainerEl } = setup(); + boardPermissionService.isUserBoardReader.mockReturnValueOnce(true); + const { users } = await uc.findSubmissionItems(boardReaderUser1.id, submissionContainerEl.id); + expect(users.length).toBe(0); }); }); }); @@ -186,127 +193,217 @@ describe(SubmissionItemUc.name, () => { const setup = () => { const user = userFactory.buildWithId(); - const columnBoard = columnBoardFactory.build(); const submissionItem = submissionItemFactory.build({ userId: user.id, }); - submissionItemService.findById.mockResolvedValueOnce(submissionItem); + boardNodeService.findByClassAndId.mockResolvedValueOnce(submissionItem); - return { submissionItem, columnBoard, user, boardDoAuthorizableService }; + return { submissionItem, user, boardNodeAuthorizableService }; }; it('should call service to find the submission item ', async () => { const { submissionItem, user } = setup(); await uc.updateSubmissionItem(user.id, submissionItem.id, false); - expect(submissionItemService.findById).toHaveBeenCalledWith(submissionItem.id); + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(SubmissionItem, submissionItem.id); }); - it('should authorize', async () => { - const { submissionItem, user, columnBoard } = setup(); - - boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValue( - new BoardDoAuthorizable({ - users: [{ userId: user.id, roles: [BoardRoles.READER] }], - id: submissionItem.id, - boardDo: submissionItem, - rootDo: columnBoard, - }) - ); - const boardDoAuthorizable = await boardDoAuthorizableService.getBoardAuthorizable(submissionItem); + it('should call Board Permission service to check user permission', async () => { + const { submissionItem, user } = setup(); await uc.updateSubmissionItem(user.id, submissionItem.id, false); - const context = { action: Action.write, requiredPermissions: [] }; - expect(authorizationService.checkPermission).toBeCalledWith(user, boardDoAuthorizable, context); + + expect(boardPermissionService.checkPermission).toBeCalledWith(user.id, submissionItem, Action.write); }); + it('should call service to update submission item', async () => { const { submissionItem, user } = setup(); await uc.updateSubmissionItem(user.id, submissionItem.id, false); - expect(submissionItemService.update).toHaveBeenCalledWith(submissionItem, false); + expect(boardNodeService.updateCompleted).toHaveBeenCalledWith(submissionItem, false); + }); + + it('should return updated submission item', async () => { + const { submissionItem, user } = setup(); + const updatedSubmissionItem = await uc.updateSubmissionItem(user.id, submissionItem.id, false); + expect(updatedSubmissionItem).toEqual(submissionItem); + }); + }); + + describe('deleteSubmissionItem', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const submissionItem = submissionItemFactory.build({ + userId: user.id, + }); + + boardNodeService.findByClassAndId.mockResolvedValueOnce(submissionItem); + + return { submissionItem, user }; + }; + + it('should call service to find the submission item ', async () => { + const { submissionItem, user } = setup(); + await uc.deleteSubmissionItem(user.id, submissionItem.id); + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(SubmissionItem, submissionItem.id); + }); + + it('should call Board Permission service to check user permission', async () => { + const { submissionItem, user } = setup(); + + await uc.deleteSubmissionItem(user.id, submissionItem.id); + + expect(boardPermissionService.checkPermission).toBeCalledWith(user.id, submissionItem, Action.write); + }); + + it('should call service to delete submission item', async () => { + const { submissionItem, user } = setup(); + await uc.deleteSubmissionItem(user.id, submissionItem.id); + expect(boardNodeService.delete).toHaveBeenCalledWith(submissionItem); }); }); describe('createElement', () => { - describe('when the user is a student', () => { + describe('when the user is board reader', () => { const setup = () => { const user = userFactory.buildWithId(); const submissionItem = submissionItemFactory.build({ userId: user.id, }); - submissionItemService.findById.mockResolvedValue(submissionItem); - const element = richTextElementFactory.build(); - boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValue( - new BoardDoAuthorizable({ - users: [{ userId: user.id, roles: [BoardRoles.READER] }], + boardNodeService.findByClassAndId.mockResolvedValueOnce(submissionItem); + + const users = [{ userId: user.id, roles: [BoardRoles.READER] }]; + boardNodeAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce( + new BoardNodeAuthorizable({ + users, id: submissionItem.id, - boardDo: submissionItem, - rootDo: columnBoardFactory.build(), + boardNode: submissionItem, + rootNode: columnBoardFactory.build(), }) ); - return { element, submissionItem, user }; + boardPermissionService.isUserBoardReader.mockReturnValueOnce(true); + boardNodeFactory.buildContentElement.mockReturnValueOnce(element); + + return { element, submissionItem, user, users }; }; - it('should call service to find the submission item ', async () => { - const { element, submissionItem, user } = setup(); - elementService.create.mockResolvedValueOnce(element); + it('should throw if type is not file or rich text', async () => { + const { submissionItem, user } = setup(); - await uc.createElement(user.id, submissionItem.id, ContentElementType.RICH_TEXT); - expect(submissionItemService.findById).toHaveBeenCalledWith(submissionItem.id); + await expect(uc.createElement(user.id, submissionItem.id, ContentElementType.LINK)).rejects.toThrow( + BadRequestException + ); }); - it('should authorize', async () => { - const { element, submissionItem, user } = setup(); - elementService.create.mockResolvedValueOnce(element); - - const boardDoAuthorizable = await boardDoAuthorizableService.getBoardAuthorizable(submissionItem); + it('should call service to find the submission item ', async () => { + const { submissionItem, user } = setup(); await uc.createElement(user.id, submissionItem.id, ContentElementType.RICH_TEXT); - const context = { action: Action.write, requiredPermissions: [] }; - expect(authorizationService.checkPermission).toBeCalledWith(user, boardDoAuthorizable, context); + + expect(boardNodeService.findByClassAndId).toHaveBeenCalledWith(SubmissionItem, submissionItem.id); }); - it('should throw if user is not creator of submission item', async () => { - const user2 = userFactory.buildWithId(); - const { submissionItem } = setup(); + it('should call Board Permission service to check user us a board reader', async () => { + const { submissionItem, user, users } = setup(); - await expect(uc.createElement(user2.id, submissionItem.id, ContentElementType.RICH_TEXT)).rejects.toThrow( - new ForbiddenException() - ); + await uc.createElement(user.id, submissionItem.id, ContentElementType.RICH_TEXT); + + expect(boardPermissionService.isUserBoardReader).toBeCalledWith(user.id, users); }); - it('should throw if type is not file or rich text', async () => { + it('should call Board Permission service to check user write permission', async () => { const { submissionItem, user } = setup(); - await expect(uc.createElement(user.id, submissionItem.id, ContentElementType.LINK)).rejects.toThrow( - new BadRequestException() - ); + + await uc.createElement(user.id, submissionItem.id, ContentElementType.RICH_TEXT); + + expect(boardPermissionService.checkPermission).toBeCalledWith(user.id, submissionItem, Action.write); }); - it('should call service to create element', async () => { - const { element, submissionItem, user } = setup(); - elementService.create.mockResolvedValueOnce(element); + it('should call factory to build content element', async () => { + const { submissionItem, user } = setup(); await uc.createElement(user.id, submissionItem.id, ContentElementType.RICH_TEXT); - expect(elementService.create).toHaveBeenCalledWith(submissionItem, ContentElementType.RICH_TEXT); + + expect(boardNodeFactory.buildContentElement).toHaveBeenCalledWith(ContentElementType.RICH_TEXT); }); + it('should add element to submission item', async () => {}); + it('should return element', async () => { const { element, submissionItem, user } = setup(); - elementService.create.mockResolvedValueOnce(element); const returnedElement = await uc.createElement(user.id, submissionItem.id, ContentElementType.RICH_TEXT); expect(returnedElement).toEqual(element); }); + }); - it('should throw if element is not file or rich text', async () => { + describe('when the factory ', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const submissionItem = submissionItemFactory.build({ + userId: user.id, + }); + + boardNodeService.findByClassAndId.mockResolvedValueOnce(submissionItem); + boardPermissionService.isUserBoardReader.mockReturnValueOnce(false); + + const otherElement = linkElementFactory.build(); + boardNodeFactory.buildContentElement.mockReturnValueOnce(otherElement); + const users = [{ userId: user.id, roles: [BoardRoles.READER] }]; + boardNodeAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce( + new BoardNodeAuthorizable({ + users, + id: submissionItem.id, + boardNode: submissionItem, + rootNode: columnBoardFactory.build(), + }) + ); + + return { submissionItem, user }; + }; + + it('should throw if returned element is not file or rich text', async () => { + const { submissionItem, user } = setup(); + + await expect(uc.createElement(user.id, submissionItem.id, ContentElementType.EXTERNAL_TOOL)).rejects.toThrow( + BadRequestException + ); + }); + }); + + describe('when the user is not board reader', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const submissionItem = submissionItemFactory.build({ + userId: user.id, + }); + const element = richTextElementFactory.build(); + + boardNodeService.findByClassAndId.mockResolvedValueOnce(submissionItem); + boardPermissionService.isUserBoardReader.mockReturnValueOnce(false); + + boardNodeFactory.buildContentElement.mockReturnValueOnce(element); + const users = [{ userId: user.id, roles: [BoardRoles.READER] }]; + boardNodeAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce( + new BoardNodeAuthorizable({ + users, + id: submissionItem.id, + boardNode: submissionItem, + rootNode: columnBoardFactory.build(), + }) + ); + + return { element, submissionItem, user }; + }; + + it('should throw if boardPermissionService.isUserBoardReader returns false', async () => { const { submissionItem, user } = setup(); - const otherElement = submissionContainerElementFactory.build(); - elementService.create.mockResolvedValueOnce(otherElement); + await expect(uc.createElement(user.id, submissionItem.id, ContentElementType.RICH_TEXT)).rejects.toThrow( - new UnprocessableEntityException() + new ForbiddenException() ); }); }); diff --git a/apps/server/src/modules/board/uc/submission-item.uc.ts b/apps/server/src/modules/board/uc/submission-item.uc.ts index 44f0530a243..8a536bc03a5 100644 --- a/apps/server/src/modules/board/uc/submission-item.uc.ts +++ b/apps/server/src/modules/board/uc/submission-item.uc.ts @@ -1,61 +1,52 @@ -import { Action, AuthorizationService } from '@modules/authorization'; -import { - BadRequestException, - ForbiddenException, - forwardRef, - Inject, - Injectable, - NotFoundException, - UnprocessableEntityException, -} from '@nestjs/common'; +import { Action } from '@modules/authorization'; +import { BadRequestException, ForbiddenException, Injectable, UnprocessableEntityException } from '@nestjs/common'; +import { EntityId } from '@shared/domain/types'; import { + BoardNodeFactory, BoardRoles, ContentElementType, FileElement, isFileElement, isRichTextElement, - isSubmissionContainerElement, isSubmissionItem, RichTextElement, + SubmissionContainerElement, SubmissionItem, UserWithBoardRoles, -} from '@shared/domain/domainobject'; -import { EntityId } from '@shared/domain/types'; -import { BoardDoAuthorizableService, ContentElementService, SubmissionItemService } from '../service'; -import { BaseUc } from './base.uc'; +} from '../domain'; +import { BoardNodeAuthorizableService, BoardNodePermissionService, BoardNodeService } from '../service'; @Injectable() -export class SubmissionItemUc extends BaseUc { +export class SubmissionItemUc { constructor( - @Inject(forwardRef(() => AuthorizationService)) - protected readonly authorizationService: AuthorizationService, - protected readonly boardDoAuthorizableService: BoardDoAuthorizableService, - protected readonly elementService: ContentElementService, - protected readonly submissionItemService: SubmissionItemService - ) { - super(authorizationService, boardDoAuthorizableService); - } + private readonly boardNodeAuthorizableService: BoardNodeAuthorizableService, + private readonly boardNodeService: BoardNodeService, + private readonly boardPermissionService: BoardNodePermissionService, + private readonly boardNodeFactory: BoardNodeFactory + ) {} async findSubmissionItems( userId: EntityId, submissionContainerId: EntityId ): Promise<{ submissionItems: SubmissionItem[]; users: UserWithBoardRoles[] }> { - const submissionContainerElement = await this.elementService.findById(submissionContainerId); - if (!isSubmissionContainerElement(submissionContainerElement)) { - throw new NotFoundException('Could not find a submission container with this id'); - } + const submissionContainerElement = await this.boardNodeService.findByClassAndId( + SubmissionContainerElement, + submissionContainerId + ); - await this.checkPermission(userId, submissionContainerElement, Action.read); + await this.boardPermissionService.checkPermission(userId, submissionContainerElement, Action.read); let submissionItems = submissionContainerElement.children.filter(isSubmissionItem); - const boardDoAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable(submissionContainerElement); + const boardDoAuthorizable = await this.boardNodeAuthorizableService.getBoardAuthorizable( + submissionContainerElement + ); // only board readers can create submission items let users = boardDoAuthorizable.users.filter((user) => user.roles.includes(BoardRoles.READER)); // board readers can only see their own submission item - if (this.isUserBoardReader(userId, boardDoAuthorizable.users)) { + if (this.boardPermissionService.isUserBoardReader(userId, boardDoAuthorizable.users)) { submissionItems = submissionItems.filter((item) => item.userId === userId); users = []; } @@ -68,20 +59,20 @@ export class SubmissionItemUc extends BaseUc { submissionItemId: EntityId, completed: boolean ): Promise { - const submissionItem = await this.submissionItemService.findById(submissionItemId); + const submissionItem = await this.boardNodeService.findByClassAndId(SubmissionItem, submissionItemId); - await this.checkPermission(userId, submissionItem, Action.write); + await this.boardPermissionService.checkPermission(userId, submissionItem, Action.write); - await this.submissionItemService.update(submissionItem, completed); + await this.boardNodeService.updateCompleted(submissionItem, completed); return submissionItem; } async deleteSubmissionItem(userId: EntityId, submissionItemId: EntityId): Promise { - const submissionItem = await this.submissionItemService.findById(submissionItemId); - await this.checkPermission(userId, submissionItem, Action.write); + const submissionItem = await this.boardNodeService.findByClassAndId(SubmissionItem, submissionItemId); + await this.boardPermissionService.checkPermission(userId, submissionItem, Action.write); - await this.submissionItemService.delete(submissionItem); + await this.boardNodeService.delete(submissionItem); } async createElement( @@ -93,22 +84,24 @@ export class SubmissionItemUc extends BaseUc { throw new BadRequestException(); } - const submissionItem = await this.submissionItemService.findById(submissionItemId); + const submissionItem = await this.boardNodeService.findByClassAndId(SubmissionItem, submissionItemId); - const boardDoAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable(submissionItem); + const boardDoAuthorizable = await this.boardNodeAuthorizableService.getBoardAuthorizable(submissionItem); - if (!this.isUserBoardReader(userId, boardDoAuthorizable.users)) { + if (!this.boardPermissionService.isUserBoardReader(userId, boardDoAuthorizable.users)) { throw new ForbiddenException(); } - await this.checkPermission(userId, submissionItem, Action.write); - - const element = await this.elementService.create(submissionItem, type); + await this.boardPermissionService.checkPermission(userId, submissionItem, Action.write); - if (!isFileElement(element) && !isRichTextElement(element)) { + const element = this.boardNodeFactory.buildContentElement(type); + // TODO this is already taken care in add to parent, but TS complains without this type guard + if (!(isFileElement(element) || isRichTextElement(element))) { throw new UnprocessableEntityException(); } + await this.boardNodeService.addToParent(submissionItem, element); + return element; } } diff --git a/apps/server/src/modules/collaborative-text-editor/api/collaborative-text-editor.uc.ts b/apps/server/src/modules/collaborative-text-editor/api/collaborative-text-editor.uc.ts index 9d0f4d0d1bb..458959c3ec6 100644 --- a/apps/server/src/modules/collaborative-text-editor/api/collaborative-text-editor.uc.ts +++ b/apps/server/src/modules/collaborative-text-editor/api/collaborative-text-editor.uc.ts @@ -2,8 +2,8 @@ import { Injectable } from '@nestjs/common'; import { User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; -import { AuthorizationContextBuilder, AuthorizationService } from '@src/modules/authorization'; -import { BoardDoAuthorizableService, ContentElementService } from '@src/modules/board'; +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { BoardNodeAuthorizableService, BoardNodeService } from '@modules/board'; import { CollaborativeTextEditor } from '../domain/do/collaborative-text-editor'; import { CollaborativeTextEditorService } from '../service/collaborative-text-editor.service'; import { @@ -16,8 +16,8 @@ export class CollaborativeTextEditorUc { constructor( private readonly authorizationService: AuthorizationService, private readonly collaborativeTextEditorService: CollaborativeTextEditorService, - private readonly contentElementService: ContentElementService, - private readonly boardDoAuthorizableService: BoardDoAuthorizableService + private readonly boardNodeService: BoardNodeService, + private readonly boardNodeAuthorizableService: BoardNodeAuthorizableService ) {} async getOrCreateCollaborativeTextEditorForParent( @@ -50,8 +50,8 @@ export class CollaborativeTextEditorUc { } private async authorizeForContentElement(params: GetCollaborativeTextEditorForParentParams, user: User) { - const contentElement = await this.contentElementService.findById(params.parentId); - const contentElementDoAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable(contentElement); + const contentElement = await this.boardNodeService.findContentElementById(params.parentId); + const contentElementDoAuthorizable = await this.boardNodeAuthorizableService.getBoardAuthorizable(contentElement); this.authorizationService.checkPermission( user, diff --git a/apps/server/src/modules/collaborative-text-editor/api/tests/get-collaborative-text-editor.api.spec.ts b/apps/server/src/modules/collaborative-text-editor/api/tests/get-collaborative-text-editor.api.spec.ts index 4940c661c4e..999c0217d6e 100644 --- a/apps/server/src/modules/collaborative-text-editor/api/tests/get-collaborative-text-editor.api.spec.ts +++ b/apps/server/src/modules/collaborative-text-editor/api/tests/get-collaborative-text-editor.api.spec.ts @@ -3,18 +3,15 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; -import { - cardNodeFactory, - cleanupCollections, - collaborativeTextEditorNodeFactory, - columnBoardNodeFactory, - columnNodeFactory, - courseFactory, - TestApiClient, - UserAndAccountTestFactory, -} from '@shared/testing'; +import { cleanupCollections, courseFactory, TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; import { EtherpadClientAdapter } from '@src/infra/etherpad-client'; +import { BoardExternalReferenceType } from '@src/modules/board'; +import { + cardEntityFactory, + collaborativeTextEditorEntityFactory, + columnBoardEntityFactory, + columnEntityFactory, +} from '@src/modules/board/testing'; import { ServerTestModule } from '@src/modules/server'; describe('Collaborative Text Editor Controller (API)', () => { @@ -110,22 +107,16 @@ describe('Collaborative Text Editor Controller (API)', () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); const course = courseFactory.build({ students: [studentUser] }); - await em.persistAndFlush([studentUser, course]); + await em.persistAndFlush([studentAccount, studentUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.buildWithId({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); - const collaborativeTextEditorElement = collaborativeTextEditorNodeFactory.build({ parent: cardNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); + const cardNode = cardEntityFactory.withParent(columnNode).build(); + const collaborativeTextEditorElement = collaborativeTextEditorEntityFactory.withParent(cardNode).build(); - await em.persistAndFlush([ - studentAccount, - collaborativeTextEditorElement, - columnBoardNode, - columnNode, - cardNode, - ]); + await em.persistAndFlush([collaborativeTextEditorElement, columnBoardNode, columnNode, cardNode]); em.clear(); const loggedInClient = await testApiClient.login(studentAccount); @@ -183,12 +174,12 @@ describe('Collaborative Text Editor Controller (API)', () => { await em.persistAndFlush([studentUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); - const collaborativeTextEditorElement = collaborativeTextEditorNodeFactory.build({ parent: cardNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); + const cardNode = cardEntityFactory.withParent(columnNode).build(); + const collaborativeTextEditorElement = collaborativeTextEditorEntityFactory.withParent(cardNode).build(); await em.persistAndFlush([ studentAccount, @@ -254,12 +245,12 @@ describe('Collaborative Text Editor Controller (API)', () => { await em.persistAndFlush([studentUser, course]); - const columnBoardNode = columnBoardNodeFactory.buildWithId({ + const columnBoardNode = columnBoardEntityFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); - const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); - const cardNode = cardNodeFactory.buildWithId({ parent: columnNode }); - const collaborativeTextEditorElement = collaborativeTextEditorNodeFactory.build({ parent: cardNode }); + const columnNode = columnEntityFactory.withParent(columnBoardNode).build(); + const cardNode = cardEntityFactory.withParent(columnNode).build(); + const collaborativeTextEditorElement = collaborativeTextEditorEntityFactory.withParent(cardNode).build(); await em.persistAndFlush([ studentAccount, diff --git a/apps/server/src/modules/deletion/repo/scope/deletion-request-scope.ts b/apps/server/src/modules/deletion/repo/scope/deletion-request-scope.ts index dd6420fde10..6e602f0690c 100644 --- a/apps/server/src/modules/deletion/repo/scope/deletion-request-scope.ts +++ b/apps/server/src/modules/deletion/repo/scope/deletion-request-scope.ts @@ -1,4 +1,4 @@ -import { Scope } from '@shared/repo'; +import { Scope } from '@shared/repo/scope'; import { DeletionRequestEntity } from '../entity'; import { StatusModel } from '../../domain/types'; diff --git a/apps/server/src/modules/group/repo/group.scope.ts b/apps/server/src/modules/group/repo/group.scope.ts index 12e3ba735e7..54c262cdaba 100644 --- a/apps/server/src/modules/group/repo/group.scope.ts +++ b/apps/server/src/modules/group/repo/group.scope.ts @@ -1,6 +1,7 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { EntityId } from '@shared/domain/types'; -import { MongoPatterns, Scope } from '@shared/repo'; +import { MongoPatterns } from '@shared/repo'; +import { Scope } from '@shared/repo/scope'; import { GroupEntity, GroupEntityTypes } from '../entity'; export class GroupScope extends Scope { diff --git a/apps/server/src/modules/learnroom/controller/dto/single-column-board/board-column-board.response.ts b/apps/server/src/modules/learnroom/controller/dto/single-column-board/board-column-board.response.ts index 9975470646d..a27ad9cc356 100644 --- a/apps/server/src/modules/learnroom/controller/dto/single-column-board/board-column-board.response.ts +++ b/apps/server/src/modules/learnroom/controller/dto/single-column-board/board-column-board.response.ts @@ -1,6 +1,6 @@ +import { BoardLayout } from '@modules/board'; import { ApiProperty } from '@nestjs/swagger'; import { DecodeHtmlEntities } from '@shared/controller'; -import { BoardLayout } from '@shared/domain/domainobject'; export class BoardColumnBoardResponse { constructor({ id, columnBoardId, title, published, createdAt, updatedAt, layout }: BoardColumnBoardResponse) { diff --git a/apps/server/src/modules/learnroom/learnroom.module.ts b/apps/server/src/modules/learnroom/learnroom.module.ts index b1f53af3da8..3627b94b129 100644 --- a/apps/server/src/modules/learnroom/learnroom.module.ts +++ b/apps/server/src/modules/learnroom/learnroom.module.ts @@ -20,6 +20,7 @@ import { BoardNodeRepo } from '../board/repo'; import { COURSE_REPO } from './domain'; import { CommonCartridgeImportMapper } from './mapper/common-cartridge-import.mapper'; import { CommonCartridgeMapper } from './mapper/common-cartridge.mapper'; +import { ColumnBoardNodeRepo } from './repo'; import { CourseMikroOrmRepo } from './repo/mikro-orm/course.repo'; import { BoardCopyService, @@ -75,6 +76,7 @@ import { CommonCartridgeFileValidatorPipe } from './utils'; RoomsService, UserRepo, GroupDeletedHandlerService, + ColumnBoardNodeRepo, ], exports: [ CourseCopyService, diff --git a/apps/server/src/modules/learnroom/mapper/common-cartridge-import.mapper.spec.ts b/apps/server/src/modules/learnroom/mapper/common-cartridge-import.mapper.spec.ts index 9227c724be4..c9a4f0d544f 100644 --- a/apps/server/src/modules/learnroom/mapper/common-cartridge-import.mapper.spec.ts +++ b/apps/server/src/modules/learnroom/mapper/common-cartridge-import.mapper.spec.ts @@ -1,12 +1,12 @@ import { faker } from '@faker-js/faker'; import { Test, TestingModule } from '@nestjs/testing'; -import { CardInitProps, ColumnInitProps, ContentElementType } from '@shared/domain/domainobject'; -import { LinkContentBody, RichTextContentBody } from '@src/modules/board/controller/dto'; +import { ContentElementType } from '@modules/board/domain'; +import { LinkContentBody, RichTextContentBody } from '@modules/board/controller/dto'; import { CommonCartridgeImportResourceProps, CommonCartridgeOrganizationProps, CommonCartridgeResourceTypeV1P1, -} from '@src/modules/common-cartridge'; +} from '@modules/common-cartridge'; import { InputFormat } from '@shared/domain/types'; import { CommonCartridgeImportMapper } from './common-cartridge-import.mapper'; @@ -56,7 +56,7 @@ describe('CommonCartridgeImportMapper', () => { const result = sut.mapOrganizationToColumn(organization); - expect(result).toEqual({ + expect(result).toEqual({ title: organization.title, }); }); @@ -72,7 +72,7 @@ describe('CommonCartridgeImportMapper', () => { const result = sut.mapOrganizationToCard(organization); - expect(result).toEqual({ + expect(result).toEqual({ title: organization.title, height: 150, }); @@ -87,7 +87,7 @@ describe('CommonCartridgeImportMapper', () => { const result = sut.mapOrganizationToCard(organization, false); - expect(result).toEqual({ + expect(result).toEqual({ title: '', height: 150, }); diff --git a/apps/server/src/modules/learnroom/mapper/common-cartridge-import.mapper.ts b/apps/server/src/modules/learnroom/mapper/common-cartridge-import.mapper.ts index bd387623c3b..e199bf466a9 100644 --- a/apps/server/src/modules/learnroom/mapper/common-cartridge-import.mapper.ts +++ b/apps/server/src/modules/learnroom/mapper/common-cartridge-import.mapper.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { CardInitProps, ColumnInitProps, ContentElementType } from '@shared/domain/domainobject'; +import { ContentElementType } from '@modules/board/domain'; import { InputFormat } from '@shared/domain/types'; -import { AnyElementContentBody, LinkContentBody, RichTextContentBody } from '@src/modules/board/controller/dto'; -import { CommonCartridgeOrganizationProps, CommonCartridgeResourceTypeV1P1 } from '@src/modules/common-cartridge'; +import { AnyElementContentBody, LinkContentBody, RichTextContentBody } from '@modules/board/controller/dto'; +import { CommonCartridgeOrganizationProps, CommonCartridgeResourceTypeV1P1 } from '@modules/common-cartridge'; import { CommonCartridgeResourceProps, CommonCartridgeWebContentResourceProps, @@ -11,13 +11,13 @@ import { @Injectable() export class CommonCartridgeImportMapper { - public mapOrganizationToColumn(organization: CommonCartridgeOrganizationProps): ColumnInitProps { + public mapOrganizationToColumn(organization: CommonCartridgeOrganizationProps) { return { title: organization.title, }; } - public mapOrganizationToCard(organization: CommonCartridgeOrganizationProps, withTitle = true): CardInitProps { + public mapOrganizationToCard(organization: CommonCartridgeOrganizationProps, withTitle = true) { return { title: withTitle ? organization.title : '', height: 150, diff --git a/apps/server/src/modules/learnroom/mapper/common-cartridge.mapper.spec.ts b/apps/server/src/modules/learnroom/mapper/common-cartridge.mapper.spec.ts index 526ad859a29..2fe6f0967a1 100644 --- a/apps/server/src/modules/learnroom/mapper/common-cartridge.mapper.spec.ts +++ b/apps/server/src/modules/learnroom/mapper/common-cartridge.mapper.spec.ts @@ -15,15 +15,8 @@ import { import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { ComponentProperties, ComponentType } from '@shared/domain/entity'; -import { - courseFactory, - lessonFactory, - linkElementFactory, - richTextElementFactory, - setupEntities, - taskFactory, - userFactory, -} from '@shared/testing'; +import { courseFactory, lessonFactory, setupEntities, taskFactory, userFactory } from '@shared/testing'; +import { linkElementFactory, richTextElementFactory } from '@modules/board/testing'; import { LearnroomConfig } from '../learnroom.config'; import { CommonCartridgeMapper } from './common-cartridge.mapper'; diff --git a/apps/server/src/modules/learnroom/mapper/common-cartridge.mapper.ts b/apps/server/src/modules/learnroom/mapper/common-cartridge.mapper.ts index 068f029fb3d..3d684905903 100644 --- a/apps/server/src/modules/learnroom/mapper/common-cartridge.mapper.ts +++ b/apps/server/src/modules/learnroom/mapper/common-cartridge.mapper.ts @@ -1,3 +1,4 @@ +import { LinkElement, RichTextElement } from '@modules/board/domain'; import { CommonCartridgeElementProps, CommonCartridgeElementType, @@ -11,7 +12,6 @@ import { } from '@modules/common-cartridge'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { LinkElement, RichTextElement } from '@shared/domain/domainobject'; import { ComponentProperties, ComponentType, Course, LessonEntity, Task } from '@shared/domain/entity'; import { LearnroomConfig } from '../learnroom.config'; diff --git a/apps/server/src/modules/learnroom/mapper/room-board-response.mapper.spec.ts b/apps/server/src/modules/learnroom/mapper/room-board-response.mapper.spec.ts index 36a5c9073c8..6b8803f8a90 100644 --- a/apps/server/src/modules/learnroom/mapper/room-board-response.mapper.spec.ts +++ b/apps/server/src/modules/learnroom/mapper/room-board-response.mapper.spec.ts @@ -1,7 +1,7 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardLayout } from '@shared/domain/domainobject'; import { courseFactory, setupEntities, taskFactory } from '@shared/testing'; +import { BoardLayout } from '@src/modules/board'; import { BoardElementResponse, SingleColumnBoardResponse } from '../controller/dto'; import { ColumnBoardMetaData, RoomBoardDTO, RoomBoardElementTypes } from '../types'; import { RoomBoardResponseMapper } from './room-board-response.mapper'; diff --git a/apps/server/src/modules/learnroom/repo/index.ts b/apps/server/src/modules/learnroom/repo/index.ts new file mode 100644 index 00000000000..5207a639cba --- /dev/null +++ b/apps/server/src/modules/learnroom/repo/index.ts @@ -0,0 +1,2 @@ +export { ColumnBoardNodeRepo } from './mikro-orm/column-board-node.repo'; +export { CourseMikroOrmRepo } from './mikro-orm/course.repo'; diff --git a/apps/server/src/modules/learnroom/repo/mikro-orm/column-board-node.repo.spec.ts b/apps/server/src/modules/learnroom/repo/mikro-orm/column-board-node.repo.spec.ts new file mode 100644 index 00000000000..c3bc7673863 --- /dev/null +++ b/apps/server/src/modules/learnroom/repo/mikro-orm/column-board-node.repo.spec.ts @@ -0,0 +1,60 @@ +import { EntityManager } from '@mikro-orm/mongodb'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ColumnBoardNode } from '@shared/domain/entity/column-board-node.entity'; +import { columnBoardFactory } from '@modules/board/testing'; +import { ColumnBoardNodeRepo } from './column-board-node.repo'; + +describe('ColumnBoardNodeRepo', () => { + let module: TestingModule; + let repo: ColumnBoardNodeRepo; + let em: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + ColumnBoardNodeRepo, + { + provide: EntityManager, + useValue: createMock(), + }, + ], + }).compile(); + + repo = module.get(ColumnBoardNodeRepo); + em = module.get(EntityManager); + }); + + afterAll(async () => { + await module.close(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('findById', () => { + const setup = () => { + const columnBoardNode = columnBoardFactory.build(); + em.findOneOrFail.mockResolvedValueOnce(columnBoardNode); + + return { columnBoardNode }; + }; + + it('should find ColumnBoardNode by id', async () => { + const id = 'someId'; + await repo.findById(id); + + expect(em.findOneOrFail).toHaveBeenCalledWith(ColumnBoardNode, id); + }); + + it('should return ColumnBoardNode', async () => { + const id = 'someId'; + const { columnBoardNode } = setup(); + + const result = await repo.findById(id); + + expect(result).toBe(columnBoardNode); + }); + }); +}); diff --git a/apps/server/src/modules/learnroom/repo/mikro-orm/column-board-node.repo.ts b/apps/server/src/modules/learnroom/repo/mikro-orm/column-board-node.repo.ts new file mode 100644 index 00000000000..bdff059041e --- /dev/null +++ b/apps/server/src/modules/learnroom/repo/mikro-orm/column-board-node.repo.ts @@ -0,0 +1,28 @@ +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { BoardExternalReference } from '@modules/board'; +import { Injectable } from '@nestjs/common'; +import { ColumnBoardNode } from '@shared/domain/entity/column-board-node.entity'; +import { EntityId } from '@shared/domain/types/entity-id'; + +/** + * @deprecated - this is here only for the sake of the legacy-board (lernraum) + */ +@Injectable() +export class ColumnBoardNodeRepo { + constructor(private readonly em: EntityManager) {} + + async findById(id: EntityId): Promise { + const columnBoardNode = await this.em.findOneOrFail(ColumnBoardNode, id); + + return columnBoardNode; + } + + async findByExternalReference(reference: BoardExternalReference): Promise { + const columnBoardNodes = await this.em.find(ColumnBoardNode, { + contextId: new ObjectId(reference.id), + contextType: reference.type, + }); + + return columnBoardNodes; + } +} diff --git a/apps/server/src/modules/learnroom/service/board-copy.service.spec.ts b/apps/server/src/modules/learnroom/service/board-copy.service.spec.ts index c51df30a606..7fd621811db 100644 --- a/apps/server/src/modules/learnroom/service/board-copy.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/board-copy.service.spec.ts @@ -1,19 +1,17 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ColumnBoardCopyService } from '@modules/board'; +import { ColumnBoardService } from '@modules/board'; +import { BoardExternalReferenceType } from '@modules/board/domain'; import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; import { LessonCopyService } from '@modules/lesson'; import { TaskCopyService } from '@modules/task'; import { Test, TestingModule } from '@nestjs/testing'; import { AuthorizableObject } from '@shared/domain/domain-object'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject/board/types'; import { LegacyBoard } from '@shared/domain/entity'; import { EntityId } from '@shared/domain/types'; import { LegacyBoardRepo } from '@shared/repo'; -import { BoardNodeRepo } from '@modules/board/repo'; import { boardFactory, columnboardBoardElementFactory, - columnBoardFactory, columnBoardNodeFactory, courseFactory, lessonBoardElementFactory, @@ -24,6 +22,8 @@ import { userFactory, } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; +import { columnBoardFactory } from '@src/modules/board/testing'; +import { ColumnBoardNodeRepo } from '../repo'; import { BoardCopyService } from './board-copy.service'; describe('board copy service', () => { @@ -31,7 +31,7 @@ describe('board copy service', () => { let copyService: BoardCopyService; let taskCopyService: DeepMocked; let lessonCopyService: DeepMocked; - let columnBoardCopyService: DeepMocked; + let columnBoardService: DeepMocked; let copyHelperService: DeepMocked; let boardRepo: DeepMocked; @@ -53,8 +53,8 @@ describe('board copy service', () => { useValue: createMock(), }, { - provide: ColumnBoardCopyService, - useValue: createMock(), + provide: ColumnBoardService, + useValue: createMock(), }, { provide: CopyHelperService, @@ -69,8 +69,8 @@ describe('board copy service', () => { useValue: createMock(), }, { - provide: BoardNodeRepo, - useValue: createMock(), + provide: ColumnBoardNodeRepo, + useValue: createMock(), }, ], }).compile(); @@ -79,7 +79,7 @@ describe('board copy service', () => { taskCopyService = module.get(TaskCopyService); lessonCopyService = module.get(LessonCopyService); copyHelperService = module.get(CopyHelperService); - columnBoardCopyService = module.get(ColumnBoardCopyService); + columnBoardService = module.get(ColumnBoardService); boardRepo = module.get(LegacyBoardRepo); boardRepo.save = jest.fn(); }); @@ -254,7 +254,7 @@ describe('board copy service', () => { const user = userFactory.buildWithId(); const copyOfColumnBoard = columnBoardFactory.build(); - columnBoardCopyService.copyColumnBoard.mockResolvedValue({ + columnBoardService.copyColumnBoard.mockResolvedValue({ type: CopyElementType.COLUMNBOARD, status: CopyStatusEnum.SUCCESS, copyEntity: copyOfColumnBoard, @@ -278,7 +278,7 @@ describe('board copy service', () => { }; await copyService.copyBoard({ originalBoard, user, destinationCourse }); - expect(columnBoardCopyService.copyColumnBoard).toHaveBeenCalledWith(expected); + expect(columnBoardService.copyColumnBoard).toHaveBeenCalledWith(expected); }); it('should add columnBoard copy to board copy', async () => { @@ -322,13 +322,13 @@ describe('board copy service', () => { lessonCopyService.updateCopiedEmbeddedTasks = jest.fn().mockImplementation((status: CopyStatus) => status); const originalColumnBoard = columnBoardFactory.build(); - const columnBoardTarget = columnBoardNodeFactory.buildWithId({ - id: originalColumnBoard.id, + const columnBoardTarget = columnBoardNodeFactory.build({ title: originalColumnBoard.title, }); + columnBoardTarget.id = originalColumnBoard.id; const columnBoardElement = columnboardBoardElementFactory.buildWithId({ target: columnBoardTarget }); const columnBoardCopy = columnBoardFactory.build(); - columnBoardCopyService.copyColumnBoard.mockResolvedValue({ + columnBoardService.copyColumnBoard.mockResolvedValue({ type: CopyElementType.COLUMNBOARD, status: CopyStatusEnum.SUCCESS, copyEntity: columnBoardCopy, @@ -367,14 +367,14 @@ describe('board copy service', () => { const { destinationCourse, originalBoard, user, columnBoardCopy } = setup(); await copyService.copyBoard({ originalBoard, user, destinationCourse }); - expect(columnBoardCopyService.swapLinkedIds).toHaveBeenCalledWith(columnBoardCopy.id, expect.anything()); + expect(columnBoardService.swapLinkedIds).toHaveBeenCalledWith(columnBoardCopy.id, expect.anything()); }); it('should pass task for swapping ids', async () => { const { destinationCourse, originalBoard, user, originalTask, taskCopy } = setup(); await copyService.copyBoard({ originalBoard, user, destinationCourse }); - const map = columnBoardCopyService.swapLinkedIds.mock.calls[0][1]; + const map = columnBoardService.swapLinkedIds.mock.calls[0][1]; expect(map.get(originalTask.id)).toEqual(taskCopy.id); }); @@ -382,7 +382,7 @@ describe('board copy service', () => { const { destinationCourse, originalBoard, user, originalLesson, lessonCopy } = setup(); await copyService.copyBoard({ originalBoard, user, destinationCourse }); - const map = columnBoardCopyService.swapLinkedIds.mock.calls[0][1]; + const map = columnBoardService.swapLinkedIds.mock.calls[0][1]; expect(map.get(originalLesson.id)).toEqual(lessonCopy.id); }); @@ -390,7 +390,7 @@ describe('board copy service', () => { const { originalCourse, destinationCourse, originalBoard, user } = setup(); await copyService.copyBoard({ originalBoard, user, destinationCourse }); - const map = columnBoardCopyService.swapLinkedIds.mock.calls[0][1]; + const map = columnBoardService.swapLinkedIds.mock.calls[0][1]; expect(map.get(originalCourse.id)).toEqual(destinationCourse.id); }); }); diff --git a/apps/server/src/modules/learnroom/service/board-copy.service.ts b/apps/server/src/modules/learnroom/service/board-copy.service.ts index d418a32e475..b77dd910e6f 100644 --- a/apps/server/src/modules/learnroom/service/board-copy.service.ts +++ b/apps/server/src/modules/learnroom/service/board-copy.service.ts @@ -1,15 +1,15 @@ -import { ColumnBoardCopyService } from '@modules/board'; +import { BoardExternalReferenceType, ColumnBoard, ColumnBoardService } from '@modules/board'; import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; import { LessonCopyService } from '@modules/lesson'; import { TaskCopyService } from '@modules/task'; import { Injectable } from '@nestjs/common'; import { getResolvedValues } from '@shared/common/utils/promise'; -import { ColumnBoard } from '@shared/domain/domainobject'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject/board/types'; import { ColumnboardBoardElement, ColumnBoardNode, Course, + isLesson, + isTask, LegacyBoard, LegacyBoardElement, LegacyBoardElementType, @@ -18,15 +18,12 @@ import { Task, TaskBoardElement, User, - isLesson, - isTask, } from '@shared/domain/entity'; import { EntityId } from '@shared/domain/types'; import { LegacyBoardRepo } from '@shared/repo'; import { LegacyLogger } from '@src/core/logger'; import { sortBy } from 'lodash'; - -import { BoardNodeRepo } from '@modules/board/repo'; +import { ColumnBoardNodeRepo } from '../repo'; type BoardCopyParams = { originalBoard: LegacyBoard; @@ -41,9 +38,10 @@ export class BoardCopyService { private readonly boardRepo: LegacyBoardRepo, private readonly taskCopyService: TaskCopyService, private readonly lessonCopyService: LessonCopyService, - private readonly columnBoardCopyService: ColumnBoardCopyService, + private readonly columnBoardService: ColumnBoardService, private readonly copyHelperService: CopyHelperService, - private readonly boardNodeRepo: BoardNodeRepo + // TODO comment this, legacy! + private readonly columnBoardNodeRepo: ColumnBoardNodeRepo ) {} async copyBoard(params: BoardCopyParams): Promise { @@ -135,12 +133,12 @@ export class BoardCopyService { } private async copyColumnBoard( - columnBoardNode: ColumnBoardNode, + columnBoard: ColumnBoardNode, user: User, destinationCourse: Course ): Promise { - return this.columnBoardCopyService.copyColumnBoard({ - originalColumnBoardId: columnBoardNode.id, + return this.columnBoardService.copyColumnBoard({ + originalColumnBoardId: columnBoard.id, userId: user.id, destinationExternalReference: { id: destinationCourse.id, @@ -162,8 +160,9 @@ export class BoardCopyService { references.push(lessonElement); } if (status.copyEntity instanceof ColumnBoard) { + // TODO comment this, legacy! // eslint-disable-next-line no-await-in-loop - const columnBoardNode = (await this.boardNodeRepo.findById(status.copyEntity.id)) as ColumnBoardNode; + const columnBoardNode = await this.columnBoardNodeRepo.findById(status.copyEntity.id); const columnBoardElement = new ColumnboardBoardElement({ target: columnBoardNode, }); @@ -199,7 +198,7 @@ export class BoardCopyService { const updatedElements = await Promise.all( elements.map(async (el) => { if (el.type === CopyElementType.COLUMNBOARD && el.copyEntity) { - el.copyEntity = await this.columnBoardCopyService.swapLinkedIds(el.copyEntity?.id, map); + el.copyEntity = await this.columnBoardService.swapLinkedIds(el.copyEntity?.id, map); } return el; }) diff --git a/apps/server/src/modules/learnroom/service/common-cartridge-export.service.spec.ts b/apps/server/src/modules/learnroom/service/common-cartridge-export.service.spec.ts index 72228c80798..605d9ad3cdc 100644 --- a/apps/server/src/modules/learnroom/service/common-cartridge-export.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/common-cartridge-export.service.spec.ts @@ -7,18 +7,15 @@ import { TaskService } from '@modules/task'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { ComponentType } from '@shared/domain/entity'; +import { courseFactory, lessonFactory, setupEntities, taskFactory } from '@shared/testing'; +import { ColumnBoardService } from '@src/modules/board'; import { cardFactory, columnBoardFactory, columnFactory, - courseFactory, - lessonFactory, linkElementFactory, richTextElementFactory, - setupEntities, - taskFactory, -} from '@shared/testing'; -import { ColumnBoardService } from '@src/modules/board'; +} from '@src/modules/board/testing'; import AdmZip from 'adm-zip'; import { CommonCartridgeMapper } from '../mapper/common-cartridge.mapper'; @@ -89,7 +86,7 @@ describe('CommonCartridgeExportService', () => { lessonServiceMock.findByCourseIds.mockResolvedValue([lessons, lessons.length]); taskServiceMock.findBySingleParent.mockResolvedValue([tasks, tasks.length]); configServiceMock.getOrThrow.mockReturnValue(faker.internet.url()); - columnBoardServiceMock.findIdsByExternalReference.mockResolvedValue([columnBoard.id]); + columnBoardServiceMock.findByExternalReference.mockResolvedValue([columnBoard]); columnBoardServiceMock.findById.mockResolvedValue(columnBoard); const buffer = await sut.exportCourse( @@ -198,14 +195,14 @@ describe('CommonCartridgeExportService', () => { const { archive, column } = await setup(); const manifest = getFileContent(archive, 'imsmanifest.xml'); - expect(manifest).toContain(createXmlString('title', column.title)); + expect(manifest).toContain(createXmlString('title', column.title ?? '')); }); it('should add card', async () => { const { archive, card } = await setup(); const manifest = getFileContent(archive, 'imsmanifest.xml'); - expect(manifest).toContain(createXmlString('title', card.title)); + expect(manifest).toContain(createXmlString('title', card.title ?? '')); }); it('should add content element of cards', async () => { @@ -275,14 +272,14 @@ describe('CommonCartridgeExportService', () => { const { archive, column } = await setup(); const manifest = getFileContent(archive, 'imsmanifest.xml'); - expect(manifest).toContain(createXmlString('title', column.title)); + expect(manifest).toContain(createXmlString('title', column.title ?? '')); }); it('should add card', async () => { const { archive, card } = await setup(); const manifest = getFileContent(archive, 'imsmanifest.xml'); - expect(manifest).toContain(createXmlString('title', card.title)); + expect(manifest).toContain(createXmlString('title', card.title ?? '')); }); it('should add content element of cards', async () => { diff --git a/apps/server/src/modules/learnroom/service/common-cartridge-export.service.ts b/apps/server/src/modules/learnroom/service/common-cartridge-export.service.ts index 496c68285fb..4c146bcfeb6 100644 --- a/apps/server/src/modules/learnroom/service/common-cartridge-export.service.ts +++ b/apps/server/src/modules/learnroom/service/common-cartridge-export.service.ts @@ -1,13 +1,5 @@ import { - CommonCartridgeFileBuilder, - CommonCartridgeOrganizationBuilder, - CommonCartridgeVersion, -} from '@modules/common-cartridge'; -import { LessonService } from '@modules/lesson'; -import { TaskService } from '@modules/task'; -import { Injectable } from '@nestjs/common'; -import { - AnyBoardDo, + AnyBoardNode, BoardExternalReferenceType, Card, Column, @@ -15,7 +7,15 @@ import { isColumn, isLinkElement, isRichTextElement, -} from '@shared/domain/domainobject'; +} from '@modules/board/domain'; +import { + CommonCartridgeFileBuilder, + CommonCartridgeOrganizationBuilder, + CommonCartridgeVersion, +} from '@modules/common-cartridge'; +import { LessonService } from '@modules/lesson'; +import { TaskService } from '@modules/task'; +import { Injectable } from '@nestjs/common'; import { ComponentProperties } from '@shared/domain/entity'; import { EntityId } from '@shared/domain/types'; import { ColumnBoardService } from '@src/modules/board'; @@ -110,16 +110,14 @@ export class CommonCartridgeExportService { courseId: EntityId, exportedColumnBoards: string[] ): Promise { - const columnBoardIds = ( - await this.columnBoardService.findIdsByExternalReference({ + const columnBoards = ( + await this.columnBoardService.findByExternalReference({ type: BoardExternalReferenceType.Course, id: courseId, }) - ).filter((id) => exportedColumnBoards.includes(id)); - - for await (const columnBoardId of columnBoardIds) { - const columnBoard = await this.columnBoardService.findById(columnBoardId); + ).filter((cb) => exportedColumnBoards.includes(cb.id)); + for (const columnBoard of columnBoards) { const organization = builder.addOrganization({ title: columnBoard.title, identifier: createIdentifier(columnBoard.id), @@ -134,7 +132,7 @@ export class CommonCartridgeExportService { private addColumnToOrganization(column: Column, organizationBuilder: CommonCartridgeOrganizationBuilder): void { const { id } = column; const columnOrganization = organizationBuilder.addSubOrganization({ - title: column.title, + title: column.title ?? '', identifier: createIdentifier(id), }); @@ -146,7 +144,7 @@ export class CommonCartridgeExportService { private addCardToOrganization(card: Card, organizationBuilder: CommonCartridgeOrganizationBuilder): void { const { id } = card; const cardOrganization = organizationBuilder.addSubOrganization({ - title: card.title, + title: card.title ?? '', identifier: createIdentifier(id), }); @@ -154,7 +152,7 @@ export class CommonCartridgeExportService { } private addCardElementToOrganization( - element: AnyBoardDo, + element: AnyBoardNode, organizationBuilder: CommonCartridgeOrganizationBuilder ): void { if (isRichTextElement(element)) { diff --git a/apps/server/src/modules/learnroom/service/common-cartridge-import.service.spec.ts b/apps/server/src/modules/learnroom/service/common-cartridge-import.service.spec.ts index 440457ca85a..1967d3e51a8 100644 --- a/apps/server/src/modules/learnroom/service/common-cartridge-import.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/common-cartridge-import.service.spec.ts @@ -2,7 +2,7 @@ import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { MikroORM } from '@mikro-orm/core'; import { Test, TestingModule } from '@nestjs/testing'; import { setupEntities, userFactory } from '@shared/testing'; -import { CardService, ColumnBoardService, ColumnService, ContentElementService } from '@src/modules/board'; +import { BoardNodeFactory, BoardNodeService } from '@src/modules/board'; import { readFile } from 'fs/promises'; import { CommonCartridgeImportMapper } from '../mapper/common-cartridge-import.mapper'; import { CommonCartridgeImportService } from './common-cartridge-import.service'; @@ -13,10 +13,8 @@ describe('CommonCartridgeImportService', () => { let moduleRef: TestingModule; let sut: CommonCartridgeImportService; let courseServiceMock: DeepMocked; - let columnBoardServiceMock: DeepMocked; - let columnServiceMock: DeepMocked; - let cardServiceMock: DeepMocked; - let contentElementServiceMock: DeepMocked; + let boardNodeFactoryMock: DeepMocked; + let boardNodeServiceMock: DeepMocked; beforeEach(async () => { orm = await setupEntities(); @@ -29,30 +27,20 @@ describe('CommonCartridgeImportService', () => { useValue: createMock(), }, { - provide: ColumnBoardService, - useValue: createMock(), + provide: BoardNodeFactory, + useValue: createMock(), }, { - provide: ColumnService, - useValue: createMock(), - }, - { - provide: CardService, - useValue: createMock(), - }, - { - provide: ContentElementService, - useValue: createMock(), + provide: BoardNodeService, + useValue: createMock(), }, ], }).compile(); sut = moduleRef.get(CommonCartridgeImportService); courseServiceMock = moduleRef.get(CourseService); - columnBoardServiceMock = moduleRef.get(ColumnBoardService); - columnServiceMock = moduleRef.get(ColumnService); - cardServiceMock = moduleRef.get(CardService); - contentElementServiceMock = moduleRef.get(ContentElementService); + boardNodeFactoryMock = moduleRef.get(BoardNodeFactory); + boardNodeServiceMock = moduleRef.get(BoardNodeService); }); afterAll(async () => { @@ -61,7 +49,7 @@ describe('CommonCartridgeImportService', () => { }); beforeEach(() => { - jest.clearAllMocks(); + jest.resetAllMocks(); }); it('should be defined', () => { @@ -93,7 +81,8 @@ describe('CommonCartridgeImportService', () => { await sut.importFile(user, buffer); - expect(columnBoardServiceMock.create).toHaveBeenCalledTimes(14); + expect(boardNodeFactoryMock.buildColumnBoard).toHaveBeenCalledTimes(14); + expect(boardNodeServiceMock.addRoot).toHaveBeenCalledTimes(14); }); it('should create columns', async () => { @@ -101,7 +90,8 @@ describe('CommonCartridgeImportService', () => { await sut.importFile(user, buffer); - expect(columnServiceMock.create).toHaveBeenCalledTimes(103); + expect(boardNodeFactoryMock.buildColumn).toHaveBeenCalled(); + expect(boardNodeServiceMock.addToParent).toHaveBeenCalled(); }); it('should create cards', async () => { @@ -109,7 +99,8 @@ describe('CommonCartridgeImportService', () => { await sut.importFile(user, buffer); - expect(cardServiceMock.create).toHaveBeenCalled(); + expect(boardNodeFactoryMock.buildCard).toHaveBeenCalled(); + expect(boardNodeServiceMock.addToParent).toHaveBeenCalled(); }); it('should create elements', async () => { @@ -117,7 +108,9 @@ describe('CommonCartridgeImportService', () => { await sut.importFile(user, buffer); - expect(contentElementServiceMock.create).toHaveBeenCalled(); + expect(boardNodeFactoryMock.buildContentElement).toHaveBeenCalled(); + expect(boardNodeServiceMock.addToParent).toHaveBeenCalled(); + expect(boardNodeServiceMock.updateContent).toHaveBeenCalled(); }); }); @@ -138,7 +131,8 @@ describe('CommonCartridgeImportService', () => { await sut.importFile(user, buffer); - expect(columnBoardServiceMock.create).toHaveBeenCalledTimes(3); + expect(boardNodeFactoryMock.buildColumnBoard).toHaveBeenCalledTimes(3); + expect(boardNodeServiceMock.addRoot).toHaveBeenCalledTimes(3); }); it('should create columns', async () => { @@ -146,7 +140,8 @@ describe('CommonCartridgeImportService', () => { await sut.importFile(user, buffer); - expect(columnServiceMock.create).toHaveBeenCalledTimes(6); + expect(boardNodeFactoryMock.buildColumn).toHaveBeenCalled(); + expect(boardNodeServiceMock.addToParent).toHaveBeenCalled(); }); it('should create cards', async () => { @@ -154,7 +149,8 @@ describe('CommonCartridgeImportService', () => { await sut.importFile(user, buffer); - expect(cardServiceMock.create).toHaveBeenCalled(); + expect(boardNodeFactoryMock.buildCard).toHaveBeenCalled(); + expect(boardNodeServiceMock.addToParent).toHaveBeenCalled(); }); it('should create elements', async () => { @@ -162,7 +158,9 @@ describe('CommonCartridgeImportService', () => { await sut.importFile(user, buffer); - expect(contentElementServiceMock.create).toHaveBeenCalled(); + expect(boardNodeFactoryMock.buildContentElement).toHaveBeenCalled(); + expect(boardNodeServiceMock.addToParent).toHaveBeenCalled(); + expect(boardNodeServiceMock.updateContent).toHaveBeenCalled(); }); }); }); diff --git a/apps/server/src/modules/learnroom/service/common-cartridge-import.service.ts b/apps/server/src/modules/learnroom/service/common-cartridge-import.service.ts index bd49b3c32d3..1cded6df376 100644 --- a/apps/server/src/modules/learnroom/service/common-cartridge-import.service.ts +++ b/apps/server/src/modules/learnroom/service/common-cartridge-import.service.ts @@ -1,14 +1,15 @@ -import { Injectable } from '@nestjs/common'; import { BoardExternalReferenceType, BoardLayout, + BoardNodeFactory, Card, + ContentElementType, Column, ColumnBoard, - ContentElementType, -} from '@shared/domain/domainobject'; +} from '@modules/board/domain'; +import { BoardNodeService } from '@modules/board/service'; +import { Injectable } from '@nestjs/common'; import { Course, User } from '@shared/domain/entity'; -import { CardService, ColumnBoardService, ColumnService, ContentElementService } from '@src/modules/board'; import { CommonCartridgeFileParser, CommonCartridgeOrganizationProps, @@ -21,10 +22,8 @@ import { CourseService } from './course.service'; export class CommonCartridgeImportService { constructor( private readonly courseService: CourseService, - private readonly columnBoardService: ColumnBoardService, - private readonly columnService: ColumnService, - private readonly cardService: CardService, - private readonly contentElementService: ContentElementService, + private readonly boardNodeFactory: BoardNodeFactory, + private readonly boardNodeService: BoardNodeService, private readonly mapper: CommonCartridgeImportMapper ) {} @@ -51,14 +50,15 @@ export class CommonCartridgeImportService { boardProps: CommonCartridgeOrganizationProps, organizations: CommonCartridgeOrganizationProps[] ): Promise { - const columnBoard = await this.columnBoardService.create( - { + const columnBoard = this.boardNodeFactory.buildColumnBoard({ + context: { type: BoardExternalReferenceType.Course, id: course.id, }, - BoardLayout.COLUMNS, - boardProps.title || '' - ); + layout: BoardLayout.COLUMNS, + title: boardProps.title || '', + }); + await this.boardNodeService.addRoot(columnBoard); await this.createColumns(parser, columnBoard, boardProps, organizations); } @@ -93,7 +93,10 @@ export class CommonCartridgeImportService { columnBoard: ColumnBoard, columnProps: CommonCartridgeOrganizationProps ): Promise { - const column = await this.columnService.create(columnBoard, this.mapper.mapOrganizationToColumn(columnProps)); + const column = this.boardNodeFactory.buildColumn(); + const { title } = this.mapper.mapOrganizationToColumn(columnProps); + column.title = title; + await this.boardNodeService.addToParent(columnBoard, column); await this.createCardWithElement(parser, column, columnProps, false); } @@ -103,7 +106,11 @@ export class CommonCartridgeImportService { columnProps: CommonCartridgeOrganizationProps, organizations: CommonCartridgeOrganizationProps[] ): Promise { - const column = await this.columnService.create(columnBoard, this.mapper.mapOrganizationToColumn(columnProps)); + const column = this.boardNodeFactory.buildColumn(); + const { title } = this.mapper.mapOrganizationToColumn(columnProps); + column.title = title; + await this.boardNodeService.addToParent(columnBoard, column); + const cards = organizations.filter( (organization) => organization.pathDepth === 2 && organization.path.startsWith(columnProps.path) ); @@ -127,19 +134,20 @@ export class CommonCartridgeImportService { cardProps: CommonCartridgeOrganizationProps, withTitle = true ): Promise { - const card = await this.cardService.create( - column, - undefined, - this.mapper.mapOrganizationToCard(cardProps, withTitle) - ); + const card = this.boardNodeFactory.buildCard(); + const { title, height } = this.mapper.mapOrganizationToCard(cardProps, withTitle); + card.title = title; + card.height = height; + await this.boardNodeService.addToParent(column, card); const resource = parser.getResource(cardProps); const contentElementType = this.mapper.mapResourceTypeToContentElementType(resource?.type); if (resource && contentElementType) { - const contentElement = await this.contentElementService.create(card, contentElementType); + const contentElement = this.boardNodeFactory.buildContentElement(contentElementType); + await this.boardNodeService.addToParent(card, contentElement); const contentElementBody = this.mapper.mapResourceToContentElementBody(resource); - await this.contentElementService.update(contentElement, contentElementBody); + await this.boardNodeService.updateContent(contentElement, contentElementBody); } } @@ -149,7 +157,11 @@ export class CommonCartridgeImportService { cardProps: CommonCartridgeOrganizationProps, organizations: CommonCartridgeOrganizationProps[] ) { - const card = await this.cardService.create(column, undefined, this.mapper.mapOrganizationToCard(cardProps, true)); + const card = this.boardNodeFactory.buildCard(); + const { title, height } = this.mapper.mapOrganizationToCard(cardProps, true); + card.title = title; + card.height = height; + await this.boardNodeService.addToParent(column, column); const cardElements = organizations.filter( (organization) => organization.pathDepth >= 3 && organization.path.startsWith(cardProps.path) @@ -170,16 +182,17 @@ export class CommonCartridgeImportService { const contentElementType = this.mapper.mapResourceTypeToContentElementType(resource?.type); if (resource && contentElementType) { - const contentElement = await this.contentElementService.create(card, contentElementType); + const contentElement = this.boardNodeFactory.buildContentElement(contentElementType); + await this.boardNodeService.addToParent(card, contentElement); const contentElementBody = this.mapper.mapResourceToContentElementBody(resource); - - await this.contentElementService.update(contentElement, contentElementBody); + await this.boardNodeService.updateContent(contentElement, contentElementBody); } } else { - const contentElement = await this.contentElementService.create(card, ContentElementType.RICH_TEXT); + const contentElement = this.boardNodeFactory.buildContentElement(ContentElementType.RICH_TEXT); + await this.boardNodeService.addToParent(card, contentElement); const contentElementBody = this.mapper.mapOrganizationToTextElement(cardElementProps); - await this.contentElementService.update(contentElement, contentElementBody); + await this.boardNodeService.updateContent(contentElement, contentElementBody); } } } diff --git a/apps/server/src/modules/learnroom/service/rooms.service.spec.ts b/apps/server/src/modules/learnroom/service/rooms.service.spec.ts index 359d25649eb..4b51edb946d 100644 --- a/apps/server/src/modules/learnroom/service/rooms.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/rooms.service.spec.ts @@ -1,7 +1,6 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { IConfig } from '@hpi-schul-cloud/commons/lib/interfaces/IConfig'; -import { CardService, ColumnBoardService, ColumnService, ContentElementService } from '@modules/board'; import { LessonService } from '@modules/lesson'; import { TaskService } from '@modules/task'; import { Test, TestingModule } from '@nestjs/testing'; @@ -15,8 +14,7 @@ import { taskFactory, userFactory, } from '@shared/testing'; -import { ColumnBoardNode } from '@shared/domain/entity'; -import { BoardNodeRepo } from '@modules/board/repo'; +import { ColumnBoardNodeRepo } from '../repo'; import { RoomsService } from './rooms.service'; describe('rooms service', () => { @@ -25,8 +23,7 @@ describe('rooms service', () => { let lessonService: DeepMocked; let taskService: DeepMocked; let legacyBoardRepo: DeepMocked; - let columnBoardService: DeepMocked; - let boardNodeRepo: DeepMocked; + let columnBoardNodeRepo: DeepMocked; let configBefore: IConfig; afterAll(async () => { @@ -52,28 +49,8 @@ describe('rooms service', () => { useValue: createMock(), }, { - provide: ColumnBoardService, - useValue: createMock(), - }, - { - provide: ColumnService, - useValue: createMock(), - }, - { - provide: CardService, - useValue: createMock(), - }, - { - provide: ContentElementService, - useValue: createMock(), - }, - { - provide: ColumnBoardNode, - useValue: createMock(), - }, - { - provide: BoardNodeRepo, - useValue: createMock(), + provide: ColumnBoardNodeRepo, + useValue: createMock(), }, ], }).compile(); @@ -81,8 +58,7 @@ describe('rooms service', () => { lessonService = module.get(LessonService); taskService = module.get(TaskService); legacyBoardRepo = module.get(LegacyBoardRepo); - columnBoardService = module.get(ColumnBoardService); - boardNodeRepo = module.get(BoardNodeRepo); + columnBoardNodeRepo = module.get(ColumnBoardNodeRepo); }); afterEach(() => { @@ -107,8 +83,7 @@ describe('rooms service', () => { const tasksSpy = taskService.findBySingleParent.mockResolvedValue([tasks, 3]); const lessonsSpy = lessonService.findByCourseIds.mockResolvedValue([lessons, 3]); - columnBoardService.findIdsByExternalReference.mockResolvedValue([columnBoardNode.id]); - boardNodeRepo.findById.mockResolvedValue(columnBoardNode); + columnBoardNodeRepo.findByExternalReference.mockResolvedValue([columnBoardNode]); const syncBoardElementReferencesSpy = jest.spyOn(legacyBoard, 'syncBoardElementReferences'); const saveSpy = legacyBoardRepo.save.mockResolvedValue(); @@ -145,16 +120,11 @@ describe('rooms service', () => { await roomsService.updateLegacyBoard(board, room.id, user.id); - expect(columnBoardService.findIdsByExternalReference).toHaveBeenCalledWith({ + expect(columnBoardNodeRepo.findByExternalReference).toHaveBeenCalledWith({ type: 'course', id: room.id, }); }); - it('should fetch all column boards', async () => { - const { board, room, user, columnBoardNode } = setup(); - await roomsService.updateLegacyBoard(board, room.id, user.id); - expect(boardNodeRepo.findById).toHaveBeenCalledWith(columnBoardNode.id); - }); it('should sync legacy boards lessons with fetched tasks and lessons and columnBoards', async () => { const { board, room, user, tasks, lessons, columnBoardNode, syncBoardElementReferencesSpy } = setup(); diff --git a/apps/server/src/modules/learnroom/service/rooms.service.ts b/apps/server/src/modules/learnroom/service/rooms.service.ts index 0bc0c8cf7d4..e7714ae8192 100644 --- a/apps/server/src/modules/learnroom/service/rooms.service.ts +++ b/apps/server/src/modules/learnroom/service/rooms.service.ts @@ -1,13 +1,11 @@ -import { ColumnBoardService } from '@modules/board'; +import { BoardExternalReferenceType } from '@modules/board'; import { LessonService } from '@modules/lesson'; import { TaskService } from '@modules/task'; import { Injectable } from '@nestjs/common'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; -import { LegacyBoard, ColumnBoardNode } from '@shared/domain/entity'; +import { LegacyBoard } from '@shared/domain/entity'; import { EntityId } from '@shared/domain/types'; import { LegacyBoardRepo } from '@shared/repo'; -// eslint-disable-next-line @typescript-eslint/no-restricted-imports -import { BoardNodeRepo } from '@modules/board/repo'; +import { ColumnBoardNodeRepo } from '../repo'; @Injectable() export class RoomsService { @@ -15,24 +13,20 @@ export class RoomsService { private readonly taskService: TaskService, private readonly lessonService: LessonService, private readonly boardRepo: LegacyBoardRepo, - private readonly columnBoardService: ColumnBoardService, - private readonly boardNodeRepo: BoardNodeRepo + private readonly columnBoardNodeRepo: ColumnBoardNodeRepo ) {} async updateLegacyBoard(board: LegacyBoard, roomId: EntityId, userId: EntityId): Promise { const [courseLessons] = await this.lessonService.findByCourseIds([roomId]); const [courseTasks] = await this.taskService.findBySingleParent(userId, roomId); - const columnBoardIds = await this.columnBoardService.findIdsByExternalReference({ + // TODO comment this, legacy! + const columnBoardNodes = await this.columnBoardNodeRepo.findByExternalReference({ type: BoardExternalReferenceType.Course, id: roomId, }); - const columnBoards = await Promise.all( - columnBoardIds.map(async (id) => (await this.boardNodeRepo.findById(id)) as ColumnBoardNode) - ); - - const boardElementTargets = [...courseLessons, ...courseTasks, ...columnBoards]; + const boardElementTargets = [...courseLessons, ...courseTasks, ...columnBoardNodes]; board.syncBoardElementReferences(boardElementTargets); diff --git a/apps/server/src/modules/learnroom/types/room-board.types.ts b/apps/server/src/modules/learnroom/types/room-board.types.ts index d657e0d1a9b..493894e3f55 100644 --- a/apps/server/src/modules/learnroom/types/room-board.types.ts +++ b/apps/server/src/modules/learnroom/types/room-board.types.ts @@ -1,4 +1,4 @@ -import { BoardLayout } from '@shared/domain/domainobject'; +import { BoardLayout } from '@modules/board'; import { TaskWithStatusVo } from '@shared/domain/entity'; import { EntityId } from '@shared/domain/types'; diff --git a/apps/server/src/modules/learnroom/uc/rooms.uc.ts b/apps/server/src/modules/learnroom/uc/rooms.uc.ts index a9710c128f6..a7422bf835a 100644 --- a/apps/server/src/modules/learnroom/uc/rooms.uc.ts +++ b/apps/server/src/modules/learnroom/uc/rooms.uc.ts @@ -1,6 +1,6 @@ import { ForbiddenException, Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain/types'; -import { LegacyBoardRepo, CourseRepo, UserRepo } from '@shared/repo'; +import { CourseRepo, LegacyBoardRepo, UserRepo } from '@shared/repo'; import { RoomsService } from '../service/rooms.service'; import { RoomBoardDTO } from '../types'; import { RoomBoardDTOFactory } from './room-board-dto.factory'; diff --git a/apps/server/src/modules/management/controller/api-test/database-management.api.spec.ts b/apps/server/src/modules/management/controller/api-test/database-management.api.spec.ts index 070d6ad1de8..6753f6a2b06 100644 --- a/apps/server/src/modules/management/controller/api-test/database-management.api.spec.ts +++ b/apps/server/src/modules/management/controller/api-test/database-management.api.spec.ts @@ -1,7 +1,9 @@ import { MikroORM } from '@mikro-orm/core'; +import { EntityManager } from '@mikro-orm/mongodb'; import { ManagementServerTestModule } from '@modules/management/management-server.module'; import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; +import { createCollections } from '@shared/testing'; import request from 'supertest'; describe('Database Management Controller (API)', () => { @@ -18,6 +20,9 @@ describe('Database Management Controller (API)', () => { await app.init(); orm = module.get(MikroORM); await orm.getSchemaGenerator().clearDatabase(); + + const em = module.get(EntityManager); + await createCollections(em); }); afterAll(async () => { diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.spec.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.spec.ts index a8587b6f274..57243e7dfe7 100644 --- a/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.spec.ts +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.spec.ts @@ -1,9 +1,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ColumnBoardService } from '@modules/board'; +import { ColumnBoardService, ColumnBoard } from '@modules/board'; import { CourseService } from '@modules/learnroom'; import { Test, TestingModule } from '@nestjs/testing'; -import { ColumnBoard } from '@shared/domain/domainobject'; import { setupEntities } from '@shared/testing'; import { BoardUrlHandler } from './board-url-handler'; diff --git a/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.ts b/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.ts index 0fff34ae377..447536d6850 100644 --- a/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.ts +++ b/apps/server/src/modules/meta-tag-extractor/service/url-handler/board-url-handler.ts @@ -1,7 +1,6 @@ -import { ColumnBoardService } from '@modules/board'; +import { BoardExternalReferenceType, ColumnBoardService } from '@modules/board'; import { CourseService } from '@modules/learnroom'; import { Injectable } from '@nestjs/common'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; import type { UrlHandler } from '../../interface/url-handler'; import { MetaData } from '../../types'; import { AbstractUrlHandler } from './abstract-url-handler'; diff --git a/apps/server/src/modules/sharing/service/share-token.service.spec.ts b/apps/server/src/modules/sharing/service/share-token.service.spec.ts index 3244491399b..1f721e7a00b 100644 --- a/apps/server/src/modules/sharing/service/share-token.service.spec.ts +++ b/apps/server/src/modules/sharing/service/share-token.service.spec.ts @@ -1,14 +1,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { - courseFactory, - columnBoardFactory, - lessonFactory, - setupEntities, - shareTokenFactory, - taskFactory, -} from '@shared/testing'; +import { courseFactory, lessonFactory, setupEntities, shareTokenFactory, taskFactory } from '@shared/testing'; +import { columnBoardFactory } from '@modules/board/testing'; import { CourseService } from '@modules/learnroom/service'; import { ColumnBoardService } from '@modules/board/service'; import { LessonService } from '@modules/lesson/service'; diff --git a/apps/server/src/modules/sharing/service/share-token.service.ts b/apps/server/src/modules/sharing/service/share-token.service.ts index 051bc7efb69..3c0a2748c1b 100644 --- a/apps/server/src/modules/sharing/service/share-token.service.ts +++ b/apps/server/src/modules/sharing/service/share-token.service.ts @@ -1,8 +1,8 @@ -import { Injectable, UnprocessableEntityException } from '@nestjs/common'; +import { ColumnBoardService } from '@modules/board'; import { CourseService } from '@modules/learnroom/service'; -import { ColumnBoardService } from '@modules/board/service'; import { LessonService } from '@modules/lesson/service'; import { TaskService } from '@modules/task/service'; +import { Injectable, UnprocessableEntityException } from '@nestjs/common'; import { ShareTokenContext, ShareTokenDO, diff --git a/apps/server/src/modules/sharing/uc/share-token.uc.spec.ts b/apps/server/src/modules/sharing/uc/share-token.uc.spec.ts index bb594eaceae..4b33a73d438 100644 --- a/apps/server/src/modules/sharing/uc/share-token.uc.spec.ts +++ b/apps/server/src/modules/sharing/uc/share-token.uc.spec.ts @@ -2,7 +2,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; -import { BoardDoAuthorizableService, ColumnBoardCopyService, ColumnBoardService } from '@modules/board'; +import { BoardExternalReferenceType, BoardNodeAuthorizableService, ColumnBoardService } from '@modules/board'; +import { boardNodeAuthorizableFactory, columnBoardFactory } from '@modules/board/testing'; import { CopyElementType, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; import { CourseCopyService, CourseService } from '@modules/learnroom'; import { LessonCopyService, LessonService } from '@modules/lesson'; @@ -11,11 +12,8 @@ import { schoolFactory } from '@modules/school/testing'; import { TaskCopyService, TaskService } from '@modules/task'; import { BadRequestException, InternalServerErrorException, NotImplementedException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; import { Permission } from '@shared/domain/interface'; import { - boardDoAuthorizableFactory, - columnBoardFactory, courseFactory, lessonFactory, schoolEntityFactory, @@ -36,10 +34,9 @@ describe('ShareTokenUC', () => { let courseCopyService: DeepMocked; let lessonCopyService: DeepMocked; let taskCopyService: DeepMocked; - let columnBoardCopyService: DeepMocked; let authorizationService: DeepMocked; let courseService: DeepMocked; - let boardDoAuthorizableService: DeepMocked; + let boardNodeAuthorizableService: DeepMocked; let lessonService: DeepMocked; let taskService: DeepMocked; let columnBoardService: DeepMocked; @@ -69,17 +66,13 @@ describe('ShareTokenUC', () => { provide: TaskCopyService, useValue: createMock(), }, - { - provide: ColumnBoardCopyService, - useValue: createMock(), - }, { provide: CourseService, useValue: createMock(), }, { - provide: BoardDoAuthorizableService, - useValue: createMock(), + provide: BoardNodeAuthorizableService, + useValue: createMock(), }, { provide: LessonService, @@ -109,10 +102,9 @@ describe('ShareTokenUC', () => { courseCopyService = module.get(CourseCopyService); lessonCopyService = module.get(LessonCopyService); taskCopyService = module.get(TaskCopyService); - columnBoardCopyService = module.get(ColumnBoardCopyService); authorizationService = module.get(AuthorizationService); courseService = module.get(CourseService); - boardDoAuthorizableService = module.get(BoardDoAuthorizableService); + boardNodeAuthorizableService = module.get(BoardNodeAuthorizableService); lessonService = module.get(LessonService); taskService = module.get(TaskService); columnBoardService = module.get(ColumnBoardService); @@ -339,13 +331,13 @@ describe('ShareTokenUC', () => { const setup = () => { const user = userFactory.buildWithId(); const columnBoard = columnBoardFactory.build(); - const boardDoAuthorizable = boardDoAuthorizableFactory.build(); + const boardNodeAuthorizable = boardNodeAuthorizableFactory.build(); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); columnBoardService.findById.mockResolvedValueOnce(columnBoard); - boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValue(boardDoAuthorizable); + boardNodeAuthorizableService.getBoardAuthorizable.mockResolvedValue(boardNodeAuthorizable); - return { user, columnBoard, boardDoAuthorizable }; + return { user, columnBoard, boardNodeAuthorizable }; }; it('should throw if the feature is not enabled', async () => { @@ -361,7 +353,7 @@ describe('ShareTokenUC', () => { }); it('should check permission for parent', async () => { - const { user, columnBoard, boardDoAuthorizable } = setup(); + const { user, columnBoard, boardNodeAuthorizable } = setup(); await uc.createShareToken(user.id, { parentId: columnBoard.id, @@ -370,7 +362,7 @@ describe('ShareTokenUC', () => { expect(authorizationService.checkPermission).toHaveBeenCalledWith( user, - boardDoAuthorizable, + boardNodeAuthorizable, AuthorizationContextBuilder.write([Permission.COURSE_EDIT]) ); }); @@ -1124,7 +1116,7 @@ describe('ShareTokenUC', () => { const { user, shareToken, course, columnBoard } = setup(); const newName = 'NewName'; await uc.importShareToken(user.id, shareToken.token, newName, course.id); - expect(columnBoardCopyService.copyColumnBoard).toHaveBeenCalledWith({ + expect(columnBoardService.copyColumnBoard).toHaveBeenCalledWith({ originalColumnBoardId: columnBoard.id, destinationExternalReference: { type: BoardExternalReferenceType.Course, id: course.id }, userId: user.id, @@ -1138,7 +1130,7 @@ describe('ShareTokenUC', () => { status: CopyStatusEnum.SUCCESS, copyEntity: columnBoard, }; - columnBoardCopyService.copyColumnBoard.mockResolvedValueOnce(status); + columnBoardService.copyColumnBoard.mockResolvedValueOnce(status); const newName = 'NewName'; const result = await uc.importShareToken(user.id, shareToken.token, newName, columnBoard.id); diff --git a/apps/server/src/modules/sharing/uc/share-token.uc.ts b/apps/server/src/modules/sharing/uc/share-token.uc.ts index 4da3ccd0199..e8e0babd7bf 100644 --- a/apps/server/src/modules/sharing/uc/share-token.uc.ts +++ b/apps/server/src/modules/sharing/uc/share-token.uc.ts @@ -1,12 +1,11 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; -import { BoardDoAuthorizableService, ColumnBoardCopyService, ColumnBoardService } from '@modules/board'; import { CopyStatus } from '@modules/copy-helper'; import { CourseCopyService, CourseService } from '@modules/learnroom'; import { LessonCopyService, LessonService } from '@modules/lesson'; import { TaskCopyService, TaskService } from '@modules/task'; import { BadRequestException, Injectable, InternalServerErrorException, NotImplementedException } from '@nestjs/common'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; +import { BoardNodeAuthorizableService, BoardExternalReferenceType, ColumnBoardService } from '@modules/board'; import { Course, User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; @@ -30,13 +29,12 @@ export class ShareTokenUC { private readonly courseCopyService: CourseCopyService, private readonly lessonCopyService: LessonCopyService, private readonly taskCopyService: TaskCopyService, - private readonly columnBoardCopyService: ColumnBoardCopyService, + private readonly columnBoardService: ColumnBoardService, private readonly courseService: CourseService, private readonly lessonService: LessonService, private readonly taskService: TaskService, - private readonly columnBoardService: ColumnBoardService, private readonly schoolService: SchoolService, - private readonly boardDoAuthorizableService: BoardDoAuthorizableService, + private readonly boardNodeAuthorizableService: BoardNodeAuthorizableService, private readonly logger: LegacyLogger ) { this.logger.setContext(ShareTokenUC.name); @@ -187,7 +185,7 @@ export class ShareTokenUC { ): Promise { await this.checkCourseWritePermission(user, courseId, Permission.COURSE_EDIT); - const copyStatus = this.columnBoardCopyService.copyColumnBoard({ + const copyStatus = this.columnBoardService.copyColumnBoard({ originalColumnBoardId, destinationExternalReference: { type: BoardExternalReferenceType.Course, id: courseId }, userId: user.id, @@ -239,11 +237,11 @@ export class ShareTokenUC { private async checkColumnBoardWritePermission(user: User, boardNodeId: EntityId, permission: Permission) { const columBoard = await this.columnBoardService.findById(boardNodeId); - const boardDoAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable(columBoard); + const boardNodeAuthorizableService = await this.boardNodeAuthorizableService.getBoardAuthorizable(columBoard); this.authorizationService.checkPermission( user, - boardDoAuthorizable, + boardNodeAuthorizableService, AuthorizationContextBuilder.write([permission]) ); } diff --git a/apps/server/src/modules/tool/common/service/common-tool-metadata.service.spec.ts b/apps/server/src/modules/tool/common/service/common-tool-metadata.service.spec.ts index 3b759ce156b..ed0fedbf61c 100644 --- a/apps/server/src/modules/tool/common/service/common-tool-metadata.service.spec.ts +++ b/apps/server/src/modules/tool/common/service/common-tool-metadata.service.spec.ts @@ -1,6 +1,6 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; -import { ContentElementService } from '@modules/board'; +import { BoardCommonToolService } from '@modules/board'; import { Test, TestingModule } from '@nestjs/testing'; import { ContextExternalToolRepo, SchoolExternalToolRepo } from '@shared/repo'; import { ContextExternalTool } from '../../context-external-tool/domain'; @@ -16,7 +16,7 @@ describe(CommonToolMetadataService.name, () => { let schoolExternalToolRepo: DeepMocked; let contextExternalToolRepo: DeepMocked; - let contentElementService: DeepMocked; + let boardCommonToolService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -31,8 +31,8 @@ describe(CommonToolMetadataService.name, () => { useValue: createMock(), }, { - provide: ContentElementService, - useValue: createMock(), + provide: BoardCommonToolService, + useValue: createMock(), }, ], }).compile(); @@ -40,7 +40,7 @@ describe(CommonToolMetadataService.name, () => { service = module.get(CommonToolMetadataService); schoolExternalToolRepo = module.get(SchoolExternalToolRepo); contextExternalToolRepo = module.get(ContextExternalToolRepo); - contentElementService = module.get(ContentElementService); + boardCommonToolService = module.get(BoardCommonToolService); }); afterAll(async () => { @@ -82,7 +82,7 @@ describe(CommonToolMetadataService.name, () => { contextExternalToolRepo.findBySchoolToolIdsAndContextType.mockResolvedValueOnce(contextExternalTools); contextExternalToolRepo.findBySchoolToolIdsAndContextType.mockResolvedValueOnce(contextExternalTools); contextExternalToolRepo.findBySchoolToolIdsAndContextType.mockResolvedValueOnce(contextExternalTools); - contentElementService.countBoardUsageForExternalTools.mockResolvedValueOnce(3); + boardCommonToolService.countBoardUsageForExternalTools.mockResolvedValueOnce(3); }; it('should return the amount of usages for all contexts', async () => { @@ -108,7 +108,7 @@ describe(CommonToolMetadataService.name, () => { contextExternalToolRepo.findBySchoolToolIdsAndContextType.mockResolvedValueOnce([]); contextExternalToolRepo.findBySchoolToolIdsAndContextType.mockResolvedValueOnce([]); contextExternalToolRepo.findBySchoolToolIdsAndContextType.mockResolvedValueOnce([]); - contentElementService.countBoardUsageForExternalTools.mockResolvedValueOnce(0); + boardCommonToolService.countBoardUsageForExternalTools.mockResolvedValueOnce(0); }; it('should return 0 usages for all contexts', async () => { @@ -135,7 +135,7 @@ describe(CommonToolMetadataService.name, () => { contextExternalToolRepo.findBySchoolToolIdsAndContextType.mockResolvedValueOnce(contextExternalTools); contextExternalToolRepo.findBySchoolToolIdsAndContextType.mockResolvedValueOnce(contextExternalTools); contextExternalToolRepo.findBySchoolToolIdsAndContextType.mockResolvedValueOnce(contextExternalTools); - contentElementService.countBoardUsageForExternalTools.mockResolvedValueOnce(3); + boardCommonToolService.countBoardUsageForExternalTools.mockResolvedValueOnce(3); }; it('should return the amount of usages for all contexts', async () => { diff --git a/apps/server/src/modules/tool/common/service/common-tool-metadata.service.ts b/apps/server/src/modules/tool/common/service/common-tool-metadata.service.ts index c4e53bf5ebb..62c2d79307a 100644 --- a/apps/server/src/modules/tool/common/service/common-tool-metadata.service.ts +++ b/apps/server/src/modules/tool/common/service/common-tool-metadata.service.ts @@ -1,4 +1,4 @@ -import { ContentElementService } from '@modules/board'; +import { BoardCommonToolService } from '@modules/board'; import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain/types'; import { ContextExternalToolRepo, SchoolExternalToolRepo } from '@shared/repo'; @@ -14,8 +14,8 @@ export class CommonToolMetadataService { constructor( private readonly schoolToolRepo: SchoolExternalToolRepo, private readonly contextToolRepo: ContextExternalToolRepo, - @Inject(forwardRef(() => ContentElementService)) - private readonly contentElementService: ContentElementService + @Inject(forwardRef(() => BoardCommonToolService)) + private readonly boardCommonToolService: BoardCommonToolService ) {} async getMetadataForExternalTool(toolId: EntityId): Promise { @@ -74,7 +74,7 @@ export class CommonToolMetadataService { ): Promise { let count = 0; if (contextType === ContextExternalToolType.BOARD_ELEMENT) { - count = await this.contentElementService.countBoardUsageForExternalTools(contextExternalTools); + count = await this.boardCommonToolService.countBoardUsageForExternalTools(contextExternalTools); } else { const contextIds: EntityId[] = contextExternalTools.map( (contextExternalTool: ContextExternalTool): EntityId => contextExternalTool.contextRef.id diff --git a/apps/server/src/modules/tool/common/uc/tool-permission-helper.spec.ts b/apps/server/src/modules/tool/common/uc/tool-permission-helper.spec.ts index eb4902b6675..fcd2ae12244 100644 --- a/apps/server/src/modules/tool/common/uc/tool-permission-helper.spec.ts +++ b/apps/server/src/modules/tool/common/uc/tool-permission-helper.spec.ts @@ -7,20 +7,15 @@ import { AuthorizationService, ForbiddenLoggableException, } from '@modules/authorization'; -import { BoardDoAuthorizableService, ContentElementService } from '@modules/board'; +import { BoardNodeAuthorizableService, BoardNodeAuthorizable, BoardNodeService } from '@modules/board'; import { CourseService } from '@modules/learnroom'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardDoAuthorizable, ExternalToolElement } from '@shared/domain/domainobject'; import { Permission } from '@shared/domain/interface'; -import { - boardDoAuthorizableFactory, - courseFactory, - externalToolElementFactory, - setupEntities, - userFactory, -} from '@shared/testing'; +import { courseFactory, setupEntities, userFactory } from '@shared/testing'; + +import { boardNodeAuthorizableFactory, externalToolElementFactory } from '@modules/board/testing'; import { ContextExternalTool, ContextRef } from '../../context-external-tool/domain'; import { contextExternalToolFactory } from '../../context-external-tool/testing'; import { schoolExternalToolFactory } from '../../school-external-tool/testing'; @@ -33,8 +28,8 @@ describe('ToolPermissionHelper', () => { let authorizationService: DeepMocked; let courseService: DeepMocked; - let contentElementService: DeepMocked; - let boardDoAuthorizableService: DeepMocked; + let boardNodeService: DeepMocked; + let boardNodeAuthorizableService: DeepMocked; beforeAll(async () => { await setupEntities(); @@ -50,12 +45,12 @@ describe('ToolPermissionHelper', () => { useValue: createMock(), }, { - provide: ContentElementService, - useValue: createMock(), + provide: BoardNodeService, + useValue: createMock(), }, { - provide: BoardDoAuthorizableService, - useValue: createMock(), + provide: BoardNodeAuthorizableService, + useValue: createMock(), }, ], }).compile(); @@ -63,8 +58,8 @@ describe('ToolPermissionHelper', () => { helper = module.get(ToolPermissionHelper); authorizationService = module.get(AuthorizationService); courseService = module.get(CourseService); - contentElementService = module.get(ContentElementService); - boardDoAuthorizableService = module.get(BoardDoAuthorizableService); + boardNodeService = module.get(BoardNodeService); + boardNodeAuthorizableService = module.get(BoardNodeAuthorizableService); }); afterAll(async () => { @@ -113,19 +108,19 @@ describe('ToolPermissionHelper', () => { describe('when a context external tool for context "board element" is given', () => { const setup = () => { const user = userFactory.buildWithId(); - const externalToolElement: ExternalToolElement = externalToolElementFactory.build(); + const externalToolElement = externalToolElementFactory.build(); const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId({ contextRef: new ContextRef({ id: externalToolElement.id, type: ToolContextType.BOARD_ELEMENT, }), }); - const board: BoardDoAuthorizable = boardDoAuthorizableFactory.build(); + const board: BoardNodeAuthorizable = boardNodeAuthorizableFactory.build(); const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_USER]); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - contentElementService.findById.mockResolvedValueOnce(externalToolElement); - boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce(board); + boardNodeService.findById.mockResolvedValueOnce(externalToolElement); + boardNodeAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce(board); return { user, @@ -149,7 +144,7 @@ describe('ToolPermissionHelper', () => { describe('when a context external tool for context "media board" is given', () => { const setup = () => { const user = userFactory.buildWithId(); - const board: BoardDoAuthorizable = boardDoAuthorizableFactory.build(); + const board: BoardNodeAuthorizable = boardNodeAuthorizableFactory.build(); const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId({ contextRef: new ContextRef({ id: board.id, @@ -159,7 +154,7 @@ describe('ToolPermissionHelper', () => { const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_USER]); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - boardDoAuthorizableService.findById.mockResolvedValueOnce(board); + boardNodeAuthorizableService.findById.mockResolvedValueOnce(board); return { user, @@ -280,18 +275,18 @@ describe('ToolPermissionHelper', () => { describe('when a school external tool for context "board element" is given', () => { const setup = () => { const user = userFactory.buildWithId(); - const externalToolElement: ExternalToolElement = externalToolElementFactory.build(); + const externalToolElement = externalToolElementFactory.build(); const schoolExternalTool = schoolExternalToolFactory.buildWithId(); const contextRef = new ContextRef({ id: externalToolElement.id, type: ToolContextType.BOARD_ELEMENT, }); - const board: BoardDoAuthorizable = boardDoAuthorizableFactory.build(); + const board: BoardNodeAuthorizable = boardNodeAuthorizableFactory.build(); const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_USER]); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - contentElementService.findById.mockResolvedValueOnce(externalToolElement); - boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce(board); + boardNodeService.findById.mockResolvedValueOnce(externalToolElement); + boardNodeAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce(board); return { user, @@ -322,7 +317,7 @@ describe('ToolPermissionHelper', () => { describe('when a school external tool for context "media board" is given', () => { const setup = () => { const user = userFactory.buildWithId(); - const board: BoardDoAuthorizable = boardDoAuthorizableFactory.build(); + const board: BoardNodeAuthorizable = boardNodeAuthorizableFactory.build(); const schoolExternalTool = schoolExternalToolFactory.buildWithId(); const contextRef = new ContextRef({ id: board.id, @@ -331,7 +326,7 @@ describe('ToolPermissionHelper', () => { const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_USER]); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - boardDoAuthorizableService.findById.mockResolvedValueOnce(board); + boardNodeAuthorizableService.findById.mockResolvedValueOnce(board); return { user, diff --git a/apps/server/src/modules/tool/common/uc/tool-permission-helper.ts b/apps/server/src/modules/tool/common/uc/tool-permission-helper.ts index 4e176e1ce85..b43f4d6cfa4 100644 --- a/apps/server/src/modules/tool/common/uc/tool-permission-helper.ts +++ b/apps/server/src/modules/tool/common/uc/tool-permission-helper.ts @@ -1,9 +1,8 @@ import { AuthorizationContext, AuthorizationService, ForbiddenLoggableException } from '@modules/authorization'; import { AuthorizableReferenceType } from '@modules/authorization/domain'; -import { BoardDoAuthorizableService, ContentElementService } from '@modules/board'; +import { BoardNodeAuthorizable, BoardNodeAuthorizableService, BoardNodeService } from '@modules/board'; import { CourseService } from '@modules/learnroom'; import { forwardRef, Inject, Injectable } from '@nestjs/common'; -import { BoardDoAuthorizable } from '@shared/domain/domainobject'; import { Course, User } from '@shared/domain/entity'; import { EntityId } from '@shared/domain/types'; import { ContextExternalTool } from '../../context-external-tool/domain'; @@ -15,8 +14,8 @@ export class ToolPermissionHelper { constructor( @Inject(forwardRef(() => AuthorizationService)) private readonly authorizationService: AuthorizationService, private readonly courseService: CourseService, - private readonly boardElementService: ContentElementService, - private readonly boardService: BoardDoAuthorizableService + private readonly boardNodeService: BoardNodeService, + private readonly boardService: BoardNodeAuthorizableService ) {} public async ensureContextPermissionsForSchool( @@ -60,15 +59,13 @@ export class ToolPermissionHelper { break; } case ToolContextType.BOARD_ELEMENT: { - const boardElement = await this.boardElementService.findById(contextId); - const board: BoardDoAuthorizable = await this.boardService.getBoardAuthorizable(boardElement); - + const boardElement = await this.boardNodeService.findContentElementById(contextId); + const board: BoardNodeAuthorizable = await this.boardService.getBoardAuthorizable(boardElement); this.authorizationService.checkPermission(user, board, context); break; } case ToolContextType.MEDIA_BOARD: { - const board: BoardDoAuthorizable = await this.boardService.findById(contextId); - + const board: BoardNodeAuthorizable = await this.boardService.findById(contextId); this.authorizationService.checkPermission(user, board, context); break; } diff --git a/apps/server/src/modules/tool/external-tool/controller/api-test/tool.api.spec.ts b/apps/server/src/modules/tool/external-tool/controller/api-test/tool.api.spec.ts index a986f983ce4..6361294bd08 100644 --- a/apps/server/src/modules/tool/external-tool/controller/api-test/tool.api.spec.ts +++ b/apps/server/src/modules/tool/external-tool/controller/api-test/tool.api.spec.ts @@ -4,16 +4,10 @@ import { ServerTestModule } from '@modules/server'; import { schoolExternalToolEntityFactory } from '@modules/tool/school-external-tool/testing'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { ColumnBoardNode, ExternalToolElementNodeEntity, SchoolEntity } from '@shared/domain/entity'; +import { SchoolEntity } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; -import { - cleanupCollections, - columnBoardNodeFactory, - externalToolElementNodeFactory, - schoolEntityFactory, - TestApiClient, - UserAndAccountTestFactory, -} from '@shared/testing'; +import { cleanupCollections, schoolEntityFactory, TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; +import { columnBoardEntityFactory, externalToolElementEntityFactory } from '@src/modules/board/testing'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { Response } from 'supertest'; @@ -771,9 +765,9 @@ describe('ToolController (API)', () => { describe('when externalToolId is given ', () => { const setup = async () => { const toolId: string = new ObjectId().toHexString(); - const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId(undefined, toolId); + const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.build({ id: toolId }); - const school: SchoolEntity = schoolEntityFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.build(); const schoolExternalToolEntitys: SchoolExternalToolEntity[] = schoolExternalToolEntityFactory.buildList(2, { tool: externalToolEntity, school, @@ -785,20 +779,16 @@ describe('ToolController (API)', () => { contextId: new ObjectId().toHexString(), }); - const boardTools: ContextExternalToolEntity[] = contextExternalToolEntityFactory.buildList(2, { + const boardTools: ContextExternalToolEntity[] = contextExternalToolEntityFactory.buildListWithId(2, { schoolTool: schoolExternalToolEntitys[1], contextType: ContextExternalToolType.BOARD_ELEMENT, contextId: new ObjectId().toHexString(), }); - const board: ColumnBoardNode = columnBoardNodeFactory.buildWithId(); - const externalToolElements: ExternalToolElementNodeEntity[] = externalToolElementNodeFactory.buildListWithId( - 2, - { - contextExternalTool: boardTools[0], - parent: board, - } - ); + const board = columnBoardEntityFactory.build(); + const externalToolElements = externalToolElementEntityFactory + .withParent(board) + .buildList(2, { contextExternalToolId: boardTools[0].id }); const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({}, [Permission.TOOL_ADMIN]); await em.persistAndFlush([ diff --git a/apps/server/src/modules/tool/school-external-tool/controller/api-test/tool-school.api.spec.ts b/apps/server/src/modules/tool/school-external-tool/controller/api-test/tool-school.api.spec.ts index a1bd904344e..1b62a7128fc 100644 --- a/apps/server/src/modules/tool/school-external-tool/controller/api-test/tool-school.api.spec.ts +++ b/apps/server/src/modules/tool/school-external-tool/controller/api-test/tool-school.api.spec.ts @@ -3,18 +3,12 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { ServerTestModule } from '@modules/server'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { ColumnBoardNode, ExternalToolElementNodeEntity, SchoolEntity, User } from '@shared/domain/entity'; +import { SchoolEntity, User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; -import { - columnBoardNodeFactory, - externalToolElementNodeFactory, - schoolEntityFactory, - TestApiClient, - UserAndAccountTestFactory, - userFactory, -} from '@shared/testing'; +import { schoolEntityFactory, TestApiClient, UserAndAccountTestFactory, userFactory } from '@shared/testing'; import { AccountEntity } from '@src/modules/account/domain/entity/account.entity'; import { accountFactory } from '@src/modules/account/testing'; +import { columnBoardEntityFactory, externalToolElementEntityFactory } from '@src/modules/board/testing'; import { ContextExternalToolEntity, ContextExternalToolType } from '../../../context-external-tool/entity'; import { contextExternalToolEntityFactory } from '../../../context-external-tool/testing'; import { CustomParameterScope, CustomParameterType, ExternalToolEntity } from '../../../external-tool/entity'; @@ -544,21 +538,20 @@ describe('ToolSchoolController (API)', () => { contextId: new ObjectId().toHexString(), }); - const boardExternalToolEntitys: ContextExternalToolEntity[] = contextExternalToolEntityFactory.buildList(2, { - schoolTool: schoolExternalToolEntity, - contextType: ContextExternalToolType.BOARD_ELEMENT, - contextId: new ObjectId().toHexString(), - }); - - const board: ColumnBoardNode = columnBoardNodeFactory.buildWithId(); - const externalToolElements: ExternalToolElementNodeEntity[] = externalToolElementNodeFactory.buildListWithId( + const boardExternalToolEntitys: ContextExternalToolEntity[] = contextExternalToolEntityFactory.buildListWithId( 2, { - contextExternalTool: boardExternalToolEntitys[0], - parent: board, + schoolTool: schoolExternalToolEntity, + contextType: ContextExternalToolType.BOARD_ELEMENT, + contextId: new ObjectId().toHexString(), } ); + const board = columnBoardEntityFactory.build(); + const externalToolElements = externalToolElementEntityFactory.withParent(board).buildList(2, { + contextExternalToolId: boardExternalToolEntitys[0].id, + }); + const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({ school }, [ Permission.SCHOOL_TOOL_ADMIN, ]); diff --git a/apps/server/src/modules/tool/tool-launch/controller/api-test/tool-launch.controller.api.spec.ts b/apps/server/src/modules/tool/tool-launch/controller/api-test/tool-launch.controller.api.spec.ts index 3b0d0387bb4..f0e3aec8e24 100644 --- a/apps/server/src/modules/tool/tool-launch/controller/api-test/tool-launch.controller.api.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/controller/api-test/tool-launch.controller.api.spec.ts @@ -3,16 +3,11 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { ServerTestModule } from '@modules/server'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; -import { Course, MediaBoardNode, SchoolEntity } from '@shared/domain/entity'; +import { Course, SchoolEntity } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; -import { - courseFactory, - schoolEntityFactory, - TestApiClient, - UserAndAccountTestFactory, - mediaBoardNodeFactory, -} from '@shared/testing'; +import { courseFactory, schoolEntityFactory, TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; +import { BoardExternalReferenceType } from '@src/modules/board'; +import { mediaBoardEntityFactory } from '@src/modules/board/testing'; import { Response } from 'supertest'; import { CustomParameterLocation, @@ -363,7 +358,7 @@ describe('ToolLaunchController (API)', () => { const { teacherUser, teacherAccount } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.CONTEXT_TOOL_USER, ]); - const mediaBoard: MediaBoardNode = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: teacherUser.id, type: BoardExternalReferenceType.User }, }); @@ -433,7 +428,7 @@ describe('ToolLaunchController (API)', () => { const { teacherUser, teacherAccount } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.CONTEXT_TOOL_USER, ]); - const mediaBoard: MediaBoardNode = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: teacherUser.id, type: BoardExternalReferenceType.User }, }); @@ -502,7 +497,7 @@ describe('ToolLaunchController (API)', () => { const { teacherUser, teacherAccount } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.CONTEXT_TOOL_USER, ]); - const mediaBoard: MediaBoardNode = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: teacherUser.id, type: BoardExternalReferenceType.User }, }); @@ -568,7 +563,7 @@ describe('ToolLaunchController (API)', () => { const { teacherUser, teacherAccount } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.CONTEXT_TOOL_USER, ]); - const mediaBoard: MediaBoardNode = mediaBoardNodeFactory.buildWithId({ + const mediaBoard = mediaBoardEntityFactory.build({ context: { id: teacherUser.id, type: BoardExternalReferenceType.User }, }); diff --git a/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-name.strategy.spec.ts b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-name.strategy.spec.ts index 8554f66ad80..c4893063bb8 100644 --- a/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-name.strategy.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-name.strategy.spec.ts @@ -1,11 +1,11 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; -import { ColumnBoardService, ContentElementService } from '@modules/board'; +import { BoardCommonToolService, BoardExternalReferenceType, ColumnBoard, BoardNodeService } from '@modules/board'; import { CourseService } from '@modules/learnroom'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardExternalReferenceType, ColumnBoard, ExternalToolElement } from '@shared/domain/domainobject'; import { Course } from '@shared/domain/entity'; -import { columnBoardFactory, courseFactory, externalToolElementFactory, setupEntities } from '@shared/testing'; +import { courseFactory, setupEntities } from '@shared/testing'; +import { columnBoardFactory, externalToolElementFactory } from '@modules/board/testing'; import { ToolContextType } from '../../../common/enum'; import { ContextExternalTool } from '../../../context-external-tool/domain'; import { contextExternalToolFactory } from '../../../context-external-tool/testing'; @@ -19,8 +19,8 @@ describe(AutoContextNameStrategy.name, () => { let strategy: AutoContextNameStrategy; let courseService: DeepMocked; - let contentElementService: DeepMocked; - let columnBoardService: DeepMocked; + let boardCommonToolService: DeepMocked; + let boardNodeService: DeepMocked; beforeAll(async () => { await setupEntities(); @@ -33,20 +33,20 @@ describe(AutoContextNameStrategy.name, () => { useValue: createMock(), }, { - provide: ContentElementService, - useValue: createMock(), + provide: BoardCommonToolService, + useValue: createMock(), }, { - provide: ColumnBoardService, - useValue: createMock(), + provide: BoardNodeService, + useValue: createMock(), }, ], }).compile(); strategy = module.get(AutoContextNameStrategy); courseService = module.get(CourseService); - contentElementService = module.get(ContentElementService); - columnBoardService = module.get(ColumnBoardService); + boardCommonToolService = module.get(BoardCommonToolService); + boardNodeService = module.get(BoardNodeService); }); afterAll(async () => { @@ -109,7 +109,7 @@ describe(AutoContextNameStrategy.name, () => { name: 'testName', }); - const externalToolElement: ExternalToolElement = externalToolElementFactory.build(); + const externalToolElement = externalToolElementFactory.build(); const columnBoard: ColumnBoard = columnBoardFactory.build({ context: { @@ -119,8 +119,8 @@ describe(AutoContextNameStrategy.name, () => { }); courseService.findById.mockResolvedValue(course); - contentElementService.findById.mockResolvedValue(externalToolElement); - columnBoardService.findByDescendant.mockResolvedValue(columnBoard); + boardNodeService.findContentElementById.mockResolvedValue(externalToolElement); + boardCommonToolService.findByDescendant.mockResolvedValue(columnBoard); return { schoolExternalTool, @@ -149,7 +149,7 @@ describe(AutoContextNameStrategy.name, () => { }, }); - const externalToolElement: ExternalToolElement = externalToolElementFactory.build(); + const externalToolElement = externalToolElementFactory.build(); const columnBoard: ColumnBoard = columnBoardFactory.build({ context: { @@ -158,8 +158,8 @@ describe(AutoContextNameStrategy.name, () => { }, }); - contentElementService.findById.mockResolvedValue(externalToolElement); - columnBoardService.findByDescendant.mockResolvedValue(columnBoard); + boardNodeService.findContentElementById.mockResolvedValue(externalToolElement); + boardCommonToolService.findByDescendant.mockResolvedValue(columnBoard); return { schoolExternalTool, diff --git a/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-name.strategy.ts b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-name.strategy.ts index 2931c662e9c..0cf81a0aa51 100644 --- a/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-name.strategy.ts +++ b/apps/server/src/modules/tool/tool-launch/service/auto-parameter-strategy/auto-context-name.strategy.ts @@ -1,7 +1,12 @@ -import { ColumnBoardService, ContentElementService } from '@modules/board'; +import { + BoardCommonToolService, + BoardExternalReferenceType, + BoardNodeService, + ColumnBoard, + MediaBoard, +} from '@modules/board'; import { CourseService } from '@modules/learnroom'; import { Injectable } from '@nestjs/common'; -import { AnyContentElementDo, BoardExternalReferenceType, ColumnBoard, MediaBoard } from '@shared/domain/domainobject'; import { Course } from '@shared/domain/entity'; import { EntityId } from '@shared/domain/types'; @@ -15,8 +20,8 @@ import { AutoParameterStrategy } from './auto-parameter.strategy'; export class AutoContextNameStrategy implements AutoParameterStrategy { constructor( private readonly courseService: CourseService, - private readonly contentElementService: ContentElementService, - private readonly columnBoardService: ColumnBoardService + private readonly boardCommonToolService: BoardCommonToolService, + private readonly boardNodeService: BoardNodeService ) {} async getValue( @@ -49,9 +54,9 @@ export class AutoContextNameStrategy implements AutoParameterStrategy { } private async getBoardValue(elementId: EntityId): Promise { - const element: AnyContentElementDo = await this.contentElementService.findById(elementId); + const element = await this.boardNodeService.findContentElementById(elementId); - const board: ColumnBoard | MediaBoard = await this.columnBoardService.findByDescendant(element); + const board: ColumnBoard | MediaBoard = await this.boardCommonToolService.findByDescendant(element); if (board.context.type === BoardExternalReferenceType.Course) { const courseName: string = await this.getCourseValue(board.context.id); diff --git a/apps/server/src/shared/domain/domainobject/board/board-composite.do.spec.ts b/apps/server/src/shared/domain/domainobject/board/board-composite.do.spec.ts deleted file mode 100644 index 17fde5a10b0..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/board-composite.do.spec.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { ObjectId } from '@mikro-orm/mongodb'; -import { BoardComposite, BoardCompositeProps } from './board-composite.do'; -import { AnyBoardDo } from './types'; - -class BoardObject extends BoardComposite { - isAllowedAsChild(): boolean { - return true; - } - - accept(): void {} - - async acceptAsync(): Promise { - await Promise.resolve(); - } -} - -class SecondBoardObject extends BoardComposite { - isAllowedAsChild(): boolean { - return true; - } - - accept(): void {} - - async acceptAsync(): Promise { - await Promise.resolve(); - } -} - -const buildBoardObject = (): AnyBoardDo => - new BoardObject({ - id: new ObjectId().toHexString(), - children: [], - createdAt: new Date(), - updatedAt: new Date(), - }) as unknown as AnyBoardDo; - -const buildSecondBoardObject = (): AnyBoardDo => - new SecondBoardObject({ - id: new ObjectId().toHexString(), - children: [ - new SecondBoardObject({ - id: new ObjectId().toHexString(), - children: [], - createdAt: new Date(), - updatedAt: new Date(), - }) as unknown as AnyBoardDo, - ], - createdAt: new Date(), - updatedAt: new Date(), - }) as unknown as AnyBoardDo; - -describe(`${BoardComposite.name}`, () => { - const setup = () => { - const parent = buildBoardObject(); - parent.addChild(buildBoardObject()); - parent.addChild(buildBoardObject()); - parent.addChild(buildBoardObject()); - parent.addChild(buildSecondBoardObject()); - - return { parent, children: parent.children }; - }; - - describe('removeChild', () => { - it('should remove the child', () => { - const { parent, children } = setup(); - const expectedChildren = [children[0], children[2], children[3]]; - - parent.removeChild(children[1]); - - expect(parent.children).toEqual(expectedChildren); - }); - }); - - describe('addChild', () => { - it('should add the child at the requested position', () => { - const { parent, children } = setup(); - const extraChild = buildBoardObject(); - const expectedChildren = [children[0], extraChild, children[1], children[2], children[3]]; - - parent.addChild(extraChild, 1); - - expect(children).toEqual(expectedChildren); - }); - - describe('when position is not given', () => { - it('should append the child', () => { - const { parent, children } = setup(); - const extraChild = buildBoardObject(); - const expectedChildren = [...children, extraChild]; - - parent.addChild(extraChild); - - expect(children).toEqual(expectedChildren); - }); - }); - - describe('when position = 0', () => { - it('should prepend the child', () => { - const { parent, children } = setup(); - const extraChild = buildBoardObject(); - const expectedChildren = [extraChild, ...children]; - - parent.addChild(extraChild, 0); - - expect(children).toEqual(expectedChildren); - }); - }); - - describe('when position < 0', () => { - it('should throw an error', () => { - const { parent } = setup(); - const extraChild = buildBoardObject(); - - expect(() => parent.addChild(extraChild, -1)).toThrow(); - }); - }); - - describe('when position is too large', () => { - it('should throw an error', () => { - const { parent } = setup(); - const extraChild = buildBoardObject(); - - expect(() => parent.addChild(extraChild, 42)).toThrow(); - }); - }); - - describe('when the object is not allowed as a child', () => { - it('should throw an error', () => { - const { parent } = setup(); - const extraChild = buildBoardObject(); - - jest.spyOn(parent, 'isAllowedAsChild').mockReturnValue(false); - - expect(() => parent.addChild(extraChild, 1)).toThrow(); - }); - }); - - describe('when the object is already a child', () => { - it('should throw an error', () => { - const { parent, children } = setup(); - - expect(() => parent.addChild(children[0])).toThrow(); - }); - }); - }); - - describe('getChildrenOfType', () => { - it('should return the children of the given type', () => { - const { parent, children } = setup(); - const expectedChildren = [children[3].children[0], children[3]]; - - const result = parent.getChildrenOfType( - SecondBoardObject as unknown as new (...args: AnyBoardDo[]) => AnyBoardDo - ); - - expect(result).toEqual(expectedChildren); - }); - }); -}); diff --git a/apps/server/src/shared/domain/domainobject/board/board-composite.do.ts b/apps/server/src/shared/domain/domainobject/board/board-composite.do.ts deleted file mode 100644 index 754807a14af..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/board-composite.do.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { BadRequestException, ForbiddenException } from '@nestjs/common'; -import { DomainObject } from '@shared/domain/domain-object'; // fix import if it is avaible -import { EntityId } from '@shared/domain/types'; -import type { AnyBoardDo, BoardCompositeVisitor, BoardCompositeVisitorAsync } from './types'; - -export abstract class BoardComposite extends DomainObject { - get children(): AnyBoardDo[] { - return this.props.children ?? []; - } - - get createdAt(): Date { - return this.props.createdAt; - } - - get updatedAt(): Date { - return this.props.updatedAt; - } - - addChild(child: AnyBoardDo, position?: number): void { - if (!this.isAllowedAsChild(child)) { - throw new ForbiddenException(`Cannot add child of type '${child.constructor.name}'`); - } - position = position ?? this.children.length; - if (position < 0 || position > this.children.length) { - throw new BadRequestException(`Invalid child position '${position}'`); - } - if (this.hasChild(child)) { - throw new BadRequestException(`Cannot add existing child id='${child.id}'`); - } - this.children.splice(position, 0, child); - } - - abstract isAllowedAsChild(domainObject: AnyBoardDo): boolean; - - removeChild(child: AnyBoardDo): void { - this.props.children = this.children.filter((ch) => ch.id !== child.id); - } - - hasChild(child: AnyBoardDo): boolean { - // TODO check by object identity instead of id - const exists = this.children.some((obj) => obj.id === child.id); - return exists; - } - - getChildrenOfType(type: new (...args: U[]) => U): U[] { - const childrenOfType: U[] = []; - for (const child of this.children) { - if (child.children) { - childrenOfType.push(...child.getChildrenOfType(type)); - } - if (child instanceof type) { - childrenOfType.push(child); - } - } - - return childrenOfType; - } - - abstract accept(visitor: BoardCompositeVisitor): void; - - abstract acceptAsync(visitor: BoardCompositeVisitorAsync): Promise; -} - -export interface BoardCompositeProps { - id: EntityId; - children?: AnyBoardDo[]; - createdAt: Date; - updatedAt: Date; -} diff --git a/apps/server/src/shared/domain/domainobject/board/card.do.spec.ts b/apps/server/src/shared/domain/domainobject/board/card.do.spec.ts deleted file mode 100644 index 7ee0fd5b9b5..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/card.do.spec.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { createMock } from '@golevelup/ts-jest'; -import { - cardFactory, - externalToolElementFactory, - richTextElementFactory, - submissionContainerElementFactory, -} from '@shared/testing'; -import { Card } from './card.do'; -import { BoardCompositeVisitor, BoardCompositeVisitorAsync } from './types'; - -describe(Card.name, () => { - describe('isAllowedAsChild', () => { - it('should allow rich text element objects', () => { - const card = cardFactory.build(); - const richTextElement = richTextElementFactory.build(); - expect(card.isAllowedAsChild(richTextElement)).toBe(true); - }); - - it('should allow submission container element objects', () => { - const card = cardFactory.build(); - const submissionContainerElement = submissionContainerElementFactory.build(); - expect(card.isAllowedAsChild(submissionContainerElement)).toBe(true); - }); - - it('should allow external tool element objects', () => { - const card = cardFactory.build(); - const externalToolElement = externalToolElementFactory.build(); - expect(card.isAllowedAsChild(externalToolElement)).toBe(true); - }); - }); - - describe('accept', () => { - it('should call the right visitor method', () => { - const visitor = createMock(); - const card = cardFactory.build(); - - card.accept(visitor); - - expect(visitor.visitCard).toHaveBeenCalledWith(card); - }); - }); - - describe('acceptAsync', () => { - it('should call the right async visitor method', async () => { - const visitor = createMock(); - const card = cardFactory.build(); - - await card.acceptAsync(visitor); - - expect(visitor.visitCardAsync).toHaveBeenCalledWith(card); - }); - }); - - describe('set title', () => { - it('should set the title property', () => { - const card = cardFactory.build({ title: 'card #1' }); - card.title = 'card #2'; - expect(card.title).toEqual('card #2'); - }); - }); - - describe('set height', () => { - it('should set the height property', () => { - const card = cardFactory.build({ height: 10 }); - card.height = 42; - expect(card.height).toEqual(42); - }); - }); -}); diff --git a/apps/server/src/shared/domain/domainobject/board/card.do.ts b/apps/server/src/shared/domain/domainobject/board/card.do.ts deleted file mode 100644 index 8635e2db8d1..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/card.do.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { DrawingElement } from '@shared/domain/domainobject/board/drawing-element.do'; -import { BoardComposite, BoardCompositeProps } from './board-composite.do'; -import { CollaborativeTextEditorElement } from './collaborative-text-editor-element.do'; -import { ExternalToolElement } from './external-tool-element.do'; -import { FileElement } from './file-element.do'; -import { LinkElement } from './link-element.do'; -import { RichTextElement } from './rich-text-element.do'; -import { SubmissionContainerElement } from './submission-container-element.do'; -import type { AnyBoardDo, BoardCompositeVisitor, BoardCompositeVisitorAsync } from './types'; - -export class Card extends BoardComposite { - get title(): string { - return this.props.title; - } - - set title(title: string) { - this.props.title = title; - } - - get height(): number { - return this.props.height; - } - - set height(height: number) { - this.props.height = height; - } - - isAllowedAsChild(domainObject: AnyBoardDo): boolean { - const allowed = - domainObject instanceof FileElement || - domainObject instanceof DrawingElement || - domainObject instanceof LinkElement || - domainObject instanceof RichTextElement || - domainObject instanceof SubmissionContainerElement || - domainObject instanceof ExternalToolElement || - domainObject instanceof CollaborativeTextEditorElement; - return allowed; - } - - accept(visitor: BoardCompositeVisitor): void { - visitor.visitCard(this); - } - - async acceptAsync(visitor: BoardCompositeVisitorAsync): Promise { - await visitor.visitCardAsync(this); - } -} - -export interface CardProps extends BoardCompositeProps { - title: string; - height: number; -} - -export type CardInitProps = Omit; - -export function isCard(reference: unknown): reference is Card { - return reference instanceof Card; -} diff --git a/apps/server/src/shared/domain/domainobject/board/collaborative-text-editor-element.do.spec.ts b/apps/server/src/shared/domain/domainobject/board/collaborative-text-editor-element.do.spec.ts deleted file mode 100644 index 379159ca791..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/collaborative-text-editor-element.do.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { createMock } from '@golevelup/ts-jest'; -import { collaborativeTextEditorElementFactory } from '@shared/testing'; -import { - CollaborativeTextEditorElement, - isCollaborativeTextEditorElement, -} from './collaborative-text-editor-element.do'; -import { BoardCompositeVisitor, BoardCompositeVisitorAsync } from './types'; - -describe(CollaborativeTextEditorElement.name, () => { - describe('isAllowedAsChild', () => { - it('should return false', () => { - const collaborativeTextEditorElement = collaborativeTextEditorElementFactory.build(); - - expect(collaborativeTextEditorElement.isAllowedAsChild()).toBe(false); - }); - }); - - describe('when trying to add a child to a CollaborativeTextEditorElement', () => { - it('should throw an error ', () => { - const collaborativeTextEditorElement = collaborativeTextEditorElementFactory.build(); - const collaborativeTextEditorElementChild = collaborativeTextEditorElementFactory.build(); - - expect(() => collaborativeTextEditorElement.addChild(collaborativeTextEditorElementChild)).toThrow(); - }); - }); - - describe('accept', () => { - it('should call the right visitor method', () => { - const visitor = createMock(); - const collaborativeTextEditorElement = collaborativeTextEditorElementFactory.build(); - - collaborativeTextEditorElement.accept(visitor); - - expect(visitor.visitCollaborativeTextEditorElement).toHaveBeenCalledWith(collaborativeTextEditorElement); - }); - }); - - describe('acceptAsync', () => { - it('should call the right async visitor method', async () => { - const visitor = createMock(); - const collaborativeTextEditorElement = collaborativeTextEditorElementFactory.build(); - - await collaborativeTextEditorElement.acceptAsync(visitor); - - expect(visitor.visitCollaborativeTextEditorElementAsync).toHaveBeenCalledWith(collaborativeTextEditorElement); - }); - }); - - describe('isCollaborativeTextEditorElement', () => { - describe('when element is collaborative text editor element', () => { - it('should return true', () => { - const collaborativeTextEditorElement = collaborativeTextEditorElementFactory.build(); - - expect(isCollaborativeTextEditorElement(collaborativeTextEditorElement)).toBe(true); - }); - }); - - describe('when element is not collaborative text editor element', () => { - it('should return false', () => { - const notCollaborativeTextEditorElement = {}; - - expect(isCollaborativeTextEditorElement(notCollaborativeTextEditorElement)).toBe(false); - }); - }); - }); -}); diff --git a/apps/server/src/shared/domain/domainobject/board/collaborative-text-editor-element.do.ts b/apps/server/src/shared/domain/domainobject/board/collaborative-text-editor-element.do.ts deleted file mode 100644 index 9d939f1746c..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/collaborative-text-editor-element.do.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { BoardComposite, BoardCompositeProps } from './board-composite.do'; -import type { BoardCompositeVisitor, BoardCompositeVisitorAsync } from './types'; - -export class CollaborativeTextEditorElement extends BoardComposite { - isAllowedAsChild(): boolean { - return false; - } - - accept(visitor: BoardCompositeVisitor): void { - visitor.visitCollaborativeTextEditorElement(this); - } - - async acceptAsync(visitor: BoardCompositeVisitorAsync): Promise { - await visitor.visitCollaborativeTextEditorElementAsync(this); - } -} - -export function isCollaborativeTextEditorElement(reference: unknown): reference is CollaborativeTextEditorElement { - return reference instanceof CollaborativeTextEditorElement; -} diff --git a/apps/server/src/shared/domain/domainobject/board/column-board.do.spec.ts b/apps/server/src/shared/domain/domainobject/board/column-board.do.spec.ts deleted file mode 100644 index c56f9efa787..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/column-board.do.spec.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { createMock } from '@golevelup/ts-jest'; -import { columnBoardFactory, columnFactory } from '@shared/testing'; -import { ObjectId } from '@mikro-orm/mongodb'; -import { ColumnBoard } from './column-board.do'; -import { BoardCompositeVisitor, BoardCompositeVisitorAsync, BoardExternalReferenceType } from './types'; - -describe(ColumnBoard.name, () => { - describe('isAllowedAsChild', () => { - it('should allow column objects', () => { - const columnBoard = columnBoardFactory.build(); - const column = columnFactory.build(); - expect(columnBoard.isAllowedAsChild(column)).toBe(true); - }); - }); - - describe('accept', () => { - it('should call the right visitor method', () => { - const visitor = createMock(); - const columnBoard = columnBoardFactory.build(); - - columnBoard.accept(visitor); - - expect(visitor.visitColumnBoard).toHaveBeenCalledWith(columnBoard); - }); - }); - - describe('acceptAsync', () => { - it('should call the right async visitor method', async () => { - const visitor = createMock(); - const columnBoard = columnBoardFactory.build(); - - await columnBoard.acceptAsync(visitor); - - expect(visitor.visitColumnBoardAsync).toHaveBeenCalledWith(columnBoard); - }); - }); - - describe('set context', () => { - it('should store context', () => { - const columnBoard = columnBoardFactory.build(); - - const context = { type: BoardExternalReferenceType.Course, id: new ObjectId().toHexString() }; - columnBoard.context = { ...context }; - - expect(columnBoard.context).toEqual(context); - }); - }); - - describe('set isVisible', () => { - it('should store isVisible', () => { - const columnBoard = columnBoardFactory.build(); - - columnBoard.isVisible = true; - - expect(columnBoard.isVisible).toBe(true); - }); - }); -}); diff --git a/apps/server/src/shared/domain/domainobject/board/column-board.do.ts b/apps/server/src/shared/domain/domainobject/board/column-board.do.ts deleted file mode 100644 index 7115e9a3c2c..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/column-board.do.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { BoardComposite, BoardCompositeProps } from './board-composite.do'; -import { Column } from './column.do'; -import type { AnyBoardDo, BoardCompositeVisitor, BoardCompositeVisitorAsync, BoardExternalReference } from './types'; -import { BoardLayout } from './types/board-layout.enum'; - -export class ColumnBoard extends BoardComposite { - get title(): string { - return this.props.title; - } - - set title(title: string) { - this.props.title = title; - } - - get context(): BoardExternalReference { - return this.props.context; - } - - set context(context: BoardExternalReference) { - this.props.context = context; - } - - get isVisible(): boolean { - return this.props.isVisible; - } - - set isVisible(isVisible: boolean) { - this.props.isVisible = isVisible; - } - - get layout(): BoardLayout { - return this.props.layout; - } - - isAllowedAsChild(domainObject: AnyBoardDo): boolean { - const allowed = domainObject instanceof Column; - return allowed; - } - - accept(visitor: BoardCompositeVisitor): void { - visitor.visitColumnBoard(this); - } - - async acceptAsync(visitor: BoardCompositeVisitorAsync): Promise { - await visitor.visitColumnBoardAsync(this); - } -} - -export interface ColumnBoardProps extends BoardCompositeProps { - title: string; - context: BoardExternalReference; - isVisible: boolean; - layout: BoardLayout; -} - -export function isColumnBoard(reference: unknown): reference is ColumnBoard { - return reference instanceof ColumnBoard; -} diff --git a/apps/server/src/shared/domain/domainobject/board/column.do.spec.ts b/apps/server/src/shared/domain/domainobject/board/column.do.spec.ts deleted file mode 100644 index 617023c4452..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/column.do.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { createMock } from '@golevelup/ts-jest'; -import { cardFactory, columnFactory } from '@shared/testing'; -import { Column } from './column.do'; -import { BoardCompositeVisitor, BoardCompositeVisitorAsync } from './types'; - -describe(Column.name, () => { - describe('isAllowedAsChild', () => { - it('should allow card objects', () => { - const column = columnFactory.build(); - const card = cardFactory.build(); - expect(column.isAllowedAsChild(card)).toBe(true); - }); - }); - - describe('accept', () => { - it('should call the right visitor method', () => { - const visitor = createMock(); - const column = columnFactory.build(); - - column.accept(visitor); - - expect(visitor.visitColumn).toHaveBeenCalledWith(column); - }); - }); - - describe('acceptAsync', () => { - it('should call the right async visitor method', async () => { - const visitor = createMock(); - const column = columnFactory.build(); - - await column.acceptAsync(visitor); - - expect(visitor.visitColumnAsync).toHaveBeenCalledWith(column); - }); - }); -}); diff --git a/apps/server/src/shared/domain/domainobject/board/column.do.ts b/apps/server/src/shared/domain/domainobject/board/column.do.ts deleted file mode 100644 index 0b736f43ecc..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/column.do.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { BoardComposite, BoardCompositeProps } from './board-composite.do'; -import { Card } from './card.do'; -import type { AnyBoardDo, BoardCompositeVisitor, BoardCompositeVisitorAsync } from './types'; - -export class Column extends BoardComposite { - get title(): string { - return this.props.title; - } - - set title(title: string) { - this.props.title = title; - } - - isAllowedAsChild(domainObject: AnyBoardDo): boolean { - const allowed = domainObject instanceof Card; - return allowed; - } - - accept(visitor: BoardCompositeVisitor): void { - visitor.visitColumn(this); - } - - async acceptAsync(visitor: BoardCompositeVisitorAsync): Promise { - await visitor.visitColumnAsync(this); - } -} - -export interface ColumnProps extends BoardCompositeProps { - title: string; -} - -export type ColumnInitProps = Omit; - -export function isColumn(reference: unknown): reference is Column { - return reference instanceof Column; -} diff --git a/apps/server/src/shared/domain/domainobject/board/content-element.factory.spec.ts b/apps/server/src/shared/domain/domainobject/board/content-element.factory.spec.ts deleted file mode 100644 index 352bbaaa293..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/content-element.factory.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { NotImplementedException } from '@nestjs/common'; -import { DrawingElement } from '@shared/domain/domainobject/board/drawing-element.do'; -import { ContentElementFactory } from './content-element.factory'; -import { ExternalToolElement } from './external-tool-element.do'; -import { FileElement } from './file-element.do'; -import { RichTextElement } from './rich-text-element.do'; -import { SubmissionContainerElement } from './submission-container-element.do'; -import { ContentElementType } from './types'; - -describe(ContentElementFactory.name, () => { - describe('build', () => { - const setup = () => { - const contentElementFactory = new ContentElementFactory(); - - return { contentElementFactory }; - }; - - it('should return element of FILE', () => { - const { contentElementFactory } = setup(); - - const element = contentElementFactory.build(ContentElementType.FILE); - - expect(element).toBeInstanceOf(FileElement); - }); - - it('should return element of RICH_TEXT', () => { - const { contentElementFactory } = setup(); - - const element = contentElementFactory.build(ContentElementType.RICH_TEXT); - - expect(element).toBeInstanceOf(RichTextElement); - }); - - it('should return element of DRAWING', () => { - const { contentElementFactory } = setup(); - - const element = contentElementFactory.build(ContentElementType.DRAWING); - - expect(element).toBeInstanceOf(DrawingElement); - }); - - it('should return element of SUBMISSION_CONTAINER', () => { - const { contentElementFactory } = setup(); - - const element = contentElementFactory.build(ContentElementType.SUBMISSION_CONTAINER); - - expect(element).toBeInstanceOf(SubmissionContainerElement); - }); - - it('should return element of EXTERNAL_TOOL', () => { - const { contentElementFactory } = setup(); - - const element = contentElementFactory.build(ContentElementType.EXTERNAL_TOOL); - - expect(element).toBeInstanceOf(ExternalToolElement); - }); - - it('should throw NotImplementedException', () => { - const { contentElementFactory } = setup(); - - // @ts-expect-error check unknown type - expect(() => contentElementFactory.build('UNKNOWN')).toThrow(NotImplementedException); - }); - }); -}); diff --git a/apps/server/src/shared/domain/domainobject/board/content-element.factory.ts b/apps/server/src/shared/domain/domainobject/board/content-element.factory.ts deleted file mode 100644 index 1e382576bbf..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/content-element.factory.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { ObjectId } from '@mikro-orm/mongodb'; -import { Injectable, NotImplementedException } from '@nestjs/common'; -import { InputFormat } from '@shared/domain/types'; -import { CollaborativeTextEditorElement } from './collaborative-text-editor-element.do'; -import { DrawingElement } from './drawing-element.do'; -import { ExternalToolElement } from './external-tool-element.do'; -import { FileElement } from './file-element.do'; -import { LinkElement } from './link-element.do'; -import { RichTextElement } from './rich-text-element.do'; -import { SubmissionContainerElement } from './submission-container-element.do'; -import { AnyContentElementDo, ContentElementType } from './types'; - -@Injectable() -export class ContentElementFactory { - build(type: ContentElementType): AnyContentElementDo { - let element!: AnyContentElementDo; - - switch (type) { - case ContentElementType.FILE: - element = this.buildFile(); - break; - case ContentElementType.LINK: - element = this.buildLink(); - break; - case ContentElementType.RICH_TEXT: - element = this.buildRichText(); - break; - case ContentElementType.DRAWING: - element = this.buildDrawing(); - break; - case ContentElementType.SUBMISSION_CONTAINER: - element = this.buildSubmissionContainer(); - break; - case ContentElementType.EXTERNAL_TOOL: - element = this.buildExternalTool(); - break; - case ContentElementType.COLLABORATIVE_TEXT_EDITOR: - element = this.buildCollaborativeTextEditor(); - break; - default: - break; - } - - if (!element) { - throw new NotImplementedException(`unknown type ${type} of element`); - } - - return element; - } - - private buildFile() { - const element = new FileElement({ - id: new ObjectId().toHexString(), - caption: '', - alternativeText: '', - children: [], - createdAt: new Date(), - updatedAt: new Date(), - }); - - return element; - } - - private buildLink() { - const element = new LinkElement({ - id: new ObjectId().toHexString(), - url: '', - title: '', - createdAt: new Date(), - updatedAt: new Date(), - }); - - return element; - } - - private buildRichText() { - const element = new RichTextElement({ - id: new ObjectId().toHexString(), - text: '', - inputFormat: InputFormat.RICH_TEXT_CK5, - children: [], - createdAt: new Date(), - updatedAt: new Date(), - }); - - return element; - } - - private buildDrawing() { - const element = new DrawingElement({ - id: new ObjectId().toHexString(), - description: '', - children: [], - createdAt: new Date(), - updatedAt: new Date(), - }); - - return element; - } - - private buildSubmissionContainer() { - const element = new SubmissionContainerElement({ - id: new ObjectId().toHexString(), - dueDate: null, - children: [], - createdAt: new Date(), - updatedAt: new Date(), - }); - - return element; - } - - private buildExternalTool() { - const element = new ExternalToolElement({ - id: new ObjectId().toHexString(), - children: [], - createdAt: new Date(), - updatedAt: new Date(), - }); - - return element; - } - - private buildCollaborativeTextEditor() { - const element = new CollaborativeTextEditorElement({ - id: new ObjectId().toHexString(), - children: [], - createdAt: new Date(), - updatedAt: new Date(), - }); - - return element; - } -} diff --git a/apps/server/src/shared/domain/domainobject/board/drawing-element.do.spec.ts b/apps/server/src/shared/domain/domainobject/board/drawing-element.do.spec.ts deleted file mode 100644 index b8876c7c0b1..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/drawing-element.do.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { createMock } from '@golevelup/ts-jest'; -import { drawingElementFactory } from '@shared/testing/factory/domainobject/board/drawing-element.do.factory'; -import { DrawingElement } from '@shared/domain/domainobject/board/drawing-element.do'; -import { BoardCompositeVisitor, BoardCompositeVisitorAsync } from './types'; - -describe(DrawingElement.name, () => { - describe('when trying to add a child to a drawing element', () => { - it('should throw an error ', () => { - const drawingElement = drawingElementFactory.build(); - const drawingElementChild = drawingElementFactory.build(); - - expect(() => drawingElement.addChild(drawingElementChild)).toThrow(); - }); - }); - - describe('accept', () => { - it('should call the right visitor method', () => { - const visitor = createMock(); - const drawingElement = drawingElementFactory.build(); - - drawingElement.accept(visitor); - - expect(visitor.visitDrawingElement).toHaveBeenCalledWith(drawingElement); - }); - }); - - describe('acceptAsync', () => { - it('should call the right async visitor method', async () => { - const visitor = createMock(); - const drawingElement = drawingElementFactory.build(); - - await drawingElement.acceptAsync(visitor); - - expect(visitor.visitDrawingElementAsync).toHaveBeenCalledWith(drawingElement); - }); - }); -}); diff --git a/apps/server/src/shared/domain/domainobject/board/drawing-element.do.ts b/apps/server/src/shared/domain/domainobject/board/drawing-element.do.ts deleted file mode 100644 index e4bf11936e8..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/drawing-element.do.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { BoardComposite, BoardCompositeProps } from './board-composite.do'; -import type { BoardCompositeVisitor, BoardCompositeVisitorAsync } from './types'; - -export class DrawingElement extends BoardComposite { - get description(): string { - return this.props.description; - } - - set description(value: string) { - this.props.description = value; - } - - isAllowedAsChild(): boolean { - return false; - } - - accept(visitor: BoardCompositeVisitor): void { - visitor.visitDrawingElement(this); - } - - async acceptAsync(visitor: BoardCompositeVisitorAsync): Promise { - await visitor.visitDrawingElementAsync(this); - } -} - -export interface DrawingElementProps extends BoardCompositeProps { - description: string; -} - -export function isDrawingElement(reference: unknown): reference is DrawingElement { - return reference instanceof DrawingElement; -} diff --git a/apps/server/src/shared/domain/domainobject/board/external-tool-element.do.spec.ts b/apps/server/src/shared/domain/domainobject/board/external-tool-element.do.spec.ts deleted file mode 100644 index 47fa813afba..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/external-tool-element.do.spec.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { createMock } from '@golevelup/ts-jest'; -import { ObjectId } from '@mikro-orm/mongodb'; -import { externalToolElementFactory } from '@shared/testing'; -import { ExternalToolElement } from './external-tool-element.do'; -import { BoardCompositeVisitor, BoardCompositeVisitorAsync } from './types'; - -describe(ExternalToolElement.name, () => { - describe('when trying to add a child to a external tool element', () => { - it('should throw an error ', () => { - const externalToolElement = externalToolElementFactory.build(); - const externalToolElementChild = externalToolElementFactory.build(); - - expect(() => externalToolElement.addChild(externalToolElementChild)).toThrow(); - }); - }); - - describe('update contextExternalToolId', () => { - it('should be able to update contextExternalToolId', () => { - const externalToolElement = externalToolElementFactory.build(); - const contextExternalToolId = new ObjectId().toHexString(); - - externalToolElement.contextExternalToolId = contextExternalToolId; - - expect(externalToolElement.contextExternalToolId).toEqual(contextExternalToolId); - }); - }); - - describe('accept', () => { - it('should call the right visitor method', () => { - const visitor = createMock(); - const externalToolElement = externalToolElementFactory.build(); - - externalToolElement.accept(visitor); - - expect(visitor.visitExternalToolElement).toHaveBeenCalledWith(externalToolElement); - }); - }); - - describe('acceptAsync', () => { - it('should call the right async visitor method', async () => { - const visitor = createMock(); - const externalToolElement = externalToolElementFactory.build(); - - await externalToolElement.acceptAsync(visitor); - - expect(visitor.visitExternalToolElementAsync).toHaveBeenCalledWith(externalToolElement); - }); - }); -}); diff --git a/apps/server/src/shared/domain/domainobject/board/external-tool-element.do.ts b/apps/server/src/shared/domain/domainobject/board/external-tool-element.do.ts deleted file mode 100644 index 84f053a1268..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/external-tool-element.do.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { BoardComposite, BoardCompositeProps } from './board-composite.do'; -import type { BoardCompositeVisitor, BoardCompositeVisitorAsync } from './types'; - -export class ExternalToolElement extends BoardComposite { - get contextExternalToolId(): string | undefined { - return this.props.contextExternalToolId; - } - - set contextExternalToolId(value: string | undefined) { - this.props.contextExternalToolId = value; - } - - isAllowedAsChild(): boolean { - return false; - } - - accept(visitor: BoardCompositeVisitor): void { - visitor.visitExternalToolElement(this); - } - - async acceptAsync(visitor: BoardCompositeVisitorAsync): Promise { - await visitor.visitExternalToolElementAsync(this); - } -} - -export interface ExternalToolElementProps extends BoardCompositeProps { - contextExternalToolId?: string; -} - -export function isExternalToolElement(reference: unknown): reference is ExternalToolElement { - return reference instanceof ExternalToolElement; -} diff --git a/apps/server/src/shared/domain/domainobject/board/file-element.do.spec.ts b/apps/server/src/shared/domain/domainobject/board/file-element.do.spec.ts deleted file mode 100644 index 6de914f6751..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/file-element.do.spec.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { createMock } from '@golevelup/ts-jest'; -import { fileElementFactory } from '@shared/testing'; -import { FileElement } from './file-element.do'; -import { BoardCompositeVisitor, BoardCompositeVisitorAsync } from './types'; - -describe(FileElement.name, () => { - describe('get caption', () => { - describe('when caption is set', () => { - it('should return the caption', () => { - const fileElement = fileElementFactory.build(); - - expect(fileElement.caption).toEqual(fileElement.caption); - }); - }); - - describe('when caption is not set', () => { - it('should return an empty string', () => { - const fileElement = fileElementFactory.build({ caption: undefined }); - - expect(fileElement.caption).toEqual(''); - }); - }); - }); - - describe('set caption', () => { - it('should set caption', () => { - const fileElement = fileElementFactory.build(); - const text = 'new caption'; - fileElement.caption = text; - - expect(fileElement.caption).toEqual(text); - }); - }); - - describe('get alternative text', () => { - describe('when alternative text is set', () => { - it('should return the alternative text', () => { - const fileElement = fileElementFactory.build(); - - expect(fileElement.alternativeText).toEqual(fileElement.alternativeText); - }); - }); - - describe('when alternative text is not set', () => { - it('should return an empty string', () => { - const fileElement = fileElementFactory.build({ alternativeText: undefined }); - - expect(fileElement.alternativeText).toEqual(''); - }); - }); - }); - - describe('set alternative text', () => { - it('should set alternative text', () => { - const fileElement = fileElementFactory.build(); - const text = 'new alternative text'; - fileElement.alternativeText = text; - - expect(fileElement.alternativeText).toEqual(text); - }); - }); - - describe('when trying to add a child to a file element', () => { - it('should throw an error ', () => { - const fileElement = fileElementFactory.build(); - const fileElementChild = fileElementFactory.build(); - - expect(() => fileElement.addChild(fileElementChild)).toThrow(); - }); - }); - - describe('update caption', () => { - it('should be able to update caption', () => { - const fileElement = fileElementFactory.build(); - const text = 'this is the titanic movie from 1997 in Blue-Ray Quality'; - fileElement.caption = text; - - expect(fileElement.caption).toEqual(text); - }); - }); - - describe('update alternative text', () => { - it('should be able to update alternative text', () => { - const fileElement = fileElementFactory.build(); - const text = 'this is the titanic movie from 1997 in Blue-Ray Quality'; - fileElement.alternativeText = text; - - expect(fileElement.alternativeText).toEqual(text); - }); - }); - - describe('accept', () => { - it('should call the right visitor method', () => { - const visitor = createMock(); - const fileElement = fileElementFactory.build(); - - fileElement.accept(visitor); - - expect(visitor.visitFileElement).toHaveBeenCalledWith(fileElement); - }); - }); - - describe('acceptAsync', () => { - it('should call the right async visitor method', async () => { - const visitor = createMock(); - const fileElement = fileElementFactory.build(); - - await fileElement.acceptAsync(visitor); - - expect(visitor.visitFileElementAsync).toHaveBeenCalledWith(fileElement); - }); - }); -}); diff --git a/apps/server/src/shared/domain/domainobject/board/file-element.do.ts b/apps/server/src/shared/domain/domainobject/board/file-element.do.ts deleted file mode 100644 index 4d58aa300ea..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/file-element.do.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { BoardComposite, BoardCompositeProps } from './board-composite.do'; -import type { BoardCompositeVisitor, BoardCompositeVisitorAsync } from './types'; - -export class FileElement extends BoardComposite { - get caption(): string { - return this.props.caption || ''; - } - - set caption(value: string) { - this.props.caption = value; - } - - get alternativeText(): string { - return this.props.alternativeText || ''; - } - - set alternativeText(value: string) { - this.props.alternativeText = value; - } - - isAllowedAsChild(): boolean { - return false; - } - - accept(visitor: BoardCompositeVisitor): void { - visitor.visitFileElement(this); - } - - async acceptAsync(visitor: BoardCompositeVisitorAsync): Promise { - await visitor.visitFileElementAsync(this); - } -} - -export interface FileElementProps extends BoardCompositeProps { - caption: string; - alternativeText: string; -} - -export function isFileElement(reference: unknown): reference is FileElement { - return reference instanceof FileElement; -} diff --git a/apps/server/src/shared/domain/domainobject/board/index.ts b/apps/server/src/shared/domain/domainobject/board/index.ts deleted file mode 100644 index dfee37bab05..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -export * from './board-composite.do'; -export * from './card.do'; -export * from './collaborative-text-editor-element.do'; -export * from './column-board.do'; -export * from './column.do'; -export * from './content-element.factory'; -export * from './drawing-element.do'; -export * from './external-tool-element.do'; -export * from './file-element.do'; -export * from './link-element.do'; -export * from './media-board'; -export * from './rich-text-element.do'; -export * from './submission-container-element.do'; -export * from './submission-item.do'; -export * from './submission-item.factory'; -export * from './types'; diff --git a/apps/server/src/shared/domain/domainobject/board/link-element.do.spec.ts b/apps/server/src/shared/domain/domainobject/board/link-element.do.spec.ts deleted file mode 100644 index 4a044e9be58..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/link-element.do.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { createMock } from '@golevelup/ts-jest'; -import { linkElementFactory } from '@shared/testing'; -import { LinkElement } from './link-element.do'; -import { BoardCompositeVisitor, BoardCompositeVisitorAsync } from './types'; - -describe(LinkElement.name, () => { - describe('when trying to add a child to a link element', () => { - it('should throw an error ', () => { - const linkElement = linkElementFactory.build(); - const linkElementFactoryChild = linkElementFactory.build(); - - expect(() => linkElement.addChild(linkElementFactoryChild)).toThrow(); - }); - }); - - describe('accept', () => { - it('should call the right visitor method', () => { - const visitor = createMock(); - const linkElement = linkElementFactory.build(); - - linkElement.accept(visitor); - - expect(visitor.visitLinkElement).toHaveBeenCalledWith(linkElement); - }); - }); - - describe('acceptAsync', () => { - it('should call the right async visitor method', async () => { - const visitor = createMock(); - const linkElement = linkElementFactory.build(); - - await linkElement.acceptAsync(visitor); - - expect(visitor.visitLinkElementAsync).toHaveBeenCalledWith(linkElement); - }); - }); -}); diff --git a/apps/server/src/shared/domain/domainobject/board/link-element.do.ts b/apps/server/src/shared/domain/domainobject/board/link-element.do.ts deleted file mode 100644 index 7b38cbd938e..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/link-element.do.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { BoardComposite, BoardCompositeProps } from './board-composite.do'; -import type { BoardCompositeVisitor, BoardCompositeVisitorAsync } from './types'; - -export class LinkElement extends BoardComposite { - get url(): string { - return this.props.url ?? ''; - } - - set url(value: string) { - this.props.url = value; - } - - get title(): string { - return this.props.title ?? ''; - } - - set title(value: string) { - this.props.title = value; - } - - get description(): string { - return this.props.description ?? ''; - } - - set description(value: string) { - this.props.description = value ?? ''; - } - - get imageUrl(): string { - return this.props.imageUrl ?? ''; - } - - set imageUrl(value: string) { - this.props.imageUrl = value; - } - - isAllowedAsChild(): boolean { - return false; - } - - accept(visitor: BoardCompositeVisitor): void { - visitor.visitLinkElement(this); - } - - async acceptAsync(visitor: BoardCompositeVisitorAsync): Promise { - await visitor.visitLinkElementAsync(this); - } -} - -export interface LinkElementProps extends BoardCompositeProps { - url: string; - title: string; - description?: string; - imageUrl?: string; -} - -export function isLinkElement(reference: unknown): reference is LinkElement { - return reference instanceof LinkElement; -} diff --git a/apps/server/src/shared/domain/domainobject/board/media-board/media-board.do.ts b/apps/server/src/shared/domain/domainobject/board/media-board/media-board.do.ts deleted file mode 100644 index 7ce27fa02c6..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/media-board/media-board.do.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { MediaBoardColors, MediaBoardLayoutType } from '@modules/board/domain'; -import { BoardComposite, BoardCompositeProps } from '../board-composite.do'; -import type { AnyBoardDo, BoardCompositeVisitor, BoardCompositeVisitorAsync, BoardExternalReference } from '../types'; -import { MediaLine } from './media-line.do'; - -export class MediaBoard extends BoardComposite { - get context(): BoardExternalReference { - return this.props.context; - } - - get layout(): MediaBoardLayoutType { - return this.props.layout; - } - - set layout(layout: MediaBoardLayoutType) { - this.props.layout = layout; - } - - get mediaAvailableLineBackgroundColor(): MediaBoardColors { - return this.props.mediaAvailableLineBackgroundColor; - } - - set mediaAvailableLineBackgroundColor(mediaAvailableLineBackgroundColor: MediaBoardColors) { - this.props.mediaAvailableLineBackgroundColor = mediaAvailableLineBackgroundColor; - } - - get mediaAvailableLineCollapsed(): boolean { - return this.props.mediaAvailableLineCollapsed; - } - - set mediaAvailableLineCollapsed(collapsed: boolean) { - this.props.mediaAvailableLineCollapsed = collapsed; - } - - isAllowedAsChild(domainObject: AnyBoardDo): boolean { - const allowed: boolean = domainObject instanceof MediaLine; - - return allowed; - } - - accept(visitor: BoardCompositeVisitor): void { - visitor.visitMediaBoard(this); - } - - async acceptAsync(visitor: BoardCompositeVisitorAsync): Promise { - await visitor.visitMediaBoardAsync(this); - } -} - -export interface MediaBoardProps extends BoardCompositeProps { - context: BoardExternalReference; - layout: MediaBoardLayoutType; - mediaAvailableLineBackgroundColor: MediaBoardColors; - mediaAvailableLineCollapsed: boolean; -} diff --git a/apps/server/src/shared/domain/domainobject/board/media-board/media-external-tool-element.do.spec.ts b/apps/server/src/shared/domain/domainobject/board/media-board/media-external-tool-element.do.spec.ts deleted file mode 100644 index 7cb3da16522..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/media-board/media-external-tool-element.do.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { mediaExternalToolElementFactory } from '@shared/testing'; -import { MediaExternalToolElement } from './media-external-tool-element.do'; - -describe(MediaExternalToolElement.name, () => { - describe('when trying to add a child to a media external tool element', () => { - it('should throw an error ', () => { - const externalToolElement = mediaExternalToolElementFactory.build(); - const externalToolElementChild = mediaExternalToolElementFactory.build(); - - expect(() => externalToolElement.addChild(externalToolElementChild)).toThrow(); - }); - }); -}); diff --git a/apps/server/src/shared/domain/domainobject/board/media-board/media-external-tool-element.do.ts b/apps/server/src/shared/domain/domainobject/board/media-board/media-external-tool-element.do.ts deleted file mode 100644 index e399864f750..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/media-board/media-external-tool-element.do.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { EntityId } from '../../../types'; -import { BoardComposite, BoardCompositeProps } from '../board-composite.do'; -import type { BoardCompositeVisitor, BoardCompositeVisitorAsync } from '../types'; - -export class MediaExternalToolElement extends BoardComposite { - get contextExternalToolId(): EntityId { - return this.props.contextExternalToolId; - } - - isAllowedAsChild(): boolean { - return false; - } - - accept(visitor: BoardCompositeVisitor): void { - visitor.visitMediaExternalToolElement(this); - } - - async acceptAsync(visitor: BoardCompositeVisitorAsync): Promise { - await visitor.visitMediaExternalToolElementAsync(this); - } -} - -export interface MediaExternalToolElementProps extends BoardCompositeProps { - contextExternalToolId: EntityId; -} - -export type MediaExternalToolElementInitProps = Omit; - -export function isMediaExternalToolElement(reference: unknown): reference is MediaExternalToolElement { - return reference instanceof MediaExternalToolElement; -} diff --git a/apps/server/src/shared/domain/domainobject/board/media-board/media-line.do.ts b/apps/server/src/shared/domain/domainobject/board/media-board/media-line.do.ts deleted file mode 100644 index da98fd7cf26..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/media-board/media-line.do.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { MediaBoardColors } from '@modules/board/domain'; -import { BoardComposite, BoardCompositeProps } from '../board-composite.do'; -import type { AnyBoardDo, BoardCompositeVisitor, BoardCompositeVisitorAsync } from '../types'; -import { MediaExternalToolElement } from './media-external-tool-element.do'; - -export class MediaLine extends BoardComposite { - get title(): string { - return this.props.title; - } - - set title(title: string) { - this.props.title = title; - } - - get backgroundColor(): MediaBoardColors { - return this.props.backgroundColor; - } - - set backgroundColor(backgroundColor: MediaBoardColors) { - this.props.backgroundColor = backgroundColor; - } - - get collapsed(): boolean { - return this.props.collapsed; - } - - set collapsed(collapsed: boolean) { - this.props.collapsed = collapsed; - } - - isAllowedAsChild(domainObject: AnyBoardDo): boolean { - const allowed: boolean = domainObject instanceof MediaExternalToolElement; - - return allowed; - } - - accept(visitor: BoardCompositeVisitor): void { - visitor.visitMediaLine(this); - } - - async acceptAsync(visitor: BoardCompositeVisitorAsync): Promise { - await visitor.visitMediaLineAsync(this); - } -} - -export interface MediaLineProps extends BoardCompositeProps { - title: string; - backgroundColor: MediaBoardColors; - collapsed: boolean; -} - -export type MediaLineInitProps = Omit; - -export function isMediaLine(reference: unknown): reference is MediaLine { - return reference instanceof MediaLine; -} diff --git a/apps/server/src/shared/domain/domainobject/board/rich-text-element.do.spec.ts b/apps/server/src/shared/domain/domainobject/board/rich-text-element.do.spec.ts deleted file mode 100644 index 7636c7ae872..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/rich-text-element.do.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { createMock } from '@golevelup/ts-jest'; -import { richTextElementFactory } from '@shared/testing'; -import { RichTextElement } from './rich-text-element.do'; -import { BoardCompositeVisitor, BoardCompositeVisitorAsync } from './types'; - -describe(RichTextElement.name, () => { - describe('when trying to add a child to a rich text element', () => { - it('should throw an error ', () => { - const richTextElement = richTextElementFactory.build(); - const richTextElementChild = richTextElementFactory.build(); - - expect(() => richTextElement.addChild(richTextElementChild)).toThrow(); - }); - }); - - describe('accept', () => { - it('should call the right visitor method', () => { - const visitor = createMock(); - const richTextElement = richTextElementFactory.build(); - - richTextElement.accept(visitor); - - expect(visitor.visitRichTextElement).toHaveBeenCalledWith(richTextElement); - }); - }); - - describe('acceptAsync', () => { - it('should call the right async visitor method', async () => { - const visitor = createMock(); - const richTextElement = richTextElementFactory.build(); - - await richTextElement.acceptAsync(visitor); - - expect(visitor.visitRichTextElementAsync).toHaveBeenCalledWith(richTextElement); - }); - }); -}); diff --git a/apps/server/src/shared/domain/domainobject/board/rich-text-element.do.ts b/apps/server/src/shared/domain/domainobject/board/rich-text-element.do.ts deleted file mode 100644 index 0603b384b54..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/rich-text-element.do.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { InputFormat } from '@shared/domain/types'; -import { BoardComposite, BoardCompositeProps } from './board-composite.do'; -import type { BoardCompositeVisitor, BoardCompositeVisitorAsync } from './types'; - -export class RichTextElement extends BoardComposite { - get text(): string { - return this.props.text; - } - - set text(value: string) { - this.props.text = value; - } - - get inputFormat(): InputFormat { - return this.props.inputFormat; - } - - set inputFormat(value: InputFormat) { - this.props.inputFormat = value; - } - - isAllowedAsChild(): boolean { - return false; - } - - accept(visitor: BoardCompositeVisitor): void { - visitor.visitRichTextElement(this); - } - - async acceptAsync(visitor: BoardCompositeVisitorAsync): Promise { - await visitor.visitRichTextElementAsync(this); - } -} - -export interface RichTextElementProps extends BoardCompositeProps { - text: string; - inputFormat: InputFormat; -} - -export function isRichTextElement(reference: unknown): reference is RichTextElement { - return reference instanceof RichTextElement; -} diff --git a/apps/server/src/shared/domain/domainobject/board/submission-container-element.do.spec.ts b/apps/server/src/shared/domain/domainobject/board/submission-container-element.do.spec.ts deleted file mode 100644 index 0153f7777c2..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/submission-container-element.do.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { createMock } from '@golevelup/ts-jest'; -import { submissionContainerElementFactory, submissionItemFactory } from '@shared/testing'; -import { SubmissionContainerElement } from './submission-container-element.do'; -import { BoardCompositeVisitor, BoardCompositeVisitorAsync } from './types'; - -describe(SubmissionContainerElement.name, () => { - describe('when trying to add a child to a submission container element', () => { - it('should throw an error ', () => { - const submissionContainerElement = submissionContainerElementFactory.build(); - const submissionContainerElementChild = submissionContainerElementFactory.build(); - - expect(() => submissionContainerElement.addChild(submissionContainerElementChild)).toThrow(); - }); - - it('should not throw if child is submission-item ', () => { - const submissionContainerElement = submissionContainerElementFactory.build(); - const submissionContainerElementChild = submissionItemFactory.build(); - - expect(() => submissionContainerElement.addChild(submissionContainerElementChild)).not.toThrow(); - }); - }); - - describe('accept', () => { - it('should call the right visitor method', () => { - const visitor = createMock(); - const submissionContainerElement = submissionContainerElementFactory.build(); - - submissionContainerElement.accept(visitor); - - expect(visitor.visitSubmissionContainerElement).toHaveBeenCalledWith(submissionContainerElement); - }); - }); - - describe('acceptAsync', () => { - it('should call the right async visitor method', async () => { - const visitor = createMock(); - const submissionContainerElement = submissionContainerElementFactory.build(); - - await submissionContainerElement.acceptAsync(visitor); - - expect(visitor.visitSubmissionContainerElementAsync).toHaveBeenCalledWith(submissionContainerElement); - }); - }); -}); diff --git a/apps/server/src/shared/domain/domainobject/board/submission-container-element.do.ts b/apps/server/src/shared/domain/domainobject/board/submission-container-element.do.ts deleted file mode 100644 index 0980eb7a569..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/submission-container-element.do.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { BoardComposite, BoardCompositeProps } from './board-composite.do'; -import { SubmissionItem } from './submission-item.do'; -import type { AnyBoardDo, BoardCompositeVisitor, BoardCompositeVisitorAsync } from './types'; - -export class SubmissionContainerElement extends BoardComposite { - get dueDate(): Date | null { - return this.props.dueDate; - } - - set dueDate(value: Date | null) { - this.props.dueDate = value; - } - - isAllowedAsChild(domainObject: AnyBoardDo): boolean { - const allowed = domainObject instanceof SubmissionItem; - return allowed; - } - - accept(visitor: BoardCompositeVisitor): void { - visitor.visitSubmissionContainerElement(this); - } - - async acceptAsync(visitor: BoardCompositeVisitorAsync): Promise { - await visitor.visitSubmissionContainerElementAsync(this); - } -} - -export interface SubmissionContainerElementProps extends BoardCompositeProps { - dueDate: Date | null; -} - -export function isSubmissionContainerElement(reference: unknown): reference is SubmissionContainerElement { - return reference instanceof SubmissionContainerElement; -} diff --git a/apps/server/src/shared/domain/domainobject/board/submission-item.do.spec.ts b/apps/server/src/shared/domain/domainobject/board/submission-item.do.spec.ts deleted file mode 100644 index df7b4b6f95e..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/submission-item.do.spec.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { createMock } from '@golevelup/ts-jest'; -import { submissionContainerElementFactory, submissionItemFactory } from '@shared/testing'; -import { ObjectId } from '@mikro-orm/mongodb'; -import { SubmissionItem } from './submission-item.do'; -import { BoardCompositeVisitor, BoardCompositeVisitorAsync } from './types'; - -describe(SubmissionItem.name, () => { - describe('when trying to add a child to a submission item element', () => { - it('should throw an error ', () => { - const submissionItem = submissionItemFactory.build(); - const submissionItemChild = submissionItemFactory.build(); - - expect(() => submissionItem.addChild(submissionItemChild)).toThrow(); - }); - }); - - describe('accept', () => { - it('should call the right visitor method', () => { - const visitor = createMock(); - const submissionItem = submissionItemFactory.build(); - - submissionItem.accept(visitor); - - expect(visitor.visitSubmissionItem).toHaveBeenCalledWith(submissionItem); - }); - }); - - describe('acceptAsync', () => { - it('should call the right async visitor method', async () => { - const visitor = createMock(); - const submissionContainerElement = submissionContainerElementFactory.build(); - - await submissionContainerElement.acceptAsync(visitor); - - expect(visitor.visitSubmissionContainerElementAsync).toHaveBeenCalledWith(submissionContainerElement); - }); - }); - - describe('set userId', () => { - it('should set userId', () => { - const userId = new ObjectId().toHexString(); - const submissionItem = submissionItemFactory.build(); - submissionItem.userId = userId; - - expect(submissionItem.userId).toEqual(userId); - }); - }); - - describe('set completed', () => { - it('should set completed', () => { - const completed = true; - const submissionItem = submissionItemFactory.build(); - submissionItem.completed = completed; - - expect(submissionItem.completed).toEqual(completed); - }); - }); -}); diff --git a/apps/server/src/shared/domain/domainobject/board/submission-item.do.ts b/apps/server/src/shared/domain/domainobject/board/submission-item.do.ts deleted file mode 100644 index d6e1a462956..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/submission-item.do.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { FileElement, isFileElement, isRichTextElement, RichTextElement } from '@shared/domain/domainobject'; -import { EntityId } from '@shared/domain/types'; -import { BoardComposite, BoardCompositeProps } from './board-composite.do'; -import type { AnyBoardDo, BoardCompositeVisitor, BoardCompositeVisitorAsync } from './types'; - -export class SubmissionItem extends BoardComposite { - get completed(): boolean { - return this.props.completed; - } - - set completed(value: boolean) { - this.props.completed = value; - } - - get userId(): EntityId { - return this.props.userId; - } - - set userId(value: EntityId) { - this.props.userId = value; - } - - isAllowedAsChild(child: AnyBoardDo): boolean { - const allowed = isFileElement(child) || isRichTextElement(child); - - return allowed; - } - - accept(visitor: BoardCompositeVisitor): void { - visitor.visitSubmissionItem(this); - } - - async acceptAsync(visitor: BoardCompositeVisitorAsync): Promise { - await visitor.visitSubmissionItemAsync(this); - } -} - -export interface SubmissionItemProps extends BoardCompositeProps { - completed: boolean; - userId: EntityId; -} - -export function isSubmissionItem(reference: unknown): reference is SubmissionItem { - return reference instanceof SubmissionItem; -} - -export const isSubmissionItemContent = (element: AnyBoardDo): element is RichTextElement | FileElement => - isRichTextElement(element) || isFileElement(element); diff --git a/apps/server/src/shared/domain/domainobject/board/submission-item.factory.spec.ts b/apps/server/src/shared/domain/domainobject/board/submission-item.factory.spec.ts deleted file mode 100644 index 8260ea5f2f7..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/submission-item.factory.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { SubmissionItem } from './submission-item.do'; -import { SubmissionItemFactory } from './submission-item.factory'; - -describe(SubmissionItemFactory.name, () => { - describe('build', () => { - const setup = () => { - const submissionItemFactory = new SubmissionItemFactory(); - - return { submissionItemFactory }; - }; - - it('should return SubmissionItem', () => { - const { submissionItemFactory } = setup(); - - const element = submissionItemFactory.build(); - - expect(element).toBeInstanceOf(SubmissionItem); - }); - }); -}); diff --git a/apps/server/src/shared/domain/domainobject/board/submission-item.factory.ts b/apps/server/src/shared/domain/domainobject/board/submission-item.factory.ts deleted file mode 100644 index c9c5a4b3f4d..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/submission-item.factory.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { ObjectId } from '@mikro-orm/mongodb'; -import { SubmissionItem } from './submission-item.do'; - -@Injectable() -export class SubmissionItemFactory { - build(): SubmissionItem { - return new SubmissionItem({ - id: new ObjectId().toHexString(), - createdAt: new Date(), - updatedAt: new Date(), - completed: false, - userId: new ObjectId().toHexString(), - }); - } -} diff --git a/apps/server/src/shared/domain/domainobject/board/types/any-board-do.ts b/apps/server/src/shared/domain/domainobject/board/types/any-board-do.ts deleted file mode 100644 index a9083e4d3d8..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/types/any-board-do.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Card } from '../card.do'; -import { ColumnBoard } from '../column-board.do'; -import { Column } from '../column.do'; -import { SubmissionItem } from '../submission-item.do'; -import { AnyContentElementDo } from './any-content-element-do'; -import { AnyMediaBoardDo } from './any-media-board-do'; - -export type AnyBoardDo = ColumnBoard | Column | Card | AnyContentElementDo | SubmissionItem | AnyMediaBoardDo; diff --git a/apps/server/src/shared/domain/domainobject/board/types/any-content-element-do.ts b/apps/server/src/shared/domain/domainobject/board/types/any-content-element-do.ts deleted file mode 100644 index e40eb2035dc..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/types/any-content-element-do.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { CollaborativeTextEditorElement } from '../collaborative-text-editor-element.do'; -import { DrawingElement } from '../drawing-element.do'; -import { ExternalToolElement } from '../external-tool-element.do'; -import { FileElement } from '../file-element.do'; -import { LinkElement } from '../link-element.do'; -import { RichTextElement } from '../rich-text-element.do'; -import { SubmissionContainerElement } from '../submission-container-element.do'; -import type { AnyBoardDo } from './any-board-do'; - -export type AnyContentElementDo = - | CollaborativeTextEditorElement - | DrawingElement - | ExternalToolElement - | FileElement - | LinkElement - | RichTextElement - | SubmissionContainerElement; - -export const isAnyContentElement = (element: AnyBoardDo): element is AnyContentElementDo => { - const result = - element instanceof CollaborativeTextEditorElement || - element instanceof DrawingElement || - element instanceof ExternalToolElement || - element instanceof FileElement || - element instanceof LinkElement || - element instanceof RichTextElement || - element instanceof SubmissionContainerElement; - - return result; -}; diff --git a/apps/server/src/shared/domain/domainobject/board/types/any-media-board-do.ts b/apps/server/src/shared/domain/domainobject/board/types/any-media-board-do.ts deleted file mode 100644 index be18e6de06b..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/types/any-media-board-do.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { MediaBoard, MediaLine } from '../media-board'; -import type { AnyMediaContentElementDo } from './any-media-content-element-do'; - -export type AnyMediaBoardDo = MediaBoard | MediaLine | AnyMediaContentElementDo; diff --git a/apps/server/src/shared/domain/domainobject/board/types/any-media-content-element-do.ts b/apps/server/src/shared/domain/domainobject/board/types/any-media-content-element-do.ts deleted file mode 100644 index f8b7b137cdc..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/types/any-media-content-element-do.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { MediaExternalToolElement } from '../media-board'; -import type { AnyBoardDo } from './any-board-do'; - -export type AnyMediaContentElementDo = MediaExternalToolElement; - -export const isAnyMediaContentElement = (element: AnyBoardDo): element is AnyMediaContentElementDo => { - const result: boolean = element instanceof MediaExternalToolElement; - - return result; -}; diff --git a/apps/server/src/shared/domain/domainobject/board/types/board-composite-visitor.ts b/apps/server/src/shared/domain/domainobject/board/types/board-composite-visitor.ts deleted file mode 100644 index ab52ec61e3c..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/types/board-composite-visitor.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { Card } from '../card.do'; -import { CollaborativeTextEditorElement } from '../collaborative-text-editor-element.do'; -import type { ColumnBoard } from '../column-board.do'; -import type { Column } from '../column.do'; -import type { DrawingElement } from '../drawing-element.do'; -import type { ExternalToolElement } from '../external-tool-element.do'; -import type { FileElement } from '../file-element.do'; -import type { LinkElement } from '../link-element.do'; -import type { MediaBoard, MediaExternalToolElement, MediaLine } from '../media-board'; -import type { RichTextElement } from '../rich-text-element.do'; -import type { SubmissionContainerElement } from '../submission-container-element.do'; -import type { SubmissionItem } from '../submission-item.do'; - -export interface BoardCompositeVisitor extends MediaBoardCompositeVisitor, ColumnBoardCompositeVisitor {} -export interface BoardCompositeVisitorAsync extends MediaBoardCompositeVisitorAsync, ColumnBoardCompositeVisitorAsync {} - -export interface ColumnBoardCompositeVisitor { - visitColumnBoard(columnBoard: ColumnBoard): void; - visitColumn(column: Column): void; - visitCard(card: Card): void; - visitFileElement(fileElement: FileElement): void; - visitLinkElement(linkElement: LinkElement): void; - visitRichTextElement(richTextElement: RichTextElement): void; - visitDrawingElement(drawingElement: DrawingElement): void; - visitSubmissionContainerElement(submissionContainerElement: SubmissionContainerElement): void; - visitSubmissionItem(submissionItem: SubmissionItem): void; - visitExternalToolElement(externalToolElement: ExternalToolElement): void; - visitCollaborativeTextEditorElement(collaborativeTextEditorElement: CollaborativeTextEditorElement): void; -} - -export interface ColumnBoardCompositeVisitorAsync { - visitColumnBoardAsync(columnBoard: ColumnBoard): Promise; - visitColumnAsync(column: Column): Promise; - visitCardAsync(card: Card): Promise; - visitFileElementAsync(fileElement: FileElement): Promise; - visitLinkElementAsync(linkElement: LinkElement): Promise; - visitRichTextElementAsync(richTextElement: RichTextElement): Promise; - visitDrawingElementAsync(drawingElement: DrawingElement): Promise; - visitSubmissionContainerElementAsync(submissionContainerElement: SubmissionContainerElement): Promise; - visitSubmissionItemAsync(submissionItem: SubmissionItem): Promise; - visitExternalToolElementAsync(externalToolElement: ExternalToolElement): Promise; - visitCollaborativeTextEditorElementAsync( - collaborativeTextEditorElement: CollaborativeTextEditorElement - ): Promise; -} - -export interface MediaBoardCompositeVisitor { - visitMediaBoard(mediaBoard: MediaBoard): void; - visitMediaLine(mediaLine: MediaLine): void; - visitMediaExternalToolElement(mediaElement: MediaExternalToolElement): void; -} - -export interface MediaBoardCompositeVisitorAsync { - visitMediaBoardAsync(mediaBoard: MediaBoard): Promise; - visitMediaLineAsync(mediaLine: MediaLine): Promise; - visitMediaExternalToolElementAsync(mediaElement: MediaExternalToolElement): Promise; -} diff --git a/apps/server/src/shared/domain/domainobject/board/types/board-do-authorizable.ts b/apps/server/src/shared/domain/domainobject/board/types/board-do-authorizable.ts deleted file mode 100644 index da44db61602..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/types/board-do-authorizable.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; -import { EntityId } from '@shared/domain/types'; -import { ColumnBoard } from '../column-board.do'; -import { MediaBoard } from '../media-board'; -import { AnyBoardDo } from './any-board-do'; - -export enum BoardRoles { - EDITOR = 'editor', - READER = 'reader', -} - -export interface UserWithBoardRoles { - firstName?: string; - lastName?: string; - roles: BoardRoles[]; - userId: EntityId; -} - -export interface BoardDoAuthorizableProps extends AuthorizableObject { - id: EntityId; - users: UserWithBoardRoles[]; - boardDo: AnyBoardDo; - rootDo: ColumnBoard | MediaBoard; - parentDo?: AnyBoardDo; -} - -export class BoardDoAuthorizable extends DomainObject { - get users(): UserWithBoardRoles[] { - return this.props.users; - } - - get boardDo(): AnyBoardDo { - return this.props.boardDo; - } - - set boardDo(value: AnyBoardDo) { - this.props.boardDo = value; - } - - get parentDo(): AnyBoardDo | undefined { - return this.props.parentDo; - } - - set parentDo(value: AnyBoardDo | undefined) { - this.props.parentDo = value; - } - - get rootDo(): ColumnBoard | MediaBoard { - return this.props.rootDo; - } -} diff --git a/apps/server/src/shared/domain/domainobject/board/types/column-board-info.ts b/apps/server/src/shared/domain/domainobject/board/types/column-board-info.ts deleted file mode 100644 index 10f4d1dcc33..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/types/column-board-info.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { EntityId } from '@shared/domain/types'; - -export type ColumnBoardInfo = { - id: EntityId; - title: string; - createdAt: Date; - updatedAt: Date; -}; diff --git a/apps/server/src/shared/domain/domainobject/board/types/index.ts b/apps/server/src/shared/domain/domainobject/board/types/index.ts deleted file mode 100644 index 8c080ffa4a9..00000000000 --- a/apps/server/src/shared/domain/domainobject/board/types/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from './any-board-do'; -export * from './any-content-element-do'; -export { AnyMediaBoardDo } from './any-media-board-do'; -export { AnyMediaContentElementDo, isAnyMediaContentElement } from './any-media-content-element-do'; -export * from './board-composite-visitor'; -export * from './board-do-authorizable'; -export * from './board-external-reference'; -export * from './column-board-info'; -export * from './content-elements.enum'; -export * from './board-layout.enum'; diff --git a/apps/server/src/shared/domain/domainobject/index.ts b/apps/server/src/shared/domain/domainobject/index.ts index 46e88212097..51fbc5000a2 100644 --- a/apps/server/src/shared/domain/domainobject/index.ts +++ b/apps/server/src/shared/domain/domainobject/index.ts @@ -1,7 +1,6 @@ export * from './base.do'; export * from './pseudonym.do'; export * from './video-conference.do'; -export * from './board'; export * from './user-login-migration.do'; export * from './legacy-school.do'; export * from './user.do'; diff --git a/apps/server/src/shared/domain/entity/all-entities.spec.ts b/apps/server/src/shared/domain/entity/all-entities.spec.ts index b7a27fd58b4..3c92153fc72 100644 --- a/apps/server/src/shared/domain/entity/all-entities.spec.ts +++ b/apps/server/src/shared/domain/entity/all-entities.spec.ts @@ -1,7 +1,8 @@ +import { MongoMemoryDatabaseModule } from '@infra/database'; import { MikroORM } from '@mikro-orm/core'; import { EntityManager } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; -import { MongoMemoryDatabaseModule } from '@infra/database'; +import { createCollections } from '@shared/testing'; import { ALL_ENTITIES } from '.'; describe('BaseRepo', () => { @@ -17,10 +18,11 @@ describe('BaseRepo', () => { em = module.get(EntityManager); orm = module.get(MikroORM); + + await createCollections(em); }); afterAll(async () => { - await orm.close(); await module.close(); }); diff --git a/apps/server/src/shared/domain/entity/all-entities.ts b/apps/server/src/shared/domain/entity/all-entities.ts index 00e7784ccde..701d7fc3c75 100644 --- a/apps/server/src/shared/domain/entity/all-entities.ts +++ b/apps/server/src/shared/domain/entity/all-entities.ts @@ -1,3 +1,5 @@ +import { AccountEntity } from '@modules/account/domain/entity/account.entity'; +import { BoardNodeEntity } from '@modules/board/repo/entity'; import { ClassEntity } from '@modules/class/entity'; import { GroupEntity } from '@modules/group/entity'; import { InstanceEntity } from '@modules/instance'; @@ -10,27 +12,10 @@ import { ContextExternalToolEntity } from '@modules/tool/context-external-tool/e import { ExternalToolEntity } from '@modules/tool/external-tool/entity'; import { SchoolExternalToolEntity } from '@modules/tool/school-external-tool/entity'; import { MediaUserLicenseEntity, UserLicenseEntity } from '@modules/user-license/entity'; -import { AccountEntity } from '@src/modules/account/domain/entity/account.entity'; -import { DeletionLogEntity } from '@src/modules/deletion/repo/entity/deletion-log.entity'; -import { DeletionRequestEntity } from '@src/modules/deletion/repo/entity/deletion-request.entity'; -import { RocketChatUserEntity } from '@src/modules/rocketchat-user/entity'; -import { - BoardNode, - CardNode, - CollaborativeTextEditorElementNode, - ColumnBoardNode, - ColumnNode, - DrawingElementNode, - ExternalToolElementNodeEntity, - FileElementNode, - LinkElementNode, - MediaBoardNode, - MediaExternalToolElementNode, - MediaLineNode, - RichTextElementNode, - SubmissionContainerElementNode, - SubmissionItemNode, -} from './boardnode'; +import { DeletionLogEntity } from '@modules/deletion/repo/entity/deletion-log.entity'; +import { DeletionRequestEntity } from '@modules/deletion/repo/entity/deletion-request.entity'; +import { RocketChatUserEntity } from '@modules/rocketchat-user/entity'; +import { ColumnBoardNode } from './column-board-node.entity'; import { Course } from './course.entity'; import { CourseGroup } from './coursegroup.entity'; import { DashboardGridElementModel, DashboardModelEntity } from './dashboard.model.entity'; @@ -63,25 +48,12 @@ export const ALL_ENTITIES = [ AccountEntity, LegacyBoard, LegacyBoardElement, - BoardNode, - CardNode, + BoardNodeEntity, ColumnboardBoardElement, ColumnBoardNode, - ColumnNode, ClassEntity, DeletionRequestEntity, DeletionLogEntity, - FileElementNode, - LinkElementNode, - RichTextElementNode, - DrawingElementNode, - SubmissionContainerElementNode, - SubmissionItemNode, - ExternalToolElementNodeEntity, - CollaborativeTextEditorElementNode, - MediaBoardNode, - MediaLineNode, - MediaExternalToolElementNode, ContextExternalToolEntity, CountyEmbeddable, Course, diff --git a/apps/server/src/shared/domain/entity/boardnode/boardnode.entity.spec.ts b/apps/server/src/shared/domain/entity/boardnode/boardnode.entity.spec.ts deleted file mode 100644 index a8dd6d411ef..00000000000 --- a/apps/server/src/shared/domain/entity/boardnode/boardnode.entity.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { BoardExternalReferenceType, BoardLayout } from '@shared/domain/domainobject'; -import { cardNodeFactory, columnBoardNodeFactory, setupEntities } from '@shared/testing'; -import { BoardNode } from './boardnode.entity'; -import { ColumnBoardNode } from './column-board-node.entity'; - -describe(BoardNode.name, () => { - beforeAll(async () => { - await setupEntities(); - }); - - describe('constructor', () => { - it('should throw an error when parent has no id', () => { - const board = columnBoardNodeFactory.build(); - expect(() => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const column = new ColumnBoardNode({ - parent: board, - title: 'column #1', - context: { type: BoardExternalReferenceType.Course, id: 'course1' }, - isVisible: true, - layout: BoardLayout.COLUMNS, - }); - column.title = 'hate to get useless sonar lint errors'; - }).toThrowError(); - }); - }); - - describe('hasParent', () => { - it('should return false for root nodes', () => { - const node = cardNodeFactory.build(); - expect(node.hasParent()).toBe(false); - }); - - it('should return true for nested nodes', () => { - const node = cardNodeFactory.build(); - node.path = ',63ffb662acf052cfb874d0de,63ffb662acf052cfb874d0df,'; - expect(node.hasParent()).toBe(true); - }); - }); - - describe('ancestorIds', () => { - it('should return the list of ancestor ids', () => { - const node = cardNodeFactory.build(); - node.path = ',63ffb662acf052cfb874d0de,63ffb662acf052cfb874d0df,'; - expect(node.ancestorIds).toEqual(['63ffb662acf052cfb874d0de', '63ffb662acf052cfb874d0df']); - }); - }); - - describe('parentId', () => { - describe('on root', () => { - it('should return undefined', () => { - const node = cardNodeFactory.build(); - expect(node.parentId).toBe(undefined); - }); - }); - - describe('on nested node', () => { - it('should return parent id', () => { - const node = cardNodeFactory.build(); - node.path = ',63ffb662acf052cfb874d0de,63ffb662acf052cfb874d0df,'; - expect(node.parentId).toBe('63ffb662acf052cfb874d0df'); - }); - }); - }); -}); diff --git a/apps/server/src/shared/domain/entity/boardnode/boardnode.entity.ts b/apps/server/src/shared/domain/entity/boardnode/boardnode.entity.ts deleted file mode 100644 index 0792088e38b..00000000000 --- a/apps/server/src/shared/domain/entity/boardnode/boardnode.entity.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Entity, Enum, Index, Property } from '@mikro-orm/core'; -import { InternalServerErrorException } from '@nestjs/common'; -import { AnyBoardDo } from '../../domainobject'; -import { EntityId } from '../../types'; -import { BaseEntityWithTimestamps } from '../base.entity'; -import { BoardDoBuilder, BoardNodeType } from './types'; - -const PATH_SEPARATOR = ','; - -@Entity({ tableName: 'boardnodes', discriminatorColumn: 'type', abstract: true }) -@Index({ properties: ['path'] }) -export abstract class BoardNode extends BaseEntityWithTimestamps { - constructor(props: BoardNodeProps) { - super(); - if (props.parent && props.parent.id == null) { - throw new InternalServerErrorException('Cannot create board node with a parent having no id'); - } - if (props.id != null) { - this.id = props.id; - } - this.path = props.parent ? BoardNode.joinPath(props.parent.path, props.parent.id) : PATH_SEPARATOR; - this.level = props.parent ? props.parent.level + 1 : 0; - this.position = props.position ?? 0; - this.title = props.title; - } - - @Property({ nullable: false }) - path: string; - - @Property({ nullable: false }) - level: number; - - @Property({ nullable: false }) - position: number; - - @Enum(() => BoardNodeType) - type!: BoardNodeType; - - @Property({ nullable: true }) - title?: string; - - get parentId(): EntityId | undefined { - const parentId = this.hasParent() ? this.ancestorIds[this.ancestorIds.length - 1] : undefined; - return parentId; - } - - get ancestorIds(): EntityId[] { - const parentIds = this.path.split(PATH_SEPARATOR).filter((id) => id !== ''); - return parentIds; - } - - get pathOfChildren(): string { - return BoardNode.joinPath(this.path, this.id); - } - - hasParent() { - return this.ancestorIds.length > 0; - } - - abstract useDoBuilder(builder: BoardDoBuilder): AnyBoardDo; - - static joinPath(path: string, id: EntityId) { - return `${path}${id}${PATH_SEPARATOR}`; - } -} - -export interface BoardNodeProps { - id?: EntityId; - parent?: BoardNode; - position?: number; - title?: string; -} diff --git a/apps/server/src/shared/domain/entity/boardnode/card-node.entity.ts b/apps/server/src/shared/domain/entity/boardnode/card-node.entity.ts deleted file mode 100644 index 1dea8cb9c5e..00000000000 --- a/apps/server/src/shared/domain/entity/boardnode/card-node.entity.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Entity, Property } from '@mikro-orm/core'; -import { Card } from '@shared/domain/domainobject'; -import { BoardNode, BoardNodeProps } from './boardnode.entity'; -import { BoardDoBuilder } from './types'; -import { BoardNodeType } from './types/board-node-type'; - -@Entity({ discriminatorValue: BoardNodeType.CARD }) -export class CardNode extends BoardNode { - constructor(props: CardNodeProps) { - super(props); - this.type = BoardNodeType.CARD; - this.height = props.height; - } - - @Property() - height: number; - - useDoBuilder(builder: BoardDoBuilder): Card { - const domainObject = builder.buildCard(this); - return domainObject; - } -} - -export interface CardNodeProps extends BoardNodeProps { - height: number; -} diff --git a/apps/server/src/shared/domain/entity/boardnode/collaborative-text-editor-element-node.entity.ts b/apps/server/src/shared/domain/entity/boardnode/collaborative-text-editor-element-node.entity.ts deleted file mode 100644 index 700f9a15482..00000000000 --- a/apps/server/src/shared/domain/entity/boardnode/collaborative-text-editor-element-node.entity.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Entity } from '@mikro-orm/core'; -import { AnyBoardDo } from '../../domainobject'; -import { BoardNode, BoardNodeProps } from './boardnode.entity'; -import { BoardDoBuilder, BoardNodeType } from './types'; - -@Entity({ discriminatorValue: BoardNodeType.COLLABORATIVE_TEXT_EDITOR }) -export class CollaborativeTextEditorElementNode extends BoardNode { - constructor(props: BoardNodeProps) { - super(props); - this.type = BoardNodeType.COLLABORATIVE_TEXT_EDITOR; - } - - useDoBuilder(builder: BoardDoBuilder): AnyBoardDo { - const domainObject = builder.buildCollaborativeTextEditorElement(this); - - return domainObject; - } -} diff --git a/apps/server/src/shared/domain/entity/boardnode/column-board-node.entity.spec.ts b/apps/server/src/shared/domain/entity/boardnode/column-board-node.entity.spec.ts deleted file mode 100644 index aa82f8eeeea..00000000000 --- a/apps/server/src/shared/domain/entity/boardnode/column-board-node.entity.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { columnBoardNodeFactory, setupEntities } from '@shared/testing'; -import { ColumnBoardNode } from './column-board-node.entity'; - -describe(ColumnBoardNode.name, () => { - beforeAll(async () => { - await setupEntities(); - }); - - describe('publish', () => { - it('should set isVisible to true', () => { - const columnBoard = columnBoardNodeFactory.build(); - columnBoard.publish(); - expect(columnBoard.isVisible).toBe(true); - }); - }); - describe('unpublish', () => { - it('should set isVisible to false', () => { - const columnBoard = columnBoardNodeFactory.build(); - columnBoard.unpublish(); - expect(columnBoard.isVisible).toBe(false); - }); - }); -}); diff --git a/apps/server/src/shared/domain/entity/boardnode/column-board-node.entity.ts b/apps/server/src/shared/domain/entity/boardnode/column-board-node.entity.ts deleted file mode 100644 index c3417942074..00000000000 --- a/apps/server/src/shared/domain/entity/boardnode/column-board-node.entity.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { Entity, Index, Property } from '@mikro-orm/core'; -import { ObjectId } from '@mikro-orm/mongodb'; -import { - AnyBoardDo, - BoardExternalReference, - BoardExternalReferenceType, - BoardLayout, -} from '@shared/domain/domainobject/board/types'; -import { LearnroomElement } from '../../interface'; -import { BoardNode } from './boardnode.entity'; -import { type RootBoardNodeProps } from './root-board-node.entity'; -import { BoardDoBuilder, BoardNodeType } from './types'; - -// TODO Use an abstract base class for root nodes that have a contextId and a contextType. Multiple STI abstract base classes are blocked by MikroORM 6.1.2 (issue #3745) -@Entity({ discriminatorValue: BoardNodeType.COLUMN_BOARD }) -@Index({ properties: ['_contextId'] }) -@Index({ properties: ['_contextType'] }) -export class ColumnBoardNode extends BoardNode implements LearnroomElement { - constructor(props: ColumnBoardNodeProps) { - super(props); - this.type = BoardNodeType.COLUMN_BOARD; - - this._contextType = props.context.type; - this._contextId = new ObjectId(props.context.id); - - this.isVisible = props.isVisible ?? false; - - this.layout = props.layout ?? BoardLayout.COLUMNS; - } - - @Property({ fieldName: 'contextType' }) - _contextType: BoardExternalReferenceType; - - @Property({ fieldName: 'context' }) - _contextId: ObjectId; - - @Property({ type: 'boolean', nullable: false }) - isVisible = false; - - get context(): BoardExternalReference { - return { - type: this._contextType, - id: this._contextId.toHexString(), - }; - } - - @Property({ nullable: false }) - layout: BoardLayout; - - useDoBuilder(builder: BoardDoBuilder): AnyBoardDo { - const domainObject = builder.buildColumnBoard(this); - return domainObject; - } - - /** - * @deprecated - this is here only for the sake of the legacy-board (lernraum) - */ - publish(): void { - this.isVisible = true; - } - - /** - * @deprecated - this is here only for the sake of the legacy-board (lernraum) - */ - unpublish(): void { - this.isVisible = false; - } -} - -export interface ColumnBoardNodeProps extends RootBoardNodeProps { - isVisible: boolean; - layout: BoardLayout; -} diff --git a/apps/server/src/shared/domain/entity/boardnode/column-node.entity.ts b/apps/server/src/shared/domain/entity/boardnode/column-node.entity.ts deleted file mode 100644 index e008092bb07..00000000000 --- a/apps/server/src/shared/domain/entity/boardnode/column-node.entity.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Entity } from '@mikro-orm/core'; -import { AnyBoardDo } from '../../domainobject'; -import { BoardNode, BoardNodeProps } from './boardnode.entity'; -import { BoardDoBuilder } from './types'; -import { BoardNodeType } from './types/board-node-type'; - -@Entity({ discriminatorValue: BoardNodeType.COLUMN }) -export class ColumnNode extends BoardNode { - constructor(props: BoardNodeProps) { - super(props); - this.type = BoardNodeType.COLUMN; - } - - useDoBuilder(builder: BoardDoBuilder): AnyBoardDo { - const domainObject = builder.buildColumn(this); - return domainObject; - } -} diff --git a/apps/server/src/shared/domain/entity/boardnode/drawing-element-node.entity.spec.ts b/apps/server/src/shared/domain/entity/boardnode/drawing-element-node.entity.spec.ts deleted file mode 100644 index ce868baff25..00000000000 --- a/apps/server/src/shared/domain/entity/boardnode/drawing-element-node.entity.spec.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { DrawingElementNode } from '@shared/domain/entity/boardnode/drawing-element-node.entity'; -import { drawingElementFactory } from '@shared/testing/factory/domainobject/board/drawing-element.do.factory'; -import { BoardDoBuilder, BoardNodeType } from './types'; - -describe(DrawingElementNode.name, () => { - describe('when trying to create a drawing element', () => { - const setup = () => { - const elementProps = { description: '' }; - const builder: DeepMocked = createMock(); - - return { elementProps, builder }; - }; - - it('should create a DrawingElementNode', () => { - const { elementProps } = setup(); - - const element = new DrawingElementNode(elementProps); - - expect(element.type).toEqual(BoardNodeType.DRAWING_ELEMENT); - }); - }); - - describe('useDoBuilder()', () => { - const setup = () => { - const element = new DrawingElementNode({ description: '' }); - const builder: DeepMocked = createMock(); - const elementDo = drawingElementFactory.build(); - - builder.buildDrawingElement.mockReturnValue(elementDo); - - return { element, builder, elementDo }; - }; - - it('should call the specific builder method', () => { - const { element, builder } = setup(); - - element.useDoBuilder(builder); - - expect(builder.buildDrawingElement).toHaveBeenCalledWith(element); - }); - - it('should call the specific builder method', () => { - const { element, builder } = setup(); - - element.useDoBuilder(builder); - - expect(builder.buildDrawingElement).toHaveBeenCalledWith(element); - }); - - it('should return DrawingElementDo', () => { - const { element, builder, elementDo } = setup(); - - const result = element.useDoBuilder(builder); - - expect(result).toEqual(elementDo); - }); - }); -}); diff --git a/apps/server/src/shared/domain/entity/boardnode/drawing-element-node.entity.ts b/apps/server/src/shared/domain/entity/boardnode/drawing-element-node.entity.ts deleted file mode 100644 index 471e2290220..00000000000 --- a/apps/server/src/shared/domain/entity/boardnode/drawing-element-node.entity.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Entity, Property } from '@mikro-orm/core'; -import { AnyBoardDo } from '@shared/domain/domainobject'; -import { BoardNode, BoardNodeProps } from './boardnode.entity'; -import { BoardDoBuilder, BoardNodeType } from './types'; - -@Entity({ discriminatorValue: BoardNodeType.DRAWING_ELEMENT }) -export class DrawingElementNode extends BoardNode { - @Property() - description: string; - - constructor(props: DrawingElementNodeProps) { - super(props); - this.type = BoardNodeType.DRAWING_ELEMENT; - this.description = props.description; - } - - useDoBuilder(builder: BoardDoBuilder): AnyBoardDo { - const domainObject = builder.buildDrawingElement(this); - return domainObject; - } -} - -export interface DrawingElementNodeProps extends BoardNodeProps { - description: string; -} diff --git a/apps/server/src/shared/domain/entity/boardnode/external-tool-element-node.entity.spec.ts b/apps/server/src/shared/domain/entity/boardnode/external-tool-element-node.entity.spec.ts deleted file mode 100644 index 954e4ddc19f..00000000000 --- a/apps/server/src/shared/domain/entity/boardnode/external-tool-element-node.entity.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { contextExternalToolEntityFactory } from '@modules/tool/context-external-tool/testing'; -import { ExternalToolElement } from '@shared/domain/domainobject'; -import { externalToolElementFactory, setupEntities } from '@shared/testing'; -import { ExternalToolElementNodeEntity, ExternalToolElementNodeEntityProps } from './external-tool-element-node.entity'; -import { BoardDoBuilder, BoardNodeType } from './types'; - -describe(ExternalToolElementNodeEntity.name, () => { - beforeAll(async () => { - await setupEntities(); - }); - - describe('when trying to create a external tool element', () => { - const setup = () => { - const elementProps: ExternalToolElementNodeEntityProps = { - contextExternalTool: contextExternalToolEntityFactory.buildWithId(), - }; - const builder: DeepMocked = createMock(); - - return { elementProps, builder }; - }; - - it('should create a ExternalToolElementNode', () => { - const { elementProps } = setup(); - - const element = new ExternalToolElementNodeEntity(elementProps); - - expect(element.type).toEqual(BoardNodeType.EXTERNAL_TOOL); - }); - }); - - describe('useDoBuilder()', () => { - const setup = () => { - const element = new ExternalToolElementNodeEntity({ - contextExternalTool: contextExternalToolEntityFactory.buildWithId(), - }); - const builder: DeepMocked = createMock(); - const elementDo: ExternalToolElement = externalToolElementFactory.build(); - - builder.buildExternalToolElement.mockReturnValue(elementDo); - - return { element, builder, elementDo }; - }; - - it('should call the specific builder method', () => { - const { element, builder } = setup(); - - element.useDoBuilder(builder); - - expect(builder.buildExternalToolElement).toHaveBeenCalledWith(element); - }); - - it('should return ExternalToolElement', () => { - const { element, builder, elementDo } = setup(); - - const result = element.useDoBuilder(builder); - - expect(result).toEqual(elementDo); - }); - }); -}); diff --git a/apps/server/src/shared/domain/entity/boardnode/external-tool-element-node.entity.ts b/apps/server/src/shared/domain/entity/boardnode/external-tool-element-node.entity.ts deleted file mode 100644 index ffe2ef83bec..00000000000 --- a/apps/server/src/shared/domain/entity/boardnode/external-tool-element-node.entity.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Entity, ManyToOne } from '@mikro-orm/core'; -import { AnyBoardDo } from '@shared/domain/domainobject'; -import { ContextExternalToolEntity } from '@modules/tool/context-external-tool/entity/context-external-tool.entity'; -import { BoardNode, BoardNodeProps } from './boardnode.entity'; -import { BoardDoBuilder, BoardNodeType } from './types'; - -@Entity({ discriminatorValue: BoardNodeType.EXTERNAL_TOOL }) -export class ExternalToolElementNodeEntity extends BoardNode { - @ManyToOne({ nullable: true }) - contextExternalTool?: ContextExternalToolEntity; - - constructor(props: ExternalToolElementNodeEntityProps) { - super(props); - this.type = BoardNodeType.EXTERNAL_TOOL; - this.contextExternalTool = props.contextExternalTool; - } - - useDoBuilder(builder: BoardDoBuilder): AnyBoardDo { - const domainObject = builder.buildExternalToolElement(this); - return domainObject; - } -} - -export interface ExternalToolElementNodeEntityProps extends BoardNodeProps { - contextExternalTool?: ContextExternalToolEntity; -} diff --git a/apps/server/src/shared/domain/entity/boardnode/file-element-node.entity.spec.ts b/apps/server/src/shared/domain/entity/boardnode/file-element-node.entity.spec.ts deleted file mode 100644 index 050cf255f73..00000000000 --- a/apps/server/src/shared/domain/entity/boardnode/file-element-node.entity.spec.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { fileElementFactory } from '@shared/testing'; -import { FileElementNode } from './file-element-node.entity'; -import { BoardDoBuilder, BoardNodeType } from './types'; - -describe(FileElementNode.name, () => { - describe('when trying to create a file element', () => { - const setup = () => { - const elementProps = { caption: 'Test', alternativeText: 'testAltText' }; - const builder: DeepMocked = createMock(); - - return { elementProps, builder }; - }; - - it('should create a FileElementNode', () => { - const { elementProps } = setup(); - - const element = new FileElementNode(elementProps); - - expect(element.type).toEqual(BoardNodeType.FILE_ELEMENT); - }); - }); - - describe('useDoBuilder()', () => { - const setup = () => { - const element = new FileElementNode({ caption: 'Test', alternativeText: 'altTest' }); - const builder: DeepMocked = createMock(); - const elementDo = fileElementFactory.build(); - - builder.buildFileElement.mockReturnValue(elementDo); - - return { element, builder, elementDo }; - }; - - it('should call the specific builder method', () => { - const { element, builder } = setup(); - - element.useDoBuilder(builder); - - expect(builder.buildFileElement).toHaveBeenCalledWith(element); - }); - - it('should return FileElementDo', () => { - const { element, builder, elementDo } = setup(); - - const result = element.useDoBuilder(builder); - - expect(result).toEqual(elementDo); - }); - }); -}); diff --git a/apps/server/src/shared/domain/entity/boardnode/file-element-node.entity.ts b/apps/server/src/shared/domain/entity/boardnode/file-element-node.entity.ts deleted file mode 100644 index 9ac93bd4da1..00000000000 --- a/apps/server/src/shared/domain/entity/boardnode/file-element-node.entity.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Entity, Property } from '@mikro-orm/core'; -import { AnyBoardDo } from '../../domainobject'; -import { BoardNode, BoardNodeProps } from './boardnode.entity'; -import { BoardDoBuilder, BoardNodeType } from './types'; - -@Entity({ discriminatorValue: BoardNodeType.FILE_ELEMENT }) -export class FileElementNode extends BoardNode { - @Property() - caption: string; - - @Property() - alternativeText: string; - - constructor(props: FileElementNodeProps) { - super(props); - this.type = BoardNodeType.FILE_ELEMENT; - this.caption = props.caption; - this.alternativeText = props.alternativeText; - } - - useDoBuilder(builder: BoardDoBuilder): AnyBoardDo { - const domainObject = builder.buildFileElement(this); - - return domainObject; - } -} - -export interface FileElementNodeProps extends BoardNodeProps { - caption: string; - alternativeText: string; -} diff --git a/apps/server/src/shared/domain/entity/boardnode/index.ts b/apps/server/src/shared/domain/entity/boardnode/index.ts deleted file mode 100644 index 258859f83cc..00000000000 --- a/apps/server/src/shared/domain/entity/boardnode/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -export * from './boardnode.entity'; -export * from './card-node.entity'; -export * from './collaborative-text-editor-element-node.entity'; -export * from './column-board-node.entity'; -export * from './column-node.entity'; -export * from './drawing-element-node.entity'; -export * from './external-tool-element-node.entity'; -export * from './file-element-node.entity'; -export * from './link-element-node.entity'; -export * from './media-board'; -export * from './rich-text-element-node.entity'; -export * from './root-board-node.entity'; -export * from './submission-container-element-node.entity'; -export * from './submission-item-node.entity'; -export * from './types'; diff --git a/apps/server/src/shared/domain/entity/boardnode/link-element-node.entity.spec.ts b/apps/server/src/shared/domain/entity/boardnode/link-element-node.entity.spec.ts deleted file mode 100644 index 1093e57922e..00000000000 --- a/apps/server/src/shared/domain/entity/boardnode/link-element-node.entity.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { linkElementFactory } from '@shared/testing'; -import { LinkElementNode } from './link-element-node.entity'; -import { BoardDoBuilder, BoardNodeType } from './types'; - -describe(LinkElementNode.name, () => { - describe('when trying to create a link element', () => { - const setup = () => { - const elementProps = { url: 'https://www.any-fake.url/that-is-linked.html', title: 'A Great WebPage' }; - const builder: DeepMocked = createMock(); - - return { elementProps, builder }; - }; - - it('should create a LinkElementNode', () => { - const { elementProps } = setup(); - - const element = new LinkElementNode(elementProps); - - expect(element.type).toEqual(BoardNodeType.LINK_ELEMENT); - }); - }); - - describe('useDoBuilder()', () => { - const setup = () => { - const element = new LinkElementNode({ - url: 'https://www.any-fake.url/that-is-linked.html', - title: 'A Great WebPage', - }); - const builder: DeepMocked = createMock(); - const elementDo = linkElementFactory.build(); - - builder.buildLinkElement.mockReturnValue(elementDo); - - return { element, builder, elementDo }; - }; - - it('should call the specific builder method', () => { - const { element, builder } = setup(); - - element.useDoBuilder(builder); - - expect(builder.buildLinkElement).toHaveBeenCalledWith(element); - }); - - it('should return RichTextElementDo', () => { - const { element, builder, elementDo } = setup(); - - const result = element.useDoBuilder(builder); - - expect(result).toEqual(elementDo); - }); - }); -}); diff --git a/apps/server/src/shared/domain/entity/boardnode/link-element-node.entity.ts b/apps/server/src/shared/domain/entity/boardnode/link-element-node.entity.ts deleted file mode 100644 index 0102821d97b..00000000000 --- a/apps/server/src/shared/domain/entity/boardnode/link-element-node.entity.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Entity, Property } from '@mikro-orm/core'; -import { AnyBoardDo } from '../../domainobject'; -import { BoardNode, BoardNodeProps } from './boardnode.entity'; -import { BoardDoBuilder, BoardNodeType } from './types'; - -@Entity({ discriminatorValue: BoardNodeType.LINK_ELEMENT }) -export class LinkElementNode extends BoardNode { - @Property() - url: string; - - @Property() - title: string; - - @Property() - imageUrl?: string; - - constructor(props: LinkElementNodeProps) { - super(props); - this.type = BoardNodeType.LINK_ELEMENT; - this.url = props.url; - this.title = props.title; - this.imageUrl = props.imageUrl; - } - - useDoBuilder(builder: BoardDoBuilder): AnyBoardDo { - const domainObject = builder.buildLinkElement(this); - - return domainObject; - } -} - -export interface LinkElementNodeProps extends BoardNodeProps { - url: string; - title: string; - imageUrl?: string; -} diff --git a/apps/server/src/shared/domain/entity/boardnode/media-board/index.ts b/apps/server/src/shared/domain/entity/boardnode/media-board/index.ts deleted file mode 100644 index 36aceea2094..00000000000 --- a/apps/server/src/shared/domain/entity/boardnode/media-board/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { MediaBoardNode } from './media-board-node.entity'; -export { MediaLineNode, MediaLineNodeProps } from './media-line-node.entity'; -export { - MediaExternalToolElementNode, - MediaExternalToolElementNodeProps, -} from './media-external-tool-element-node.entity'; diff --git a/apps/server/src/shared/domain/entity/boardnode/media-board/media-board-node.entity.ts b/apps/server/src/shared/domain/entity/boardnode/media-board/media-board-node.entity.ts deleted file mode 100644 index 50a5ea2a8e3..00000000000 --- a/apps/server/src/shared/domain/entity/boardnode/media-board/media-board-node.entity.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Entity, Enum, Index, Property } from '@mikro-orm/core'; -import { ObjectId } from '@mikro-orm/mongodb'; -import { MediaBoardColors, MediaBoardLayoutType } from '@modules/board/domain'; -import { - type AnyBoardDo, - BoardExternalReference, - BoardExternalReferenceType, - type MediaBoard, -} from '../../../domainobject'; -import { BoardNode, BoardNodeProps } from '../boardnode.entity'; -import { type BoardDoBuilder, BoardNodeType } from '../types'; - -// TODO Use an abstract base class for root nodes that have a contextId and a contextType. Multiple STI abstract base classes are blocked by MikroORM 6.1.2 (issue #3745) -@Entity({ discriminatorValue: BoardNodeType.MEDIA_BOARD }) -@Index({ properties: ['_contextId'] }) -@Index({ properties: ['_contextType'] }) -export class MediaBoardNode extends BoardNode { - constructor(props: MediaBoardNodeProps) { - super(props); - this.type = BoardNodeType.MEDIA_BOARD; - - this._contextType = props.context.type; - this._contextId = new ObjectId(props.context.id); - this.layout = props.layout; - this.mediaAvailableLineCollapsed = props.mediaAvailableLineCollapsed; - this.mediaAvailableLineBackgroundColor = props.mediaAvailableLineBackgroundColor; - } - - @Property({ fieldName: 'contextType' }) - _contextType: BoardExternalReferenceType; - - @Property({ fieldName: 'context' }) - _contextId: ObjectId; - - @Enum(() => MediaBoardLayoutType) - layout: MediaBoardLayoutType; - - @Enum(() => MediaBoardColors) - mediaAvailableLineBackgroundColor: MediaBoardColors; - - @Property() - mediaAvailableLineCollapsed: boolean; - - get context(): BoardExternalReference { - return { - type: this._contextType, - id: this._contextId.toHexString(), - }; - } - - useDoBuilder(builder: BoardDoBuilder): AnyBoardDo { - const domainObject: MediaBoard = builder.buildMediaBoard(this); - - return domainObject; - } -} - -export interface MediaBoardNodeProps extends BoardNodeProps { - context: BoardExternalReference; - layout: MediaBoardLayoutType; - mediaAvailableLineBackgroundColor: MediaBoardColors; - mediaAvailableLineCollapsed: boolean; -} diff --git a/apps/server/src/shared/domain/entity/boardnode/media-board/media-external-tool-element-node.entity.ts b/apps/server/src/shared/domain/entity/boardnode/media-board/media-external-tool-element-node.entity.ts deleted file mode 100644 index 970495005c1..00000000000 --- a/apps/server/src/shared/domain/entity/boardnode/media-board/media-external-tool-element-node.entity.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Entity, ManyToOne } from '@mikro-orm/core'; -import { ContextExternalToolEntity } from '@modules/tool/context-external-tool/entity'; -import type { AnyBoardDo, MediaExternalToolElement } from '../../../domainobject'; -import { BoardNode, type BoardNodeProps } from '../boardnode.entity'; -import { type BoardDoBuilder, BoardNodeType } from '../types'; - -@Entity({ discriminatorValue: BoardNodeType.MEDIA_EXTERNAL_TOOL_ELEMENT }) -export class MediaExternalToolElementNode extends BoardNode { - @ManyToOne() - contextExternalTool: ContextExternalToolEntity; - - constructor(props: MediaExternalToolElementNodeProps) { - super(props); - this.type = BoardNodeType.MEDIA_EXTERNAL_TOOL_ELEMENT; - this.contextExternalTool = props.contextExternalTool; - } - - useDoBuilder(builder: BoardDoBuilder): AnyBoardDo { - const domainObject: MediaExternalToolElement = builder.buildMediaExternalToolElement(this); - return domainObject; - } -} - -export interface MediaExternalToolElementNodeProps extends BoardNodeProps { - contextExternalTool: ContextExternalToolEntity; -} diff --git a/apps/server/src/shared/domain/entity/boardnode/media-board/media-line-node.entity.ts b/apps/server/src/shared/domain/entity/boardnode/media-board/media-line-node.entity.ts deleted file mode 100644 index 511ca16735b..00000000000 --- a/apps/server/src/shared/domain/entity/boardnode/media-board/media-line-node.entity.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Entity, Enum, Property } from '@mikro-orm/core'; -import { MediaBoardColors } from '@modules/board/domain'; -import type { AnyBoardDo, MediaLine } from '../../../domainobject'; -import { BoardNode, type BoardNodeProps } from '../boardnode.entity'; -import { type BoardDoBuilder, BoardNodeType } from '../types'; - -@Entity({ discriminatorValue: BoardNodeType.MEDIA_LINE }) -export class MediaLineNode extends BoardNode { - constructor(props: MediaLineNodeProps) { - super(props); - this.type = BoardNodeType.MEDIA_LINE; - - this.title = props.title; - this.backgroundColor = props.backgroundColor; - this.collapsed = props.collapsed; - } - - @Property() - title: string; - - @Enum(() => MediaBoardColors) - backgroundColor: MediaBoardColors; - - @Property() - collapsed: boolean; - - useDoBuilder(builder: BoardDoBuilder): AnyBoardDo { - const domainObject: MediaLine = builder.buildMediaLine(this); - return domainObject; - } -} - -export interface MediaLineNodeProps extends BoardNodeProps { - title: string; - backgroundColor: MediaBoardColors; - collapsed: boolean; -} diff --git a/apps/server/src/shared/domain/entity/boardnode/rich-text-element-node.entity.spec.ts b/apps/server/src/shared/domain/entity/boardnode/rich-text-element-node.entity.spec.ts deleted file mode 100644 index fdb7a691ca0..00000000000 --- a/apps/server/src/shared/domain/entity/boardnode/rich-text-element-node.entity.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { InputFormat } from '@shared/domain/types'; -import { richTextElementFactory } from '@shared/testing'; -import { RichTextElementNode } from './rich-text-element-node.entity'; -import { BoardDoBuilder, BoardNodeType } from './types'; - -describe(RichTextElementNode.name, () => { - describe('when trying to create a rich text element', () => { - const setup = () => { - const elementProps = { text: 'Test', inputFormat: InputFormat.RICH_TEXT_CK5 }; - const builder: DeepMocked = createMock(); - - return { elementProps, builder }; - }; - - it('should create a FileElementNode', () => { - const { elementProps } = setup(); - - const element = new RichTextElementNode(elementProps); - - expect(element.type).toEqual(BoardNodeType.RICH_TEXT_ELEMENT); - }); - }); - - describe('useDoBuilder()', () => { - const setup = () => { - const element = new RichTextElementNode({ text: 'Test', inputFormat: InputFormat.RICH_TEXT_CK5 }); - const builder: DeepMocked = createMock(); - const elementDo = richTextElementFactory.build(); - - builder.buildRichTextElement.mockReturnValue(elementDo); - - return { element, builder, elementDo }; - }; - - it('should call the specific builder method', () => { - const { element, builder } = setup(); - - element.useDoBuilder(builder); - - expect(builder.buildRichTextElement).toHaveBeenCalledWith(element); - }); - - it('should return RichTextElementDo', () => { - const { element, builder, elementDo } = setup(); - - const result = element.useDoBuilder(builder); - - expect(result).toEqual(elementDo); - }); - }); -}); diff --git a/apps/server/src/shared/domain/entity/boardnode/rich-text-element-node.entity.ts b/apps/server/src/shared/domain/entity/boardnode/rich-text-element-node.entity.ts deleted file mode 100644 index 4c3a356a812..00000000000 --- a/apps/server/src/shared/domain/entity/boardnode/rich-text-element-node.entity.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Entity, Property } from '@mikro-orm/core'; -import { AnyBoardDo } from '@shared/domain/domainobject'; -import { InputFormat } from '@shared/domain/types'; -import { BoardNode, BoardNodeProps } from './boardnode.entity'; -import { BoardDoBuilder, BoardNodeType } from './types'; - -@Entity({ discriminatorValue: BoardNodeType.RICH_TEXT_ELEMENT }) -export class RichTextElementNode extends BoardNode { - @Property() - text: string; - - @Property() - inputFormat: InputFormat; - - constructor(props: RichTextElementNodeProps) { - super(props); - this.type = BoardNodeType.RICH_TEXT_ELEMENT; - this.text = props.text; - this.inputFormat = props.inputFormat; - } - - useDoBuilder(builder: BoardDoBuilder): AnyBoardDo { - const domainObject = builder.buildRichTextElement(this); - return domainObject; - } -} - -export interface RichTextElementNodeProps extends BoardNodeProps { - text: string; - inputFormat: InputFormat; -} diff --git a/apps/server/src/shared/domain/entity/boardnode/root-board-node.entity.ts b/apps/server/src/shared/domain/entity/boardnode/root-board-node.entity.ts deleted file mode 100644 index 77808057040..00000000000 --- a/apps/server/src/shared/domain/entity/boardnode/root-board-node.entity.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { BoardExternalReference } from '@shared/domain/domainobject/board/types'; -import { BoardNodeProps } from './boardnode.entity'; - -// TODO Use an abstract base class for root nodes that have a contextId and a contextType. Multiple STI abstract base classes are blocked by MikroORM 6.1.2 (issue #3745) -// export abstract class RootBoardNode extends BoardNode { ... } - -export interface RootBoardNodeProps extends BoardNodeProps { - context: BoardExternalReference; -} diff --git a/apps/server/src/shared/domain/entity/boardnode/submission-container-element-node.entity.spec.ts b/apps/server/src/shared/domain/entity/boardnode/submission-container-element-node.entity.spec.ts deleted file mode 100644 index 461277483fe..00000000000 --- a/apps/server/src/shared/domain/entity/boardnode/submission-container-element-node.entity.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { submissionContainerElementFactory } from '@shared/testing'; -import { SubmissionContainerElementNode } from './submission-container-element-node.entity'; -import { BoardDoBuilder, BoardNodeType } from './types'; - -const inThreeDays = new Date(Date.now() + 259200000); - -describe(SubmissionContainerElementNode.name, () => { - describe('when trying to create a submission container element', () => { - const setup = () => { - const elementProps = { dueDate: inThreeDays }; - const builder: DeepMocked = createMock(); - - return { elementProps, builder }; - }; - - it('should create a SubmissionContainerElement', () => { - const { elementProps } = setup(); - - const element = new SubmissionContainerElementNode(elementProps); - - expect(element.type).toEqual(BoardNodeType.SUBMISSION_CONTAINER_ELEMENT); - }); - }); - - describe('useDoBuilder()', () => { - const setup = () => { - const element = new SubmissionContainerElementNode({ dueDate: inThreeDays }); - const builder: DeepMocked = createMock(); - const elementDo = submissionContainerElementFactory.build(); - - builder.buildSubmissionContainerElement.mockReturnValue(elementDo); - - return { element, builder, elementDo }; - }; - - it('should call the specific builder method', () => { - const { element, builder } = setup(); - - element.useDoBuilder(builder); - - expect(builder.buildSubmissionContainerElement).toHaveBeenCalledWith(element); - }); - - it('should return ElementDo', () => { - const { element, builder, elementDo } = setup(); - - const result = element.useDoBuilder(builder); - - expect(result).toEqual(elementDo); - }); - }); -}); diff --git a/apps/server/src/shared/domain/entity/boardnode/submission-container-element-node.entity.ts b/apps/server/src/shared/domain/entity/boardnode/submission-container-element-node.entity.ts deleted file mode 100644 index b47cf916784..00000000000 --- a/apps/server/src/shared/domain/entity/boardnode/submission-container-element-node.entity.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Entity, Property } from '@mikro-orm/core'; -import { AnyBoardDo } from '../../domainobject'; -import { BoardNode, BoardNodeProps } from './boardnode.entity'; -import { BoardDoBuilder, BoardNodeType } from './types'; - -@Entity({ discriminatorValue: BoardNodeType.SUBMISSION_CONTAINER_ELEMENT }) -export class SubmissionContainerElementNode extends BoardNode { - @Property({ type: Date, nullable: true }) - dueDate: Date | null; - - constructor(props: SubmissionContainerNodeProps) { - super(props); - this.type = BoardNodeType.SUBMISSION_CONTAINER_ELEMENT; - this.dueDate = props.dueDate; - } - - useDoBuilder(builder: BoardDoBuilder): AnyBoardDo { - const domainObject = builder.buildSubmissionContainerElement(this); - - return domainObject; - } -} - -export interface SubmissionContainerNodeProps extends BoardNodeProps { - dueDate: Date | null; -} diff --git a/apps/server/src/shared/domain/entity/boardnode/submission-item-node.entity.spec.ts b/apps/server/src/shared/domain/entity/boardnode/submission-item-node.entity.spec.ts deleted file mode 100644 index ebf302861b2..00000000000 --- a/apps/server/src/shared/domain/entity/boardnode/submission-item-node.entity.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { submissionItemFactory } from '@shared/testing'; -import { ObjectId } from '@mikro-orm/mongodb'; -import { SubmissionItemNode } from './submission-item-node.entity'; -import { BoardDoBuilder, BoardNodeType } from './types'; - -describe(SubmissionItemNode.name, () => { - describe('when trying to create a submission container element', () => { - const setup = () => { - const elementProps = { completed: false, userId: ObjectId.toString() }; - const builder: DeepMocked = createMock(); - - return { elementProps, builder }; - }; - - it('should create a SubmissionItem', () => { - const { elementProps } = setup(); - - const element = new SubmissionItemNode(elementProps); - - expect(element.type).toEqual(BoardNodeType.SUBMISSION_ITEM); - }); - }); - - describe('useDoBuilder()', () => { - const setup = () => { - const elementProps = { completed: false, userId: ObjectId.toString() }; - const element = new SubmissionItemNode(elementProps); - - const builder: DeepMocked = createMock(); - const elementDo = submissionItemFactory.build(); - - builder.buildSubmissionItem.mockReturnValue(elementDo); - - return { element, builder, elementDo }; - }; - - it('should call the specific builder method', () => { - const { element, builder } = setup(); - - element.useDoBuilder(builder); - - expect(builder.buildSubmissionItem).toHaveBeenCalledWith(element); - }); - - it('should return ElementDo', () => { - const { element, builder, elementDo } = setup(); - - const result = element.useDoBuilder(builder); - - expect(result).toEqual(elementDo); - }); - }); -}); diff --git a/apps/server/src/shared/domain/entity/boardnode/submission-item-node.entity.ts b/apps/server/src/shared/domain/entity/boardnode/submission-item-node.entity.ts deleted file mode 100644 index b265a3fb710..00000000000 --- a/apps/server/src/shared/domain/entity/boardnode/submission-item-node.entity.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Entity, Property } from '@mikro-orm/core'; -import { EntityId } from '@shared/domain/types'; -import { AnyBoardDo } from '../../domainobject'; -import { BoardNode, BoardNodeProps } from './boardnode.entity'; -import { BoardDoBuilder, BoardNodeType } from './types'; - -@Entity({ discriminatorValue: BoardNodeType.SUBMISSION_ITEM }) -export class SubmissionItemNode extends BoardNode { - @Property() - completed!: boolean; - - // @Index() // TODO if enabled tests in management fails with ERROR [ExceptionsHandler] Failed to create indexes - @Property({ - comment: 'The user whos submission this is. Usually the student submitting the work.', - }) - userId!: EntityId; - - constructor(props: SubmissionItemNodeProps) { - super(props); - this.type = BoardNodeType.SUBMISSION_ITEM; - this.completed = props.completed; - this.userId = props.userId; - } - - useDoBuilder(builder: BoardDoBuilder): AnyBoardDo { - const domainObject = builder.buildSubmissionItem(this); - - return domainObject; - } -} - -export interface SubmissionItemNodeProps extends BoardNodeProps { - completed: boolean; - userId: EntityId; -} diff --git a/apps/server/src/shared/domain/entity/boardnode/types/board-do.builder.ts b/apps/server/src/shared/domain/entity/boardnode/types/board-do.builder.ts deleted file mode 100644 index d475836b8e5..00000000000 --- a/apps/server/src/shared/domain/entity/boardnode/types/board-do.builder.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { - Card, - CollaborativeTextEditorElement, - Column, - ColumnBoard, - DrawingElement, - ExternalToolElement, - FileElement, - LinkElement, - MediaBoard, - MediaExternalToolElement, - MediaLine, - RichTextElement, - SubmissionContainerElement, - SubmissionItem, -} from '../../../domainobject'; -import type { CardNode } from '../card-node.entity'; -import type { CollaborativeTextEditorElementNode } from '../collaborative-text-editor-element-node.entity'; -import type { ColumnBoardNode } from '../column-board-node.entity'; -import type { ColumnNode } from '../column-node.entity'; -import type { DrawingElementNode } from '../drawing-element-node.entity'; -import type { ExternalToolElementNodeEntity } from '../external-tool-element-node.entity'; -import type { FileElementNode } from '../file-element-node.entity'; -import type { LinkElementNode } from '../link-element-node.entity'; -import type { MediaBoardNode, MediaExternalToolElementNode, MediaLineNode } from '../media-board'; -import type { RichTextElementNode } from '../rich-text-element-node.entity'; -import type { SubmissionContainerElementNode } from '../submission-container-element-node.entity'; -import type { SubmissionItemNode } from '../submission-item-node.entity'; - -export interface BoardDoBuilder { - buildColumnBoard(boardNode: ColumnBoardNode): ColumnBoard; - buildColumn(boardNode: ColumnNode): Column; - buildCard(boardNode: CardNode): Card; - buildDrawingElement(boardNode: DrawingElementNode): DrawingElement; - buildFileElement(boardNode: FileElementNode): FileElement; - buildLinkElement(boardNode: LinkElementNode): LinkElement; - buildRichTextElement(boardNode: RichTextElementNode): RichTextElement; - buildSubmissionContainerElement(boardNode: SubmissionContainerElementNode): SubmissionContainerElement; - buildSubmissionItem(boardNode: SubmissionItemNode): SubmissionItem; - buildExternalToolElement(boardNode: ExternalToolElementNodeEntity): ExternalToolElement; - buildCollaborativeTextEditorElement(boardNode: CollaborativeTextEditorElementNode): CollaborativeTextEditorElement; - - buildMediaBoard(boardNode: MediaBoardNode): MediaBoard; - buildMediaLine(boardNode: MediaLineNode): MediaLine; - buildMediaExternalToolElement(boardNode: MediaExternalToolElementNode): MediaExternalToolElement; -} diff --git a/apps/server/src/shared/domain/entity/boardnode/types/index.ts b/apps/server/src/shared/domain/entity/boardnode/types/index.ts deleted file mode 100644 index e2ead1d7ef0..00000000000 --- a/apps/server/src/shared/domain/entity/boardnode/types/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './board-node-type'; -export * from './board-do.builder'; diff --git a/apps/server/src/shared/domain/entity/column-board-node.entity.spec.ts b/apps/server/src/shared/domain/entity/column-board-node.entity.spec.ts new file mode 100644 index 00000000000..b632cdbf500 --- /dev/null +++ b/apps/server/src/shared/domain/entity/column-board-node.entity.spec.ts @@ -0,0 +1,20 @@ +import { columnBoardNodeFactory } from '@shared/testing'; +import { ColumnBoardNode } from './column-board-node.entity'; + +describe(ColumnBoardNode.name, () => { + it('should be able to be published', () => { + const nodeEntity = columnBoardNodeFactory.build({ isVisible: false }); + + nodeEntity.publish(); + + expect(nodeEntity.isVisible).toBe(true); + }); + + it('should be able to be unpublished', () => { + const nodeEntity = columnBoardNodeFactory.build({ isVisible: true }); + + nodeEntity.unpublish(); + + expect(nodeEntity.isVisible).toBe(false); + }); +}); diff --git a/apps/server/src/shared/domain/entity/column-board-node.entity.ts b/apps/server/src/shared/domain/entity/column-board-node.entity.ts new file mode 100644 index 00000000000..561da89fbd7 --- /dev/null +++ b/apps/server/src/shared/domain/entity/column-board-node.entity.ts @@ -0,0 +1,61 @@ +import { Entity, Property } from '@mikro-orm/core'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { + BoardExternalReference, + BoardExternalReferenceType, +} from '@modules/board/domain/types/board-external-reference'; +import { BoardLayout } from '@modules/board/domain/types/board-layout.enum'; +import { LearnroomElement } from '../interface'; +import { BaseEntityWithTimestamps } from './base.entity'; + +// TODO comment +/** + * @deprecated - this is here only for the sake of the legacy-board (lernraum) + */ +@Entity({ tableName: 'boardnodes' }) +export class ColumnBoardNode extends BaseEntityWithTimestamps implements LearnroomElement { + constructor(props: ColumnBoardNodeProps) { + super(); + this.title = props.title; + this.isVisible = props.isVisible; + this.layout = props.layout; + this.contextType = props.context.type; + this.contextId = new ObjectId(props.context.id); + } + + @Property({ nullable: false }) + title!: string; + + @Property({ type: 'boolean', nullable: false }) + isVisible!: boolean; + + @Property({ nullable: false }) + layout!: BoardLayout; + + @Property({ fieldName: 'contextType' }) + contextType: BoardExternalReferenceType; + + @Property({ fieldName: 'context' }) + contextId: ObjectId; + + /** + * @deprecated - this is here only for the sake of the legacy-board (lernraum) + */ + publish(): void { + this.isVisible = true; + } + + /** + * @deprecated - this is here only for the sake of the legacy-board (lernraum) + */ + unpublish(): void { + this.isVisible = false; + } +} + +export interface ColumnBoardNodeProps { + title: string; + isVisible: boolean; + layout: BoardLayout; + context: BoardExternalReference; +} diff --git a/apps/server/src/shared/domain/entity/index.ts b/apps/server/src/shared/domain/entity/index.ts index 64ce19e2871..9d023584dba 100644 --- a/apps/server/src/shared/domain/entity/index.ts +++ b/apps/server/src/shared/domain/entity/index.ts @@ -1,6 +1,6 @@ export * from './all-entities'; export * from './base.entity'; -export * from './boardnode'; +export * from './column-board-node.entity'; export * from './course.entity'; export * from './coursegroup.entity'; export * from './dashboard.entity'; diff --git a/apps/server/src/shared/domain/entity/legacy-board/boardElement.entity.spec.ts b/apps/server/src/shared/domain/entity/legacy-board/boardElement.entity.spec.ts index 1bdc4d8ed55..dc9524bded0 100644 --- a/apps/server/src/shared/domain/entity/legacy-board/boardElement.entity.spec.ts +++ b/apps/server/src/shared/domain/entity/legacy-board/boardElement.entity.spec.ts @@ -1,6 +1,6 @@ import { columnBoardNodeFactory, lessonFactory, setupEntities, taskFactory } from '@shared/testing'; -import { LegacyBoardElementType } from './legacy-boardelement.entity'; import { ColumnboardBoardElement } from './column-board-boardelement'; +import { LegacyBoardElementType } from './legacy-boardelement.entity'; import { LessonBoardElement } from './lesson-boardelement.entity'; import { TaskBoardElement } from './task-boardelement.entity'; diff --git a/apps/server/src/shared/domain/entity/legacy-board/column-board-boardelement.ts b/apps/server/src/shared/domain/entity/legacy-board/column-board-boardelement.ts index d4a4ed40d2a..f6aff2a29b7 100644 --- a/apps/server/src/shared/domain/entity/legacy-board/column-board-boardelement.ts +++ b/apps/server/src/shared/domain/entity/legacy-board/column-board-boardelement.ts @@ -1,6 +1,6 @@ import { Entity, ManyToOne } from '@mikro-orm/core'; +import { ColumnBoardNode } from '../column-board-node.entity'; import { LegacyBoardElement, LegacyBoardElementType } from './legacy-boardelement.entity'; -import { ColumnBoardNode } from '../boardnode/column-board-node.entity'; @Entity({ discriminatorValue: LegacyBoardElementType.ColumnBoard }) export class ColumnboardBoardElement extends LegacyBoardElement { diff --git a/apps/server/src/shared/domain/entity/legacy-board/legacy-board.entity.spec.ts b/apps/server/src/shared/domain/entity/legacy-board/legacy-board.entity.spec.ts index 26f5157ef27..9b7373bd79e 100644 --- a/apps/server/src/shared/domain/entity/legacy-board/legacy-board.entity.spec.ts +++ b/apps/server/src/shared/domain/entity/legacy-board/legacy-board.entity.spec.ts @@ -2,7 +2,6 @@ import { BadRequestException } from '@nestjs/common'; import { boardFactory, columnboardBoardElementFactory, - columnBoardFactory, columnBoardNodeFactory, courseFactory, lessonBoardElementFactory, @@ -12,6 +11,8 @@ import { taskFactory, } from '@shared/testing'; +import { columnBoardFactory } from '@modules/board/testing'; + describe('Board Entity', () => { beforeAll(async () => { await setupEntities(); diff --git a/apps/server/src/shared/domain/entity/legacy-board/legacy-board.entity.ts b/apps/server/src/shared/domain/entity/legacy-board/legacy-board.entity.ts index df3208a5b73..03255c6c86c 100644 --- a/apps/server/src/shared/domain/entity/legacy-board/legacy-board.entity.ts +++ b/apps/server/src/shared/domain/entity/legacy-board/legacy-board.entity.ts @@ -3,14 +3,14 @@ import { BadRequestException, NotFoundException } from '@nestjs/common'; import { LearnroomElement } from '../../interface'; import { EntityId } from '../../types'; import { BaseEntityWithTimestamps } from '../base.entity'; +import { ColumnBoardNode } from '../column-board-node.entity'; import type { Course } from '../course.entity'; import { LessonEntity } from '../lesson.entity'; import { Task } from '../task.entity'; -import { LegacyBoardElement, LegacyBoardElementReference } from './legacy-boardelement.entity'; import { ColumnboardBoardElement } from './column-board-boardelement'; +import { LegacyBoardElement, LegacyBoardElementReference } from './legacy-boardelement.entity'; import { LessonBoardElement } from './lesson-boardelement.entity'; import { TaskBoardElement } from './task-boardelement.entity'; -import { ColumnBoardNode } from '../boardnode/column-board-node.entity'; export type BoardProps = { references: LegacyBoardElement[]; diff --git a/apps/server/src/shared/domain/entity/legacy-board/legacy-boardelement.entity.ts b/apps/server/src/shared/domain/entity/legacy-board/legacy-boardelement.entity.ts index 43ebb4f5bb7..8ffb805c6a9 100644 --- a/apps/server/src/shared/domain/entity/legacy-board/legacy-boardelement.entity.ts +++ b/apps/server/src/shared/domain/entity/legacy-board/legacy-boardelement.entity.ts @@ -1,9 +1,9 @@ import { Entity, Enum } from '@mikro-orm/core'; import { EntityId } from '../../types'; import { BaseEntityWithTimestamps } from '../base.entity'; +import { ColumnBoardNode } from '../column-board-node.entity'; import { LessonEntity } from '../lesson.entity'; import { Task } from '../task.entity'; -import { ColumnBoardNode } from '../boardnode/column-board-node.entity'; export type LegacyBoardElementReference = Task | LessonEntity | ColumnBoardNode; diff --git a/apps/server/src/shared/repo/importuser/importuser.repo.integration.spec.ts b/apps/server/src/shared/repo/importuser/importuser.repo.integration.spec.ts index 9f70c446495..82971ecbbd9 100644 --- a/apps/server/src/shared/repo/importuser/importuser.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/importuser/importuser.repo.integration.spec.ts @@ -5,7 +5,13 @@ import { Test, TestingModule } from '@nestjs/testing'; import { IImportUserRoleName, ImportUser, MatchCreator, SchoolEntity, User } from '@shared/domain/entity'; import { RoleName } from '@shared/domain/interface'; import { MatchCreatorScope } from '@shared/domain/types'; -import { cleanupCollections, importUserFactory, schoolEntityFactory, userFactory } from '@shared/testing'; +import { + cleanupCollections, + createCollections, + importUserFactory, + schoolEntityFactory, + userFactory, +} from '@shared/testing'; import { ImportUserRepo } from '.'; describe('ImportUserRepo', () => { @@ -29,6 +35,8 @@ describe('ImportUserRepo', () => { repo = module.get(ImportUserRepo); em = module.get(EntityManager); orm = module.get(MikroORM); + + await createCollections(em); }); afterAll(async () => { diff --git a/apps/server/src/shared/repo/legacy-board/legacy-board.repo.ts b/apps/server/src/shared/repo/legacy-board/legacy-board.repo.ts index 49c64437f6e..1583c1701a8 100644 --- a/apps/server/src/shared/repo/legacy-board/legacy-board.repo.ts +++ b/apps/server/src/shared/repo/legacy-board/legacy-board.repo.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; import { - LegacyBoard, ColumnboardBoardElement, Course, + LegacyBoard, LessonBoardElement, TaskBoardElement, } from '@shared/domain/entity'; diff --git a/apps/server/src/shared/repo/types/object-id.type.spec.ts b/apps/server/src/shared/repo/types/object-id.type.spec.ts new file mode 100644 index 00000000000..eea2768fbb6 --- /dev/null +++ b/apps/server/src/shared/repo/types/object-id.type.spec.ts @@ -0,0 +1,79 @@ +import { Platform } from '@mikro-orm/core'; +import { MongoPlatform, ObjectId } from '@mikro-orm/mongodb'; +import { ObjectIdType } from './object-id.type'; + +class InvalidPlatform extends Platform {} + +describe(ObjectIdType.name, () => { + describe('convertToDatabaseValue', () => { + const setup = () => { + const propType = new ObjectIdType(); + const platform = new MongoPlatform(); + const invalidPlatform = new InvalidPlatform(); + const entityId = new ObjectId().toHexString(); + const invalidEntityId = 'foobar'; + + return { propType, platform, invalidPlatform, entityId, invalidEntityId }; + }; + + describe('with valid entity id', () => { + it('should convert to mongo id', () => { + const { propType, platform, entityId } = setup(); + + const id = propType.convertToDatabaseValue(entityId, platform); + + expect(id).toBeInstanceOf(ObjectId); + expect(id.toHexString()).toBe(entityId); + }); + }); + + describe('with invalid entity id', () => { + it('should throw error', () => { + const { propType, platform, invalidEntityId } = setup(); + + const conversion = () => propType.convertToDatabaseValue(invalidEntityId, platform); + + expect(conversion).toThrowError(); + }); + }); + + describe('when platform is invalid', () => { + it('should throw error', () => { + const { propType, invalidPlatform, entityId } = setup(); + + const conversion = () => propType.convertToDatabaseValue(entityId, invalidPlatform); + + expect(conversion).toThrowError(); + }); + }); + }); + + describe('convertToJSValue', () => { + const setup = () => { + const propType = new ObjectIdType(); + const platform = new MongoPlatform(); + const invalidPlatform = new InvalidPlatform(); + const objectId = new ObjectId(); + + return { propType, platform, invalidPlatform, objectId }; + }; + + it('should return entity id', () => { + const { propType, platform, objectId } = setup(); + + const entityId = propType.convertToJSValue(objectId, platform); + + expect(entityId).toBe(objectId.toHexString()); + }); + + describe('when platform is invalid', () => { + it('should throw error', () => { + const { propType, invalidPlatform, objectId } = setup(); + + const conversion = () => propType.convertToJSValue(objectId, invalidPlatform); + + expect(conversion).toThrowError(); + }); + }); + }); +}); diff --git a/apps/server/src/shared/repo/types/object-id.type.ts b/apps/server/src/shared/repo/types/object-id.type.ts new file mode 100644 index 00000000000..aea114f4fc9 --- /dev/null +++ b/apps/server/src/shared/repo/types/object-id.type.ts @@ -0,0 +1,21 @@ +import { Platform, Type } from '@mikro-orm/core'; +import { EntityId } from '@shared/domain/types'; +import { MongoPlatform, ObjectId } from '@mikro-orm/mongodb'; + +export class ObjectIdType extends Type { + convertToDatabaseValue(value: EntityId, platform: Platform): ObjectId { + this.validatePlatformSupport(platform); + return new ObjectId(value); + } + + convertToJSValue(value: ObjectId, platform: Platform): EntityId { + this.validatePlatformSupport(platform); + return value.toHexString(); + } + + private validatePlatformSupport(platform: Platform): void { + if (!(platform instanceof MongoPlatform)) { + throw new Error('ObjectId custom type implemented only for Mongo.'); + } + } +} diff --git a/apps/server/src/shared/repo/user/user.scope.ts b/apps/server/src/shared/repo/user/user.scope.ts index 90524b2d4a8..47efd5afe71 100644 --- a/apps/server/src/shared/repo/user/user.scope.ts +++ b/apps/server/src/shared/repo/user/user.scope.ts @@ -1,6 +1,7 @@ import { User } from '@shared/domain/entity'; import { EntityId } from '@shared/domain/types'; -import { MongoPatterns, Scope } from '@shared/repo'; +import { MongoPatterns } from '@shared/repo'; +import { Scope } from '@shared/repo/scope'; export class UserScope extends Scope { isOutdated(isOutdated?: boolean): UserScope { diff --git a/apps/server/src/shared/testing/create-collections.ts b/apps/server/src/shared/testing/create-collections.ts new file mode 100644 index 00000000000..8c01a0cb053 --- /dev/null +++ b/apps/server/src/shared/testing/create-collections.ts @@ -0,0 +1,23 @@ +// When we call `ensureIndexes` we get a MikroORM error when the collection already exists. +// This is despite the ORM ignoring existing collections. That's why we create all collections +// manually for this particular test. + +import { EntityManager } from '@mikro-orm/mongodb'; + +// https://github.com/mikro-orm/mikro-orm/blob/fd56714e06e39c2724a3193b8b07279b8fb6c91f/packages/mongodb/src/MongoSchemaGenerator.ts#L30 +export const createCollections = async (em: EntityManager) => { + const collections = new Set(); + Object.values(em.getMetadata().getAll()).forEach((meta) => { + if (meta.collection) { + collections.add(meta.collection); + } + }); + await Promise.all( + Array.from(collections.values()).map(async (collection) => { + await em + .getDriver() + .getConnection() + .createCollection(collection as string); + }) + ); +}; diff --git a/apps/server/src/shared/testing/factory/axios-error.factory.ts b/apps/server/src/shared/testing/factory/axios-error.factory.ts index 089179dafef..a9b1a5b2f13 100644 --- a/apps/server/src/shared/testing/factory/axios-error.factory.ts +++ b/apps/server/src/shared/testing/factory/axios-error.factory.ts @@ -1,7 +1,7 @@ import { HttpStatus } from '@nestjs/common'; -import { axiosResponseFactory } from '@shared/testing'; import { AxiosError, AxiosHeaders } from 'axios'; import { Factory } from 'fishery'; +import { axiosResponseFactory } from './axios-response.factory'; class AxiosErrorFactory extends Factory { withError(error: unknown): this { diff --git a/apps/server/src/shared/testing/factory/base.factory.ts b/apps/server/src/shared/testing/factory/base.factory.ts index 0c9153bb2da..5db3cd71d5a 100644 --- a/apps/server/src/shared/testing/factory/base.factory.ts +++ b/apps/server/src/shared/testing/factory/base.factory.ts @@ -1,4 +1,5 @@ import { ObjectId } from '@mikro-orm/mongodb'; +import type { EntityId } from '@shared/domain/types'; import { BuildOptions, DeepPartial, Factory, GeneratorFn, HookFn } from 'fishery'; /** @@ -58,10 +59,9 @@ export class BaseFactory { * @returns an entity */ buildWithId(params?: DeepPartial, id?: string, options: BuildOptions = {}): T { - const entity = this.build(params, options); + const entity = this.build(params, options) as { _id: ObjectId; id: EntityId }; const generatedId = new ObjectId(id); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const entityWithId = Object.assign(entity as any, { _id: generatedId, id: generatedId.toHexString() }); + const entityWithId = Object.assign(entity, { _id: generatedId, id: generatedId.toHexString() }); return entityWithId as T; } diff --git a/apps/server/src/shared/testing/factory/boardelement.factory.ts b/apps/server/src/shared/testing/factory/boardelement.factory.ts index 5f373d2c02c..a17f44cac04 100644 --- a/apps/server/src/shared/testing/factory/boardelement.factory.ts +++ b/apps/server/src/shared/testing/factory/boardelement.factory.ts @@ -7,7 +7,7 @@ import { TaskBoardElement, } from '@shared/domain/entity'; import { BaseFactory } from './base.factory'; -import { columnBoardNodeFactory } from './boardnode'; +import { columnBoardNodeFactory } from './column-board-node.factory'; import { lessonFactory } from './lesson.factory'; import { taskFactory } from './task.factory'; diff --git a/apps/server/src/shared/testing/factory/boardnode/card-node.factory.ts b/apps/server/src/shared/testing/factory/boardnode/card-node.factory.ts deleted file mode 100644 index 07fed90ea83..00000000000 --- a/apps/server/src/shared/testing/factory/boardnode/card-node.factory.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* istanbul ignore file */ -import { CardNode, CardNodeProps } from '@shared/domain/entity'; -import { BaseFactory } from '../base.factory'; - -export const cardNodeFactory = BaseFactory.define(CardNode, ({ sequence }) => { - return { - height: 150, - title: `card #${sequence}`, - }; -}); diff --git a/apps/server/src/shared/testing/factory/boardnode/collaborative-text-editor-node.factory.ts b/apps/server/src/shared/testing/factory/boardnode/collaborative-text-editor-node.factory.ts deleted file mode 100644 index f0de6983521..00000000000 --- a/apps/server/src/shared/testing/factory/boardnode/collaborative-text-editor-node.factory.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { BoardNodeProps } from '@shared/domain/entity'; -import { CollaborativeTextEditorElementNode } from '@shared/domain/entity/boardnode/collaborative-text-editor-element-node.entity'; -import { BaseFactory } from '../base.factory'; - -export const collaborativeTextEditorNodeFactory = BaseFactory.define< - CollaborativeTextEditorElementNode, - BoardNodeProps ->(CollaborativeTextEditorElementNode, () => { - return {}; -}); diff --git a/apps/server/src/shared/testing/factory/boardnode/column-node.factory.ts b/apps/server/src/shared/testing/factory/boardnode/column-node.factory.ts deleted file mode 100644 index 4430ceec554..00000000000 --- a/apps/server/src/shared/testing/factory/boardnode/column-node.factory.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* istanbul ignore file */ -import { BoardNodeProps, ColumnNode } from '@shared/domain/entity'; -import { BaseFactory } from '../base.factory'; - -export const columnNodeFactory = BaseFactory.define(ColumnNode, ({ sequence }) => { - return { - title: `column #${sequence}`, - }; -}); diff --git a/apps/server/src/shared/testing/factory/boardnode/drawing-element-node.factory.ts b/apps/server/src/shared/testing/factory/boardnode/drawing-element-node.factory.ts deleted file mode 100644 index 65a69dc382d..00000000000 --- a/apps/server/src/shared/testing/factory/boardnode/drawing-element-node.factory.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* istanbul ignore file */ - -import { DrawingElementNode, DrawingElementNodeProps } from '@shared/domain/entity'; -import { BaseFactory } from '../base.factory'; - -export const drawingElementNodeFactory = BaseFactory.define( - DrawingElementNode, - ({ sequence }) => { - return { - description: `test-description-${sequence}`, - }; - } -); diff --git a/apps/server/src/shared/testing/factory/boardnode/external-tool-element-node.factory.ts b/apps/server/src/shared/testing/factory/boardnode/external-tool-element-node.factory.ts deleted file mode 100644 index c389c40d355..00000000000 --- a/apps/server/src/shared/testing/factory/boardnode/external-tool-element-node.factory.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ExternalToolElementNodeEntity, ExternalToolElementNodeEntityProps } from '@shared/domain/entity'; -import { BaseFactory } from '../base.factory'; - -export const externalToolElementNodeFactory = BaseFactory.define< - ExternalToolElementNodeEntity, - ExternalToolElementNodeEntityProps ->(ExternalToolElementNodeEntity, () => { - return {}; -}); diff --git a/apps/server/src/shared/testing/factory/boardnode/file-element-node.factory.ts b/apps/server/src/shared/testing/factory/boardnode/file-element-node.factory.ts deleted file mode 100644 index ed9b39990bc..00000000000 --- a/apps/server/src/shared/testing/factory/boardnode/file-element-node.factory.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* istanbul ignore file */ -import { FileElementNode, FileElementNodeProps } from '@shared/domain/entity'; -import { BaseFactory } from '../base.factory'; - -export const fileElementNodeFactory = BaseFactory.define( - FileElementNode, - ({ sequence }) => { - return { - caption: `caption #${sequence}`, - alternativeText: `alternativeText #${sequence}`, - }; - } -); diff --git a/apps/server/src/shared/testing/factory/boardnode/index.ts b/apps/server/src/shared/testing/factory/boardnode/index.ts deleted file mode 100644 index a45b70e8298..00000000000 --- a/apps/server/src/shared/testing/factory/boardnode/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -export * from './card-node.factory'; -export * from './collaborative-text-editor-node.factory'; -export * from './column-board-node.factory'; -export * from './column-node.factory'; -export * from './external-tool-element-node.factory'; -export * from './file-element-node.factory'; -export * from './link-element-node.factory'; -export { mediaBoardNodeFactory } from './media-board-node.factory'; -export { mediaExternalToolElementNodeFactory } from './media-external-tool-element-node.factory'; -export { mediaLineNodeFactory } from './media-line-node.factory'; -export * from './rich-text-element-node.factory'; -export * from './submission-container-element-node.factory'; -export * from './submission-item-node.factory'; diff --git a/apps/server/src/shared/testing/factory/boardnode/link-element-node.factory.ts b/apps/server/src/shared/testing/factory/boardnode/link-element-node.factory.ts deleted file mode 100644 index fa8eb0ceaa2..00000000000 --- a/apps/server/src/shared/testing/factory/boardnode/link-element-node.factory.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* istanbul ignore file */ -import { LinkElementNode, LinkElementNodeProps } from '@shared/domain/entity'; -import { BaseFactory } from '../base.factory'; - -export const linkElementNodeFactory = BaseFactory.define( - LinkElementNode, - ({ sequence }) => { - const url = `https://www.example.com/link/${sequence}`; - return { - url, - title: `The example page ${sequence}`, - }; - } -); diff --git a/apps/server/src/shared/testing/factory/boardnode/media-board-node.factory.ts b/apps/server/src/shared/testing/factory/boardnode/media-board-node.factory.ts deleted file mode 100644 index 57a8051f348..00000000000 --- a/apps/server/src/shared/testing/factory/boardnode/media-board-node.factory.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ObjectId } from '@mikro-orm/mongodb'; -import { MediaBoardColors, MediaBoardLayoutType } from '@modules/board/domain'; -import { BoardExternalReferenceType } from '@shared/domain/domainobject'; -import { MediaBoardNode } from '@shared/domain/entity'; -import { MediaBoardNodeProps } from '../../../domain/entity/boardnode/media-board/media-board-node.entity'; -import { BaseFactory } from '../base.factory'; - -export const mediaBoardNodeFactory = BaseFactory.define(MediaBoardNode, () => { - return { - context: { - type: BoardExternalReferenceType.User, - id: new ObjectId().toHexString(), - }, - mediaAvailableLineBackgroundColor: MediaBoardColors.TRANSPARENT, - mediaAvailableLineCollapsed: false, - layout: MediaBoardLayoutType.LIST, - }; -}); diff --git a/apps/server/src/shared/testing/factory/boardnode/media-external-tool-element-node.factory.ts b/apps/server/src/shared/testing/factory/boardnode/media-external-tool-element-node.factory.ts deleted file mode 100644 index c4bb4d6a764..00000000000 --- a/apps/server/src/shared/testing/factory/boardnode/media-external-tool-element-node.factory.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { contextExternalToolEntityFactory } from '@modules/tool/context-external-tool/testing'; -import { MediaExternalToolElementNode, type MediaExternalToolElementNodeProps } from '@shared/domain/entity'; -import { BaseFactory } from '../base.factory'; - -export const mediaExternalToolElementNodeFactory = BaseFactory.define< - MediaExternalToolElementNode, - MediaExternalToolElementNodeProps ->(MediaExternalToolElementNode, () => { - return { - contextExternalTool: contextExternalToolEntityFactory.build(), - }; -}); diff --git a/apps/server/src/shared/testing/factory/boardnode/media-line-node.factory.ts b/apps/server/src/shared/testing/factory/boardnode/media-line-node.factory.ts deleted file mode 100644 index 1dc9b337c18..00000000000 --- a/apps/server/src/shared/testing/factory/boardnode/media-line-node.factory.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { MediaBoardColors } from '@modules/board/domain'; -import { MediaLineNode, type MediaLineNodeProps } from '@shared/domain/entity'; -import { BaseFactory } from '../base.factory'; - -export const mediaLineNodeFactory = BaseFactory.define( - MediaLineNode, - ({ sequence }) => { - return { - title: `Line ${sequence}`, - backgroundColor: MediaBoardColors.TRANSPARENT, - collapsed: false, - }; - } -); diff --git a/apps/server/src/shared/testing/factory/boardnode/rich-text-element-node.factory.ts b/apps/server/src/shared/testing/factory/boardnode/rich-text-element-node.factory.ts deleted file mode 100644 index 41aa45e74dc..00000000000 --- a/apps/server/src/shared/testing/factory/boardnode/rich-text-element-node.factory.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* istanbul ignore file */ -import { RichTextElementNode, RichTextElementNodeProps } from '@shared/domain/entity'; -import { InputFormat } from '@shared/domain/types'; -import { BaseFactory } from '../base.factory'; - -export const richTextElementNodeFactory = BaseFactory.define( - RichTextElementNode, - ({ sequence }) => { - return { - text: `

text #${sequence}

`, - inputFormat: InputFormat.RICH_TEXT_CK5, - }; - } -); diff --git a/apps/server/src/shared/testing/factory/boardnode/submission-container-element-node.factory.ts b/apps/server/src/shared/testing/factory/boardnode/submission-container-element-node.factory.ts deleted file mode 100644 index e1c413c0589..00000000000 --- a/apps/server/src/shared/testing/factory/boardnode/submission-container-element-node.factory.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* istanbul ignore file */ -import { SubmissionContainerElementNode, SubmissionContainerNodeProps } from '@shared/domain/entity'; -import { BaseFactory } from '../base.factory'; - -export const submissionContainerElementNodeFactory = BaseFactory.define< - SubmissionContainerElementNode, - SubmissionContainerNodeProps ->(SubmissionContainerElementNode, () => { - return { - dueDate: null, - }; -}); diff --git a/apps/server/src/shared/testing/factory/boardnode/submission-item-node.factory.ts b/apps/server/src/shared/testing/factory/boardnode/submission-item-node.factory.ts deleted file mode 100644 index 336b2a34edd..00000000000 --- a/apps/server/src/shared/testing/factory/boardnode/submission-item-node.factory.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* istanbul ignore file */ -import { SubmissionItemNode, SubmissionItemNodeProps } from '@shared/domain/entity'; -import { BaseFactory } from '../base.factory'; -import { userFactory } from '../user.factory'; - -export const submissionItemNodeFactory = BaseFactory.define( - SubmissionItemNode, - () => { - const creator = userFactory.build(); - - return { - completed: false, - userId: creator.id, - }; - } -); diff --git a/apps/server/src/shared/testing/factory/boardnode/column-board-node.factory.ts b/apps/server/src/shared/testing/factory/column-board-node.factory.ts similarity index 69% rename from apps/server/src/shared/testing/factory/boardnode/column-board-node.factory.ts rename to apps/server/src/shared/testing/factory/column-board-node.factory.ts index 7b6377c2f2d..dbd89169472 100644 --- a/apps/server/src/shared/testing/factory/boardnode/column-board-node.factory.ts +++ b/apps/server/src/shared/testing/factory/column-board-node.factory.ts @@ -1,20 +1,21 @@ /* istanbul ignore file */ -import { BoardExternalReferenceType, BoardLayout } from '@shared/domain/domainobject/board/types'; -import { ColumnBoardNode, ColumnBoardNodeProps } from '@shared/domain/entity'; import { ObjectId } from '@mikro-orm/mongodb'; -import { BaseFactory } from '../base.factory'; +import { ColumnBoardNode, ColumnBoardNodeProps } from '@shared/domain/entity'; +import { BoardExternalReferenceType } from '@modules/board/domain/types/board-external-reference'; +import { BoardLayout } from '@modules/board/domain/types/board-layout.enum'; +import { BaseFactory } from './base.factory'; export const columnBoardNodeFactory = BaseFactory.define( ColumnBoardNode, ({ sequence }) => { return { title: `columnBoard #${sequence}`, + isVisible: true, + layout: BoardLayout.COLUMNS, context: { type: BoardExternalReferenceType.Course, id: new ObjectId().toHexString(), }, - isVisible: true, - layout: BoardLayout.COLUMNS, }; } ); diff --git a/apps/server/src/shared/testing/factory/domainobject/board/board-do-authorizable.factory.ts b/apps/server/src/shared/testing/factory/domainobject/board/board-do-authorizable.factory.ts deleted file mode 100644 index 7fcb192c277..00000000000 --- a/apps/server/src/shared/testing/factory/domainobject/board/board-do-authorizable.factory.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { BoardDoAuthorizable, BoardDoAuthorizableProps } from '@shared/domain/domainobject/board'; -import { ObjectId } from '@mikro-orm/mongodb'; -import { DomainObjectFactory } from '../domain-object.factory'; -import { columnFactory } from './column.do.factory'; -import { columnBoardFactory } from './column-board.do.factory'; - -export const boardDoAuthorizableFactory = DomainObjectFactory.define( - BoardDoAuthorizable, - () => { - const boardDo = columnFactory.build(); - const rootDo = columnBoardFactory.build({ children: [boardDo] }); - return { - id: new ObjectId().toHexString(), - users: [], - boardDo, - rootDo, - }; - } -); diff --git a/apps/server/src/shared/testing/factory/domainobject/board/collaborative-text-editor-element.do.factory.ts b/apps/server/src/shared/testing/factory/domainobject/board/collaborative-text-editor-element.do.factory.ts deleted file mode 100644 index 94fa3cf5767..00000000000 --- a/apps/server/src/shared/testing/factory/domainobject/board/collaborative-text-editor-element.do.factory.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* istanbul ignore file */ -import { ObjectId } from '@mikro-orm/mongodb'; -import { BoardCompositeProps, CollaborativeTextEditorElement } from '@shared/domain/domainobject'; -import { BaseFactory } from '../../base.factory'; - -export const collaborativeTextEditorElementFactory = BaseFactory.define< - CollaborativeTextEditorElement, - BoardCompositeProps ->(CollaborativeTextEditorElement, () => { - return { - id: new ObjectId().toHexString(), - createdAt: new Date(), - updatedAt: new Date(), - }; -}); diff --git a/apps/server/src/shared/testing/factory/domainobject/board/index.ts b/apps/server/src/shared/testing/factory/domainobject/board/index.ts deleted file mode 100644 index 9927751e2d7..00000000000 --- a/apps/server/src/shared/testing/factory/domainobject/board/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -export * from './card.do.factory'; -export * from './collaborative-text-editor-element.do.factory'; -export * from './column-board.do.factory'; -export * from './column.do.factory'; -export * from './drawing-element.do.factory'; -export * from './external-tool-element.do.factory'; -export * from './file-element.do.factory'; -export * from './link-element.do.factory'; -export * from './rich-text-element.do.factory'; -export * from './submission-container-element.do.factory'; -export * from './submission-item.do.factory'; - -export { boardDoAuthorizableFactory } from './board-do-authorizable.factory'; -export { mediaBoardFactory } from './media-board.do.factory'; -export { mediaExternalToolElementFactory } from './media-external-tool-element.do.factory'; -export { mediaLineFactory } from './media-line.do.factory'; -export { mediaAvailableLineElementFactory } from './media-available-line-element.do.factory'; -export { mediaAvailableLineFactory } from './media-available-line.do.factory'; diff --git a/apps/server/src/shared/testing/factory/domainobject/board/link-element.do.factory.ts b/apps/server/src/shared/testing/factory/domainobject/board/link-element.do.factory.ts deleted file mode 100644 index 31409d964d5..00000000000 --- a/apps/server/src/shared/testing/factory/domainobject/board/link-element.do.factory.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* istanbul ignore file */ -import { LinkElement, LinkElementProps } from '@shared/domain/domainobject'; -import { ObjectId } from '@mikro-orm/mongodb'; -import { BaseFactory } from '../../base.factory'; - -export const linkElementFactory = BaseFactory.define(LinkElement, ({ sequence }) => { - return { - id: new ObjectId().toHexString(), - url: `https://www.example.com/link/${sequence}`, - title: 'Website open graph title', - children: [], - createdAt: new Date(), - updatedAt: new Date(), - }; -}); diff --git a/apps/server/src/shared/testing/factory/domainobject/board/media-board.do.factory.ts b/apps/server/src/shared/testing/factory/domainobject/board/media-board.do.factory.ts deleted file mode 100644 index 1971f553844..00000000000 --- a/apps/server/src/shared/testing/factory/domainobject/board/media-board.do.factory.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ObjectId } from '@mikro-orm/mongodb'; -import { MediaBoardColors, MediaBoardLayoutType } from '@modules/board/domain'; -import { - AnyMediaBoardDo, - BoardExternalReferenceType, - MediaBoard, - type MediaBoardProps, -} from '@shared/domain/domainobject'; -import { DeepPartial } from 'fishery'; -import { BaseFactory } from '../../base.factory'; - -class MediaBoardFactory extends BaseFactory { - addChild(child: AnyMediaBoardDo): this { - const params: DeepPartial = { children: [child] }; - - return this.params(params); - } -} - -export const mediaBoardFactory = MediaBoardFactory.define(MediaBoard, () => { - return { - id: new ObjectId().toHexString(), - children: [], - createdAt: new Date(), - updatedAt: new Date(), - context: { - type: BoardExternalReferenceType.User, - id: new ObjectId().toHexString(), - }, - layout: MediaBoardLayoutType.LIST, - mediaAvailableLineCollapsed: false, - mediaAvailableLineBackgroundColor: MediaBoardColors.TRANSPARENT, - }; -}); diff --git a/apps/server/src/shared/testing/factory/domainobject/board/media-line.do.factory.ts b/apps/server/src/shared/testing/factory/domainobject/board/media-line.do.factory.ts deleted file mode 100644 index 46be11593f5..00000000000 --- a/apps/server/src/shared/testing/factory/domainobject/board/media-line.do.factory.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ObjectId } from '@mikro-orm/mongodb'; -import { MediaBoardColors } from '@modules/board/domain'; -import { AnyMediaBoardDo, type MediaBoardProps, MediaLine, type MediaLineProps } from '@shared/domain/domainobject'; -import { DeepPartial } from 'fishery'; -import { BaseFactory } from '../../base.factory'; - -class MediaLineFactory extends BaseFactory { - addChild(child: AnyMediaBoardDo): this { - const params: DeepPartial = { children: [child] }; - - return this.params(params); - } -} - -export const mediaLineFactory = MediaLineFactory.define(MediaLine, ({ sequence }) => { - return { - id: new ObjectId().toHexString(), - children: [], - createdAt: new Date(), - updatedAt: new Date(), - title: `Line ${sequence}`, - backgroundColor: MediaBoardColors.TRANSPARENT, - collapsed: false, - }; -}); diff --git a/apps/server/src/shared/testing/factory/domainobject/board/submission-item.do.factory.ts b/apps/server/src/shared/testing/factory/domainobject/board/submission-item.do.factory.ts deleted file mode 100644 index 3ea6b6d9880..00000000000 --- a/apps/server/src/shared/testing/factory/domainobject/board/submission-item.do.factory.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* istanbul ignore file */ -import { SubmissionItem, SubmissionItemProps } from '@shared/domain/domainobject'; -import { ObjectId } from '@mikro-orm/mongodb'; -import { BaseFactory } from '../../base.factory'; - -export const submissionItemFactory = BaseFactory.define( - SubmissionItem, - ({ sequence }) => { - return { - id: new ObjectId().toHexString(), - title: `submission item #${sequence}`, - children: [], - completed: false, - userId: new ObjectId().toHexString(), - createdAt: new Date(), - updatedAt: new Date(), - }; - } -); diff --git a/apps/server/src/shared/testing/factory/domainobject/index.ts b/apps/server/src/shared/testing/factory/domainobject/index.ts index f1998f50167..19de632fba1 100644 --- a/apps/server/src/shared/testing/factory/domainobject/index.ts +++ b/apps/server/src/shared/testing/factory/domainobject/index.ts @@ -1,4 +1,3 @@ -export * from './board'; export * from './groups'; export * from './do-base.factory'; export * from './legacy-school.factory'; diff --git a/apps/server/src/shared/testing/factory/external-tool-pseudonym.factory.ts b/apps/server/src/shared/testing/factory/external-tool-pseudonym.factory.ts index d21bfbcff78..624e55998b2 100644 --- a/apps/server/src/shared/testing/factory/external-tool-pseudonym.factory.ts +++ b/apps/server/src/shared/testing/factory/external-tool-pseudonym.factory.ts @@ -1,6 +1,6 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { ExternalToolPseudonymEntity, ExternalToolPseudonymEntityProps } from '@modules/pseudonym/entity'; -import { BaseFactory } from '@shared/testing/factory/base.factory'; +import { BaseFactory } from './base.factory'; export const externalToolPseudonymEntityFactory = BaseFactory.define< ExternalToolPseudonymEntity, diff --git a/apps/server/src/shared/testing/factory/index.ts b/apps/server/src/shared/testing/factory/index.ts index f7f4a63e966..3c3612072aa 100644 --- a/apps/server/src/shared/testing/factory/index.ts +++ b/apps/server/src/shared/testing/factory/index.ts @@ -2,7 +2,7 @@ export * from './axios-response.factory'; export * from './base.factory'; export * from './board.factory'; export * from './boardelement.factory'; -export * from './boardnode'; +export * from './column-board-node.factory'; export * from './course.factory'; export * from './coursegroup.factory'; export * from './domainobject'; diff --git a/apps/server/src/shared/testing/factory/ltitool.factory.ts b/apps/server/src/shared/testing/factory/ltitool.factory.ts index 4f7526de981..c0a54dd77a8 100644 --- a/apps/server/src/shared/testing/factory/ltitool.factory.ts +++ b/apps/server/src/shared/testing/factory/ltitool.factory.ts @@ -1,7 +1,7 @@ import { CustomLtiPropertyDO } from '@shared/domain/domainobject/ltitool.do'; import { ILtiToolProperties, LtiPrivacyPermission, LtiRoleType, LtiTool } from '@shared/domain/entity'; -import { BaseFactory } from '@shared/testing/factory/base.factory'; import { DeepPartial } from 'fishery'; +import { BaseFactory } from './base.factory'; class LtiToolFactory extends BaseFactory { withName(name: string): this { diff --git a/apps/server/src/shared/testing/factory/pseudonym.factory.ts b/apps/server/src/shared/testing/factory/pseudonym.factory.ts index 96be2d1c3c0..dffcc5debee 100644 --- a/apps/server/src/shared/testing/factory/pseudonym.factory.ts +++ b/apps/server/src/shared/testing/factory/pseudonym.factory.ts @@ -1,6 +1,6 @@ import { ObjectId } from '@mikro-orm/mongodb'; -import { BaseFactory } from '@shared/testing/factory/base.factory'; import { PseudonymEntity, PseudonymEntityProps } from '@modules/pseudonym/entity'; +import { BaseFactory } from './base.factory'; export const pseudonymEntityFactory = BaseFactory.define( PseudonymEntity, diff --git a/apps/server/src/shared/testing/factory/team.factory.ts b/apps/server/src/shared/testing/factory/team.factory.ts index 41cb21da3b5..1b2011f190c 100644 --- a/apps/server/src/shared/testing/factory/team.factory.ts +++ b/apps/server/src/shared/testing/factory/team.factory.ts @@ -1,7 +1,7 @@ import { Role, TeamEntity, TeamProperties, TeamUserEntity } from '@shared/domain/entity'; -import { BaseFactory } from '@shared/testing/factory/base.factory'; -import { teamUserFactory } from '@shared/testing/factory/teamuser.factory'; import { DeepPartial } from 'fishery'; +import { BaseFactory } from './base.factory'; +import { teamUserFactory } from './teamuser.factory'; class TeamFactory extends BaseFactory { withRoleAndUserId(role: Role, userId: string): this { diff --git a/apps/server/src/shared/testing/factory/teamuser.factory.ts b/apps/server/src/shared/testing/factory/teamuser.factory.ts index ed067157c67..b381d39baa0 100644 --- a/apps/server/src/shared/testing/factory/teamuser.factory.ts +++ b/apps/server/src/shared/testing/factory/teamuser.factory.ts @@ -1,9 +1,9 @@ import { Role, TeamUserEntity } from '@shared/domain/entity'; -import { BaseFactory } from '@shared/testing/factory/base.factory'; -import { roleFactory } from '@shared/testing/factory/role.factory'; -import { userFactory } from '@shared/testing/factory/user.factory'; import { DeepPartial } from 'fishery'; +import { BaseFactory } from './base.factory'; +import { roleFactory } from './role.factory'; import { schoolEntityFactory } from './school-entity.factory'; +import { userFactory } from './user.factory'; class TeamUserFactory extends BaseFactory { withRoleAndUserId(role: Role, userId: string): this { diff --git a/apps/server/src/shared/testing/factory/tldraw.ws.factory.ts b/apps/server/src/shared/testing/factory/tldraw.ws.factory.ts index ca24cb9fefe..af8c34b6b73 100644 --- a/apps/server/src/shared/testing/factory/tldraw.ws.factory.ts +++ b/apps/server/src/shared/testing/factory/tldraw.ws.factory.ts @@ -1,6 +1,6 @@ import { WsSharedDocDo } from '@modules/tldraw/domain/ws-shared-doc.do'; import WebSocket from 'ws'; -import { WebSocketReadyStateEnum } from '@shared/testing'; +import { WebSocketReadyStateEnum } from '../web-socket-ready-state-enum'; export class TldrawWsFactory { public static createWsSharedDocDo(): WsSharedDocDo { diff --git a/apps/server/src/shared/testing/factory/video-conference.do.factory.ts b/apps/server/src/shared/testing/factory/video-conference.do.factory.ts index 0ba97c5fa79..63ffb35e953 100644 --- a/apps/server/src/shared/testing/factory/video-conference.do.factory.ts +++ b/apps/server/src/shared/testing/factory/video-conference.do.factory.ts @@ -1,6 +1,6 @@ import { VideoConferenceDO } from '@shared/domain/domainobject'; import { VideoConferenceScope } from '@shared/domain/interface'; -import { BaseFactory } from '@shared/testing'; +import { BaseFactory } from './base.factory'; export const videoConferenceDOFactory: BaseFactory = BaseFactory.define< VideoConferenceDO, diff --git a/apps/server/src/shared/testing/factory/video-conference.factory.ts b/apps/server/src/shared/testing/factory/video-conference.factory.ts index ecff6bb3abc..96505980f70 100644 --- a/apps/server/src/shared/testing/factory/video-conference.factory.ts +++ b/apps/server/src/shared/testing/factory/video-conference.factory.ts @@ -1,9 +1,9 @@ -import { BaseFactory } from '@shared/testing/factory/base.factory'; import { IVideoConferenceProperties, TargetModels, VideoConference, } from '@shared/domain/entity/video-conference.entity'; +import { BaseFactory } from './base.factory'; export const videoConferenceFactory = BaseFactory.define( VideoConference, diff --git a/apps/server/src/shared/testing/index.ts b/apps/server/src/shared/testing/index.ts index 9b968a2e8f7..5615f70b563 100644 --- a/apps/server/src/shared/testing/index.ts +++ b/apps/server/src/shared/testing/index.ts @@ -1,5 +1,6 @@ export * from './factory'; export * from './setup-entities'; +export * from './create-collections'; export * from './cleanup-collections'; export * from './map-user-to-current-user'; export * from './test-api-client'; diff --git a/apps/server/src/shared/testing/test-socket-api-client.ts b/apps/server/src/shared/testing/test-socket-api-client.ts index 0bb8e780fde..1d00578d68a 100644 --- a/apps/server/src/shared/testing/test-socket-api-client.ts +++ b/apps/server/src/shared/testing/test-socket-api-client.ts @@ -2,8 +2,8 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { INestApplication } from '@nestjs/common'; import { User } from '@shared/domain/entity'; -import { accountFactory } from '@src/modules/account/testing'; -import { LocalAuthorizationBodyParams } from '@src/modules/authentication/controllers/dto'; +import { accountFactory } from '@modules/account/testing'; +import { LocalAuthorizationBodyParams } from '@modules/authentication/controllers/dto'; import { Socket, io } from 'socket.io-client'; import request from 'supertest'; diff --git a/backup/setup/migrations.json b/backup/setup/migrations.json index 487ec3cbbff..dd9582232dc 100644 --- a/backup/setup/migrations.json +++ b/backup/setup/migrations.json @@ -151,5 +151,14 @@ "created_at": { "$date": "2024-06-12T12:26:01.665Z" } + }, + { + "_id": { + "$oid": "6655e94f06722f2a434c135f" + }, + "name": "Migration20240528140356", + "created_at": { + "$date": "2024-05-28T14:25:19.577Z" + } } ]