From 6725e7129f4b7bc86d0d1d5899069c3e48f28158 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Tue, 14 May 2024 15:22:07 +0200 Subject: [PATCH 01/19] WIP: implementation of elementActions --- .../src/modules/board/board-ws-api.module.ts | 4 +- .../board-collaboration.gateway.spec.ts | 93 ++++++++++++++++++- .../gateway/board-collaboration.gateway.ts | 73 ++++++++++++++- .../create-content-element.message.param.ts | 29 ++++++ .../delete-content-element.message.param.ts | 6 ++ .../dto/move-content-element.message.param.ts | 22 +++++ .../update-content-element.message.param.ts | 52 +++++++++++ .../service/content-element-update.visitor.ts | 1 + 8 files changed, 272 insertions(+), 8 deletions(-) create mode 100644 apps/server/src/modules/board/gateway/dto/create-content-element.message.param.ts create mode 100644 apps/server/src/modules/board/gateway/dto/delete-content-element.message.param.ts create mode 100644 apps/server/src/modules/board/gateway/dto/move-content-element.message.param.ts create mode 100644 apps/server/src/modules/board/gateway/dto/update-content-element.message.param.ts 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 c10e7a9adf9..6955e56e43f 100644 --- a/apps/server/src/modules/board/board-ws-api.module.ts +++ b/apps/server/src/modules/board/board-ws-api.module.ts @@ -4,11 +4,11 @@ import { LoggerModule } from '@src/core/logger'; import { AuthorizationModule } from '../authorization'; import { BoardModule } from './board.module'; import { BoardCollaborationGateway } from './gateway/board-collaboration.gateway'; -import { BoardUc, CardUc, ColumnUc } from './uc'; +import { BoardUc, CardUc, ColumnUc, ElementUc } from './uc'; @Module({ imports: [BoardModule, forwardRef(() => AuthorizationModule), LoggerModule], - providers: [BoardCollaborationGateway, CardUc, ColumnUc, BoardUc, CourseRepo], + providers: [BoardCollaborationGateway, CardUc, ColumnUc, ElementUc, BoardUc, CourseRepo], exports: [], }) export class BoardWsApiModule {} 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 0859914792c..6c1fbcae607 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 @@ -2,13 +2,16 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; -import { BoardExternalReferenceType, CardProps } from '@shared/domain/domainobject'; +import { BoardExternalReferenceType, CardProps, ContentElementType } from '@shared/domain/domainobject'; +import { InputFormat } from '@shared/domain/types'; import { cardNodeFactory, cleanupCollections, columnBoardNodeFactory, columnNodeFactory, courseFactory, + richTextElementFactory, + richTextElementNodeFactory, userFactory, } from '@shared/testing'; import { getSocketApiClient, waitForEvent } from '@shared/testing/test-socket-api-client'; @@ -55,13 +58,14 @@ describe(BoardCollaborationGateway.name, () => { const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode }); const columnNode2 = columnNodeFactory.buildWithId({ parent: columnBoardNode }); - const cardNodes = cardNodeFactory.buildList(2, { parent: columnNode }); + const cardNodes = cardNodeFactory.buildListWithId(2, { parent: columnNode }); + const elementNodes = richTextElementNodeFactory.buildListWithId(3, { parent: cardNodes[0] }); - await em.persistAndFlush([columnBoardNode, columnNode, columnNode2, ...cardNodes]); + await em.persistAndFlush([columnBoardNode, columnNode, columnNode2, ...cardNodes, ...elementNodes]); em.clear(); - return { user, columnBoardNode, columnNode, columnNode2, cardNodes }; + return { user, columnBoardNode, columnNode, columnNode2, cardNodes, elementNodes }; }; it('should be defined', () => { @@ -476,11 +480,90 @@ describe(BoardCollaborationGateway.name, () => { await setup(); const cardId = 'non-existing-id'; - ioClient.emit('delete-card-request', { cardIds: [cardId] }); + ioClient.emit('delete-card-request', { cardId }); const failure = await waitForEvent(ioClient, 'delete-card-failure'); expect(failure).toBeDefined(); }); }); }); + + describe('create element', () => { + it('should answer with success', async () => { + const { cardNodes } = await setup(); + const cardId = cardNodes[1].id; + + ioClient.emit('create-element-request', { cardId, type: ContentElementType.RICH_TEXT }); + const success = (await waitForEvent(ioClient, 'create-element-success')) as { + cardId: string; + newElement: unknown; + }; + + expect(Object.keys(success)).toEqual(expect.arrayContaining(['cardId', 'newElement'])); + }); + }); + + describe('delete element', () => { + describe('when element exists', () => { + it('should answer with success', async () => { + const { elementNodes } = await setup(); + const elementId = elementNodes[0].id; + + ioClient.emit('delete-element-request', { elementId }); + const success = await waitForEvent(ioClient, 'delete-element-success'); + + expect(success).toEqual({ elementId }); + }); + }); + + describe('when element does not exist', () => { + it('should answer with failure', async () => { + await setup(); + const elementId = 'non-existing-id'; + + ioClient.emit('delete-element-request', { elementId }); + const failure = await waitForEvent(ioClient, 'delete-element-failure'); + + expect(failure).toBeDefined(); + }); + }); + }); + + describe('update element', () => { + describe('when element exists', () => { + it('should answer with success', async () => { + const { elementNodes } = await setup(); + const elementId = elementNodes[0].id; + + ioClient.emit('update-element-request', { + elementId, + data: { + type: ContentElementType.RICH_TEXT, + content: { text: 'some new text', inputFormat: InputFormat.PLAIN_TEXT }, + }, + }); + const success = await waitForEvent(ioClient, 'update-element-success'); + + expect(success).toEqual({ elementId }); + }); + }); + + describe('when element does not exist', () => { + it('should answer with failure', async () => { + await setup(); + const elementId = 'non-existing-id'; + + ioClient.emit('update-element-request', { + elementId, + data: { + type: ContentElementType.RICH_TEXT, + content: { text: 'some new text', inputFormat: InputFormat.PLAIN_TEXT }, + }, + }); + const failure = await waitForEvent(ioClient, 'update-element-failure'); + + expect(failure).toBeDefined(); + }); + }); + }); }); 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 b5f9dcb6cae..6446e7e5e8e 100644 --- a/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts +++ b/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts @@ -5,7 +5,7 @@ import { LegacyLogger } from '@src/core/logger'; import { WsJwtAuthGuard } from '@src/modules/authentication/guard/ws-jwt-auth.guard'; import { BoardResponseMapper, CardResponseMapper } from '../controller/mapper'; import { BoardDoAuthorizableService } from '../service'; -import { BoardUc, CardUc, ColumnUc } from '../uc'; +import { BoardUc, CardUc, ColumnUc, ElementUc } from '../uc'; import { CreateCardMessageParams, DeleteColumnMessageParams, @@ -25,6 +25,10 @@ import { UpdateBoardVisibilityMessageParams } from './dto/update-board-visibilit import { UpdateCardHeightMessageParams } from './dto/update-card-height.message.param'; import { UpdateCardTitleMessageParams } from './dto/update-card-title.message.param'; import { Socket } from './types'; +import { CreateContentElementMessageParams } from './dto/create-content-element.message.param'; +import { DeleteContentElementMessageParams } from './dto/delete-content-element.message.param'; +import { UpdateContentElementMessageParams } from './dto/update-content-element.message.param'; +import { MoveContentElementMessageParams } from './dto/move-content-element.message.param'; @WebSocketGateway(BoardCollaborationConfiguration.websocket) @UseGuards(WsJwtAuthGuard) @@ -36,6 +40,7 @@ export class BoardCollaborationGateway { private readonly boardUc: BoardUc, private readonly columnUc: ColumnUc, private readonly cardUc: CardUc, + private readonly elementUc: ElementUc, private readonly authorizableService: BoardDoAuthorizableService // to be removed ) {} @@ -273,6 +278,72 @@ export class BoardCollaborationGateway { } } + @SubscribeMessage('create-element-request') + @UseRequestContext() + async createElement(client: Socket, data: CreateContentElementMessageParams) { + try { + const { userId } = this.getCurrentUser(client); + const card = await this.cardUc.createElement(userId, data.cardId, data.type, data.toPosition); + const responsePayload = { + ...data, + newElement: card.getProps(), + }; + + const room = await this.ensureUserInRoom(client, data.cardId); + client.to(room).emit('create-element-success', responsePayload); + client.emit('create-element-success', responsePayload); + } catch (err) { + console.log('create-element-request', err); + client.emit('create-element-failure', new Error('Failed to create element')); + } + } + + @SubscribeMessage('update-element-request') + @UseRequestContext() + async updateElement(client: Socket, data: UpdateContentElementMessageParams) { + try { + const { userId } = this.getCurrentUser(client); + await this.elementUc.updateElement(userId, data.elementId, data.data.content); + + const room = await this.ensureUserInRoom(client, data.elementId); + client.to(room).emit('update-element-success', data); + client.emit('update-element-success', data); + } catch (err) { + console.log('update-element-request', err); + client.emit('update-element-failure', new Error('Failed to update element')); + } + } + + @SubscribeMessage('delete-element-request') + @UseRequestContext() + async deleteElement(client: Socket, data: DeleteContentElementMessageParams) { + try { + const { userId } = this.getCurrentUser(client); + const room = await this.ensureUserInRoom(client, data.elementId); + await this.elementUc.deleteElement(userId, data.elementId); + + client.to(room).emit('delete-element-success', data); + client.emit('delete-element-success', data); + } catch (err) { + client.emit('delete-element-failure', new Error('Failed to delete element')); + } + } + + @SubscribeMessage('move-element-request') + @UseRequestContext() + async moveElement(client: Socket, data: MoveContentElementMessageParams) { + try { + const { userId } = this.getCurrentUser(client); + await this.cardUc.moveElement(userId, data.elementId, data.toCardId, data.toPosition); + + const room = await this.ensureUserInRoom(client, data.elementId); + client.to(room).emit('move-element-success', data); + client.emit('move-element-success', data); + } catch (err) { + client.emit('move-element-failure', new Error('Failed to move element')); + } + } + private async ensureUserInRoom(client: Socket, id: string) { const rootId = await this.getRootIdForId(id); const room = `board_${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 new file mode 100644 index 00000000000..70885d03146 --- /dev/null +++ b/apps/server/src/modules/board/gateway/dto/create-content-element.message.param.ts @@ -0,0 +1,29 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ContentElementType } from '@shared/domain/domainobject'; +import { IsEnum, IsInt, IsMongoId, IsOptional, Min } from 'class-validator'; + +export class CreateContentElementMessageParams { + @IsMongoId() + cardId!: string; + + @IsEnum(ContentElementType) + @ApiProperty({ + description: 'The type of element', + enum: ContentElementType, + required: true, + nullable: false, + enumName: 'ContentElementType', + }) + type!: ContentElementType; + + @IsOptional() + @IsInt() + @Min(0) + @ApiPropertyOptional({ + description: 'to bring element to a specific position, default is last position', + type: Number, + required: false, + nullable: false, + }) + toPosition?: number; +} diff --git a/apps/server/src/modules/board/gateway/dto/delete-content-element.message.param.ts b/apps/server/src/modules/board/gateway/dto/delete-content-element.message.param.ts new file mode 100644 index 00000000000..47d0ad015e9 --- /dev/null +++ b/apps/server/src/modules/board/gateway/dto/delete-content-element.message.param.ts @@ -0,0 +1,6 @@ +import { IsMongoId } from 'class-validator'; + +export class DeleteContentElementMessageParams { + @IsMongoId() + elementId!: string; +} diff --git a/apps/server/src/modules/board/gateway/dto/move-content-element.message.param.ts b/apps/server/src/modules/board/gateway/dto/move-content-element.message.param.ts new file mode 100644 index 00000000000..cfa5f01d8ec --- /dev/null +++ b/apps/server/src/modules/board/gateway/dto/move-content-element.message.param.ts @@ -0,0 +1,22 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsMongoId, IsNumber, Min } from 'class-validator'; + +export class MoveContentElementMessageParams { + @IsMongoId() + elementId!: string; + + @IsMongoId() + @ApiProperty({ + required: true, + nullable: false, + }) + toCardId!: string; + + @IsNumber() + @Min(0) + @ApiProperty({ + required: true, + nullable: false, + }) + toPosition!: number; +} diff --git a/apps/server/src/modules/board/gateway/dto/update-content-element.message.param.ts b/apps/server/src/modules/board/gateway/dto/update-content-element.message.param.ts new file mode 100644 index 00000000000..7e83aed6f16 --- /dev/null +++ b/apps/server/src/modules/board/gateway/dto/update-content-element.message.param.ts @@ -0,0 +1,52 @@ +import { ApiProperty, getSchemaPath } from '@nestjs/swagger'; +import { ContentElementType } from '@shared/domain/domainobject'; +import { Type } from 'class-transformer'; +import { IsMongoId, ValidateNested } from 'class-validator'; +import { + DrawingElementContentBody, + ElementContentBody, + ExternalToolElementContentBody, + FileElementContentBody, + LinkElementContentBody, + RichTextElementContentBody, + SubmissionContainerElementContentBody, +} from '../../controller/dto'; + +export class UpdateContentElementMessageParams { + @IsMongoId() + elementId!: string; + + @ValidateNested() + @Type(() => ElementContentBody, { + discriminator: { + property: 'type', + subTypes: [ + { value: FileElementContentBody, name: ContentElementType.FILE }, + { value: LinkElementContentBody, name: ContentElementType.LINK }, + { value: RichTextElementContentBody, name: ContentElementType.RICH_TEXT }, + { value: SubmissionContainerElementContentBody, name: ContentElementType.SUBMISSION_CONTAINER }, + { value: ExternalToolElementContentBody, name: ContentElementType.EXTERNAL_TOOL }, + { value: ExternalToolElementContentBody, name: ContentElementType.DRAWING }, + { value: DrawingElementContentBody, name: ContentElementType.DRAWING }, + ], + }, + keepDiscriminatorProperty: true, + }) + @ApiProperty({ + oneOf: [ + { $ref: getSchemaPath(FileElementContentBody) }, + { $ref: getSchemaPath(LinkElementContentBody) }, + { $ref: getSchemaPath(RichTextElementContentBody) }, + { $ref: getSchemaPath(SubmissionContainerElementContentBody) }, + { $ref: getSchemaPath(ExternalToolElementContentBody) }, + { $ref: getSchemaPath(DrawingElementContentBody) }, + ], + }) + data!: + | FileElementContentBody + | LinkElementContentBody + | RichTextElementContentBody + | SubmissionContainerElementContentBody + | ExternalToolElementContentBody + | DrawingElementContentBody; +} 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 index fe1b979e2cd..0dcb053bd67 100644 --- a/apps/server/src/modules/board/service/content-element-update.visitor.ts +++ b/apps/server/src/modules/board/service/content-element-update.visitor.ts @@ -80,6 +80,7 @@ export class ContentElementUpdateVisitor implements BoardCompositeVisitorAsync { } async visitRichTextElementAsync(richTextElement: RichTextElement): Promise { + console.log('this.content', this.content); if (this.content instanceof RichTextContentBody) { richTextElement.text = sanitizeRichText(this.content.text, this.content.inputFormat); richTextElement.inputFormat = this.content.inputFormat; From 201ab932a9279e1ca2bf281ff9b026e330a2b766 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Tue, 14 May 2024 15:31:27 +0200 Subject: [PATCH 02/19] add moveElement action --- .../board-collaboration.gateway.spec.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) 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 6c1fbcae607..50b4cc9179b 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 @@ -566,4 +566,31 @@ describe(BoardCollaborationGateway.name, () => { }); }); }); + + describe('move element', () => { + describe('when element exists', () => { + it('should answer with success', async () => { + const { cardNodes, elementNodes } = await setup(); + const data = { elementId: elementNodes[0].id, toCardId: cardNodes[0].id, toPosition: 2 }; + + ioClient.emit('move-element-request', data); + const success = await waitForEvent(ioClient, 'move-element-success'); + + expect(success).toEqual(data); + }); + }); + + describe('when element does not exist', () => { + it('should answer with failure', async () => { + const { cardNodes } = await setup(); + const elementId = 'non-existing-id'; + const toCardId = cardNodes[0].id; + + ioClient.emit('move-element-request', { elementId, toCardId, toPosition: 2 }); + const failure = await waitForEvent(ioClient, 'move-element-failure'); + + expect(failure).toBeDefined(); + }); + }); + }); }); From deafacd82ebb1bd13b014b400bd43902082498c2 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Tue, 14 May 2024 15:47:57 +0200 Subject: [PATCH 03/19] chore: remove unneeded code --- .../src/modules/board/service/content-element-update.visitor.ts | 1 - 1 file changed, 1 deletion(-) 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 index 0dcb053bd67..fe1b979e2cd 100644 --- a/apps/server/src/modules/board/service/content-element-update.visitor.ts +++ b/apps/server/src/modules/board/service/content-element-update.visitor.ts @@ -80,7 +80,6 @@ export class ContentElementUpdateVisitor implements BoardCompositeVisitorAsync { } async visitRichTextElementAsync(richTextElement: RichTextElement): Promise { - console.log('this.content', this.content); if (this.content instanceof RichTextContentBody) { richTextElement.text = sanitizeRichText(this.content.text, this.content.inputFormat); richTextElement.inputFormat = this.content.inputFormat; From c4f47d51aadfc23c80a71caa802b7782f78fd495 Mon Sep 17 00:00:00 2001 From: Thomas Feldtkeller Date: Tue, 14 May 2024 16:12:20 +0200 Subject: [PATCH 04/19] fix websocket validation --- .../board-collaboration.gateway.spec.ts | 47 +++++++++---------- .../gateway/board-collaboration.gateway.ts | 14 +++--- .../board/gateway/ws-validation.pipe.ts | 26 ++++++++++ 3 files changed, 57 insertions(+), 30 deletions(-) create mode 100644 apps/server/src/modules/board/gateway/ws-validation.pipe.ts 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 50b4cc9179b..d9eaf2cd40d 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 @@ -1,4 +1,4 @@ -import { EntityManager } from '@mikro-orm/mongodb'; +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; @@ -10,7 +10,6 @@ import { columnBoardNodeFactory, columnNodeFactory, courseFactory, - richTextElementFactory, richTextElementNodeFactory, userFactory, } from '@shared/testing'; @@ -91,7 +90,7 @@ describe(BoardCollaborationGateway.name, () => { it('should answer with failure', async () => { await setup(); - ioClient.emit('create-card-request', { columnId: 'non-existing-column' }); + ioClient.emit('create-card-request', { columnId: new ObjectId().toHexString() }); const failure = await waitForEvent(ioClient, 'create-card-failure'); expect(failure).toBeDefined(); @@ -115,7 +114,7 @@ describe(BoardCollaborationGateway.name, () => { describe('when board does not exist', () => { it('should answer with failure', async () => { await setup(); - const boardId = 'non-existing-id'; + const boardId = new ObjectId().toHexString(); ioClient.emit('fetch-board-request', { boardId }); const failure = await waitForEvent(ioClient, 'fetch-board-failure'); @@ -151,9 +150,7 @@ describe(BoardCollaborationGateway.name, () => { const moveCardProps = { cardId: cardNodes[0].id, - oldIndex: 0, newIndex: 1, - fromColumnId: columnNode.id, toColumnId: columnNode.id, }; @@ -169,7 +166,7 @@ describe(BoardCollaborationGateway.name, () => { const { columnNode } = await setup(); const moveCardProps = { - cardId: 'non-existing-card', + cardId: new ObjectId().toHexString(), oldIndex: 0, newIndex: 1, fromColumnId: columnNode.id, @@ -200,7 +197,7 @@ describe(BoardCollaborationGateway.name, () => { it('should answer with failure', async () => { await setup(); - ioClient.emit('update-column-title-request', { columnId: 'non-existing-id', newTitle: 'new title' }); + ioClient.emit('update-column-title-request', { columnId: new ObjectId().toHexString(), newTitle: 'new title' }); const failure = await waitForEvent(ioClient, 'update-column-title-failure'); expect(failure).toBeDefined(); @@ -224,7 +221,7 @@ describe(BoardCollaborationGateway.name, () => { describe('when board does not exist', () => { it('should answer with failure', async () => { await setup(); - const boardId = 'non-existing-id'; + const boardId = new ObjectId().toHexString(); ioClient.emit('delete-board-request', { boardId }); const failure = await waitForEvent(ioClient, 'delete-board-failure'); @@ -250,7 +247,7 @@ describe(BoardCollaborationGateway.name, () => { describe('when board does not exist', () => { it('should answer with failure', async () => { await setup(); - const boardId = 'non-existing-id'; + const boardId = new ObjectId().toHexString(); ioClient.emit('update-board-title-request', { boardId, newTitle: 'new title' }); const failure = await waitForEvent(ioClient, 'update-board-title-failure'); @@ -276,7 +273,7 @@ describe(BoardCollaborationGateway.name, () => { describe('when board does not exist', () => { it('should answer with failure', async () => { await setup(); - const boardId = 'non-existing-id'; + const boardId = new ObjectId().toHexString(); ioClient.emit('create-column-request', { boardId }); const failure = await waitForEvent(ioClient, 'create-column-failure'); @@ -302,7 +299,7 @@ describe(BoardCollaborationGateway.name, () => { describe('when board does not exist', () => { it('should answer with failure', async () => { await setup(); - const boardId = 'non-existing-id'; + const boardId = new ObjectId().toHexString(); ioClient.emit('update-board-visibility-request', { boardId, isVisible: false }); const failure = await waitForEvent(ioClient, 'update-board-visibility-failure'); @@ -328,7 +325,7 @@ describe(BoardCollaborationGateway.name, () => { describe('when column does not exist', () => { it('should answer with failure', async () => { await setup(); - const columnId = 'not-existing-id'; + const columnId = new ObjectId().toHexString(); ioClient.emit('delete-column-request', { columnId }); const failure = await waitForEvent(ioClient, 'delete-column-failure'); @@ -365,13 +362,13 @@ describe(BoardCollaborationGateway.name, () => { const { columnBoardNode } = await setup(); const moveColumnProps = { - columnId: 'non-existing-id', + columnId: new ObjectId().toHexString(), targetBoardId: columnBoardNode.id, newIndex: 1, columnMove: { addedIndex: 1, removedIndex: 0, - columnId: 'non-existing-id', + columnId: new ObjectId().toHexString(), }, }; @@ -399,7 +396,7 @@ describe(BoardCollaborationGateway.name, () => { describe('when card does not exist', () => { it('should answer with failure', async () => { await setup(); - const cardId = 'non-existing-id'; + const cardId = new ObjectId().toHexString(); ioClient.emit('update-card-title-request', { cardId, newTitle: 'new title' }); const failure = await waitForEvent(ioClient, 'update-card-title-failure'); @@ -426,7 +423,7 @@ describe(BoardCollaborationGateway.name, () => { describe('when card does not exist', () => { it('should answer with failure', async () => { await setup(); - const cardId = 'non-existing-id'; + const cardId = new ObjectId().toHexString(); ioClient.emit('update-card-height-request', { cardId, newHeight: 200 }); const failure = await waitForEvent(ioClient, 'update-card-height-failure'); @@ -452,7 +449,7 @@ describe(BoardCollaborationGateway.name, () => { describe('when card does not exist', () => { it('should answer with failure', async () => { await setup(); - const cardId = 'non-existing-id'; + const cardId = new ObjectId().toHexString(); ioClient.emit('fetch-card-request', { cardIds: [cardId] }); const failure = await waitForEvent(ioClient, 'fetch-card-failure'); @@ -478,7 +475,7 @@ describe(BoardCollaborationGateway.name, () => { describe('when card does not exist', () => { it('should answer with failure', async () => { await setup(); - const cardId = 'non-existing-id'; + const cardId = new ObjectId().toHexString(); ioClient.emit('delete-card-request', { cardId }); const failure = await waitForEvent(ioClient, 'delete-card-failure'); @@ -519,7 +516,7 @@ describe(BoardCollaborationGateway.name, () => { describe('when element does not exist', () => { it('should answer with failure', async () => { await setup(); - const elementId = 'non-existing-id'; + const elementId = new ObjectId().toHexString(); ioClient.emit('delete-element-request', { elementId }); const failure = await waitForEvent(ioClient, 'delete-element-failure'); @@ -535,23 +532,25 @@ describe(BoardCollaborationGateway.name, () => { const { elementNodes } = await setup(); const elementId = elementNodes[0].id; - ioClient.emit('update-element-request', { + const payload = { elementId, data: { type: ContentElementType.RICH_TEXT, content: { text: 'some new text', inputFormat: InputFormat.PLAIN_TEXT }, }, - }); + }; + + ioClient.emit('update-element-request', payload); const success = await waitForEvent(ioClient, 'update-element-success'); - expect(success).toEqual({ elementId }); + expect(success).toEqual(payload); }); }); describe('when element does not exist', () => { it('should answer with failure', async () => { await setup(); - const elementId = 'non-existing-id'; + const elementId = new ObjectId().toHexString(); ioClient.emit('update-element-request', { elementId, 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 6446e7e5e8e..51a2ae9e652 100644 --- a/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts +++ b/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts @@ -1,9 +1,10 @@ import { MikroORM, UseRequestContext } from '@mikro-orm/core'; -import { UseGuards } from '@nestjs/common'; +import { UseGuards, UsePipes } from '@nestjs/common'; import { SubscribeMessage, WebSocketGateway, WsException } from '@nestjs/websockets'; import { LegacyLogger } from '@src/core/logger'; import { WsJwtAuthGuard } from '@src/modules/authentication/guard/ws-jwt-auth.guard'; import { BoardResponseMapper, CardResponseMapper } from '../controller/mapper'; +import { ColumnResponseMapper } from '../controller/mapper/column-response.mapper'; import { BoardDoAuthorizableService } from '../service'; import { BoardUc, CardUc, ColumnUc, ElementUc } from '../uc'; import { @@ -13,23 +14,24 @@ import { UpdateColumnTitleMessageParams, } from './dto'; import BoardCollaborationConfiguration from './dto/board-collaboration-config'; -import { ColumnResponseMapper } from '../controller/mapper/column-response.mapper'; import { CreateColumnMessageParams } from './dto/create-column.message.param'; +import { CreateContentElementMessageParams } from './dto/create-content-element.message.param'; import { DeleteBoardMessageParams } from './dto/delete-board.message.param'; import { DeleteCardMessageParams } from './dto/delete-card.message.param'; +import { DeleteContentElementMessageParams } from './dto/delete-content-element.message.param'; import { FetchBoardMessageParams } from './dto/fetch-board.message.param'; import { FetchCardsMessageParams } from './dto/fetch-cards.message.param'; import { MoveColumnMessageParams } from './dto/move-column.message.param'; +import { MoveContentElementMessageParams } from './dto/move-content-element.message.param'; import { UpdateBoardTitleMessageParams } from './dto/update-board-title.message.param'; import { UpdateBoardVisibilityMessageParams } from './dto/update-board-visibility.message.param'; import { UpdateCardHeightMessageParams } from './dto/update-card-height.message.param'; import { UpdateCardTitleMessageParams } from './dto/update-card-title.message.param'; -import { Socket } from './types'; -import { CreateContentElementMessageParams } from './dto/create-content-element.message.param'; -import { DeleteContentElementMessageParams } from './dto/delete-content-element.message.param'; import { UpdateContentElementMessageParams } from './dto/update-content-element.message.param'; -import { MoveContentElementMessageParams } from './dto/move-content-element.message.param'; +import { Socket } from './types'; +import { WsValidationPipe } from './ws-validation.pipe'; +@UsePipes(new WsValidationPipe()) @WebSocketGateway(BoardCollaborationConfiguration.websocket) @UseGuards(WsJwtAuthGuard) export class BoardCollaborationGateway { diff --git a/apps/server/src/modules/board/gateway/ws-validation.pipe.ts b/apps/server/src/modules/board/gateway/ws-validation.pipe.ts new file mode 100644 index 00000000000..fc6f151a056 --- /dev/null +++ b/apps/server/src/modules/board/gateway/ws-validation.pipe.ts @@ -0,0 +1,26 @@ +import { ValidationPipe, ValidationError } from '@nestjs/common'; +import { WsException } from '@nestjs/websockets'; + +export class WsValidationPipe extends ValidationPipe { + constructor() { + super({ + // enable DTO instance creation for incoming data + transform: true, + transformOptions: { + // enable type coersion, requires transform:true + enableImplicitConversion: true, + }, + whitelist: true, // only pass valid @ApiProperty-decorated DTO properties, remove others + forbidNonWhitelisted: false, // additional params are just skipped (required when extracting multiple DTO from single query) + forbidUnknownValues: true, + exceptionFactory: (errors: ValidationError[]) => new WsException(errors), + validationError: { + // make sure target (DTO) is set on validation error + // we need this to be able to get DTO metadata for checking if a value has to be the obfuscated on output + // see e.g. ErrorLoggable + target: true, + value: true, + }, + }); + } +} From b6391889ad81807692389efb2040bf4d63259e09 Mon Sep 17 00:00:00 2001 From: Thomas Feldtkeller Date: Tue, 14 May 2024 16:13:19 +0200 Subject: [PATCH 05/19] chore: fix merge conflict in test --- .../board/gateway/api-test/board-collaboration.gateway.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d9eaf2cd40d..6348e107c7e 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 @@ -582,7 +582,7 @@ describe(BoardCollaborationGateway.name, () => { describe('when element does not exist', () => { it('should answer with failure', async () => { const { cardNodes } = await setup(); - const elementId = 'non-existing-id'; + const elementId = new ObjectId().toHexString(); const toCardId = cardNodes[0].id; ioClient.emit('move-element-request', { elementId, toCardId, toPosition: 2 }); From 8470106a6812a2363ba6c0cbbdbffc97bbf84c29 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Wed, 15 May 2024 17:10:02 +0200 Subject: [PATCH 06/19] chore: add mapping for contentElements --- .../modules/board/gateway/board-collaboration.gateway.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 51a2ae9e652..c65a3cd4394 100644 --- a/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts +++ b/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts @@ -3,7 +3,7 @@ import { UseGuards, UsePipes } from '@nestjs/common'; import { SubscribeMessage, WebSocketGateway, WsException } from '@nestjs/websockets'; import { LegacyLogger } from '@src/core/logger'; import { WsJwtAuthGuard } from '@src/modules/authentication/guard/ws-jwt-auth.guard'; -import { BoardResponseMapper, CardResponseMapper } from '../controller/mapper'; +import { BoardResponseMapper, CardResponseMapper, ContentElementResponseFactory } from '../controller/mapper'; import { ColumnResponseMapper } from '../controller/mapper/column-response.mapper'; import { BoardDoAuthorizableService } from '../service'; import { BoardUc, CardUc, ColumnUc, ElementUc } from '../uc'; @@ -285,10 +285,10 @@ export class BoardCollaborationGateway { async createElement(client: Socket, data: CreateContentElementMessageParams) { try { const { userId } = this.getCurrentUser(client); - const card = await this.cardUc.createElement(userId, data.cardId, data.type, data.toPosition); + const element = await this.cardUc.createElement(userId, data.cardId, data.type, data.toPosition); const responsePayload = { ...data, - newElement: card.getProps(), + newElement: ContentElementResponseFactory.mapToResponse(element), }; const room = await this.ensureUserInRoom(client, data.cardId); From d56dc8c2781a8456ad8913683039230d9234f5fc Mon Sep 17 00:00:00 2001 From: Murat Merdoglu Date: Wed, 15 May 2024 17:32:39 +0200 Subject: [PATCH 07/19] add missing moveCard parameters --- .../modules/board/gateway/dto/move-card.message.param.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/server/src/modules/board/gateway/dto/move-card.message.param.ts b/apps/server/src/modules/board/gateway/dto/move-card.message.param.ts index 79cf9892645..30a78ce4d7c 100644 --- a/apps/server/src/modules/board/gateway/dto/move-card.message.param.ts +++ b/apps/server/src/modules/board/gateway/dto/move-card.message.param.ts @@ -4,10 +4,17 @@ export class MoveCardMessageParams { @IsMongoId() cardId!: string; + @IsMongoId() + fromColumnId!: string; + @IsMongoId() toColumnId!: string; @IsNumber() @Min(0) newIndex!: number; + + @IsNumber() + @Min(0) + oldIndex!: number; } From bf37817242eab519b4b541525b3e0591edf1d72b Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Thu, 16 May 2024 15:31:10 +0200 Subject: [PATCH 08/19] fix: drag And drop of columns --- .../modules/board/gateway/dto/move-column.message.param.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/apps/server/src/modules/board/gateway/dto/move-column.message.param.ts b/apps/server/src/modules/board/gateway/dto/move-column.message.param.ts index ce4396f3bed..f115c96795e 100644 --- a/apps/server/src/modules/board/gateway/dto/move-column.message.param.ts +++ b/apps/server/src/modules/board/gateway/dto/move-column.message.param.ts @@ -12,16 +12,9 @@ export class ColumnMove { } export class MoveColumnMessageParams { - @IsMongoId() - columnId!: string; - @IsMongoId() targetBoardId!: string; - @IsNumber() - @Min(0) - newIndex!: number; - @ValidateNested() columnMove!: ColumnMove; } From 7735797611319e8217d1a2d5bbd5107171e524ad Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Fri, 17 May 2024 10:45:00 +0200 Subject: [PATCH 09/19] chore: extending move card message with additional parameters --- .../board/gateway/dto/move-card.message.param.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/server/src/modules/board/gateway/dto/move-card.message.param.ts b/apps/server/src/modules/board/gateway/dto/move-card.message.param.ts index 30a78ce4d7c..8a3bd3a2026 100644 --- a/apps/server/src/modules/board/gateway/dto/move-card.message.param.ts +++ b/apps/server/src/modules/board/gateway/dto/move-card.message.param.ts @@ -1,4 +1,4 @@ -import { IsMongoId, IsNumber, Min } from 'class-validator'; +import { IsBoolean, IsMongoId, IsNumber, IsOptional, Min } from 'class-validator'; export class MoveCardMessageParams { @IsMongoId() @@ -17,4 +17,16 @@ export class MoveCardMessageParams { @IsNumber() @Min(0) oldIndex!: number; + + @IsNumber() + @Min(0) + fromColumnIndex!: number; + + @IsNumber() + @Min(0) + toColumnIndex!: number; + + @IsOptional() + @IsBoolean() + forceNextTick?: boolean; } From 4c67cc36c9d22165b176ae2b15993e34dc0b71e0 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Fri, 17 May 2024 16:35:58 +0200 Subject: [PATCH 10/19] chore: reduce code duplication --- .../update-element-content.body.params.ts | 1 - .../update-content-element.message.param.ts | 51 ++----------------- 2 files changed, 3 insertions(+), 49 deletions(-) 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 3c4f5857917..700b75ac950 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 @@ -150,7 +150,6 @@ export class UpdateElementContentBodyParams { { value: RichTextElementContentBody, name: ContentElementType.RICH_TEXT }, { value: SubmissionContainerElementContentBody, name: ContentElementType.SUBMISSION_CONTAINER }, { value: ExternalToolElementContentBody, name: ContentElementType.EXTERNAL_TOOL }, - { value: ExternalToolElementContentBody, name: ContentElementType.DRAWING }, { value: DrawingElementContentBody, name: ContentElementType.DRAWING }, ], }, diff --git a/apps/server/src/modules/board/gateway/dto/update-content-element.message.param.ts b/apps/server/src/modules/board/gateway/dto/update-content-element.message.param.ts index 7e83aed6f16..5f06e9dd0d2 100644 --- a/apps/server/src/modules/board/gateway/dto/update-content-element.message.param.ts +++ b/apps/server/src/modules/board/gateway/dto/update-content-element.message.param.ts @@ -1,52 +1,7 @@ -import { ApiProperty, getSchemaPath } from '@nestjs/swagger'; -import { ContentElementType } from '@shared/domain/domainobject'; -import { Type } from 'class-transformer'; -import { IsMongoId, ValidateNested } from 'class-validator'; -import { - DrawingElementContentBody, - ElementContentBody, - ExternalToolElementContentBody, - FileElementContentBody, - LinkElementContentBody, - RichTextElementContentBody, - SubmissionContainerElementContentBody, -} from '../../controller/dto'; +import { IsMongoId } from 'class-validator'; +import { UpdateElementContentBodyParams } from '../../controller/dto'; -export class UpdateContentElementMessageParams { +export class UpdateContentElementMessageParams extends UpdateElementContentBodyParams { @IsMongoId() elementId!: string; - - @ValidateNested() - @Type(() => ElementContentBody, { - discriminator: { - property: 'type', - subTypes: [ - { value: FileElementContentBody, name: ContentElementType.FILE }, - { value: LinkElementContentBody, name: ContentElementType.LINK }, - { value: RichTextElementContentBody, name: ContentElementType.RICH_TEXT }, - { value: SubmissionContainerElementContentBody, name: ContentElementType.SUBMISSION_CONTAINER }, - { value: ExternalToolElementContentBody, name: ContentElementType.EXTERNAL_TOOL }, - { value: ExternalToolElementContentBody, name: ContentElementType.DRAWING }, - { value: DrawingElementContentBody, name: ContentElementType.DRAWING }, - ], - }, - keepDiscriminatorProperty: true, - }) - @ApiProperty({ - oneOf: [ - { $ref: getSchemaPath(FileElementContentBody) }, - { $ref: getSchemaPath(LinkElementContentBody) }, - { $ref: getSchemaPath(RichTextElementContentBody) }, - { $ref: getSchemaPath(SubmissionContainerElementContentBody) }, - { $ref: getSchemaPath(ExternalToolElementContentBody) }, - { $ref: getSchemaPath(DrawingElementContentBody) }, - ], - }) - data!: - | FileElementContentBody - | LinkElementContentBody - | RichTextElementContentBody - | SubmissionContainerElementContentBody - | ExternalToolElementContentBody - | DrawingElementContentBody; } From 9a0efc7f1753416c3ccf760c78db1c1bb69e5198 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Fri, 17 May 2024 16:38:01 +0200 Subject: [PATCH 11/19] chore: add missing test --- .../api-test/board-collaboration.gateway.spec.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 6348e107c7e..6a9ffa69fc4 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 @@ -498,6 +498,18 @@ describe(BoardCollaborationGateway.name, () => { expect(Object.keys(success)).toEqual(expect.arrayContaining(['cardId', 'newElement'])); }); + + describe('when element does not exist', () => { + it('should answer with failure', async () => { + await setup(); + const elementId = new ObjectId().toHexString(); + + ioClient.emit('create-element-request', { elementId }); + const failure = await waitForEvent(ioClient, 'create-element-failure'); + + expect(failure).toBeDefined(); + }); + }); }); describe('delete element', () => { From 2060d47bcb5de1900cecf20e092c4f4f826b8ba5 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Fri, 17 May 2024 21:43:27 +0200 Subject: [PATCH 12/19] chore: remove unneeded import --- .../gateway/api-test/board-collaboration.gateway.spec.ts | 8 ++++---- .../board/gateway/dto/move-column.message.param.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) 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 6a9ffa69fc4..47544f1f6e9 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 @@ -354,7 +354,7 @@ describe(BoardCollaborationGateway.name, () => { ioClient.emit('move-column-request', moveColumnProps); const success = await waitForEvent(ioClient, 'move-column-success'); - expect(success).toEqual(expect.objectContaining(moveColumnProps)); + expect(success).toEqual(moveColumnProps); }); }); describe('when column does not exist', () => { @@ -499,12 +499,12 @@ describe(BoardCollaborationGateway.name, () => { expect(Object.keys(success)).toEqual(expect.arrayContaining(['cardId', 'newElement'])); }); - describe('when element does not exist', () => { + describe('when card does not exist', () => { it('should answer with failure', async () => { await setup(); - const elementId = new ObjectId().toHexString(); + const cardId = new ObjectId().toHexString(); - ioClient.emit('create-element-request', { elementId }); + ioClient.emit('create-element-request', { cardId }); const failure = await waitForEvent(ioClient, 'create-element-failure'); expect(failure).toBeDefined(); diff --git a/apps/server/src/modules/board/gateway/dto/move-column.message.param.ts b/apps/server/src/modules/board/gateway/dto/move-column.message.param.ts index f115c96795e..ee87f2204cb 100644 --- a/apps/server/src/modules/board/gateway/dto/move-column.message.param.ts +++ b/apps/server/src/modules/board/gateway/dto/move-column.message.param.ts @@ -1,4 +1,4 @@ -import { IsMongoId, IsNumber, IsString, Min, ValidateNested } from 'class-validator'; +import { IsMongoId, IsNumber, IsString, ValidateNested } from 'class-validator'; export class ColumnMove { @IsNumber() From 01eaf51e5be7d9afb8801f024eed046e1f7edc25 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Tue, 21 May 2024 15:43:13 +0200 Subject: [PATCH 13/19] fix: problem with delete element --- .../board/gateway/dto/delete-content-element.message.param.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/server/src/modules/board/gateway/dto/delete-content-element.message.param.ts b/apps/server/src/modules/board/gateway/dto/delete-content-element.message.param.ts index 47d0ad015e9..88fec01e6a2 100644 --- a/apps/server/src/modules/board/gateway/dto/delete-content-element.message.param.ts +++ b/apps/server/src/modules/board/gateway/dto/delete-content-element.message.param.ts @@ -1,6 +1,9 @@ import { IsMongoId } from 'class-validator'; export class DeleteContentElementMessageParams { + @IsMongoId() + cardId!: string; + @IsMongoId() elementId!: string; } From cc2819247a2e28cb12ec2bfef61e8c0a9374045f Mon Sep 17 00:00:00 2001 From: NFriedo <69233063+NFriedo@users.noreply.github.com> Date: Wed, 22 May 2024 13:52:39 +0200 Subject: [PATCH 14/19] fix move card, move column, create element api tests --- .../api-test/board-collaboration.gateway.spec.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) 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 47544f1f6e9..ed0c4f4df64 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 @@ -134,6 +134,8 @@ describe(BoardCollaborationGateway.name, () => { oldIndex: 0, newIndex: 0, fromColumnId: columnNode.id, + fromColumnIndex: 0, + toColumnIndex: 1, toColumnId: columnNode2.id, }; @@ -150,7 +152,11 @@ describe(BoardCollaborationGateway.name, () => { const moveCardProps = { cardId: cardNodes[0].id, + oldIndex: 0, newIndex: 1, + fromColumnId: columnNode.id, + fromColumnIndex: 0, + toColumnIndex: 0, toColumnId: columnNode.id, }; @@ -170,7 +176,9 @@ describe(BoardCollaborationGateway.name, () => { oldIndex: 0, newIndex: 1, fromColumnId: columnNode.id, + fromColumnIndex: 0, toColumnId: columnNode.id, + toColumnIndex: 0, }; ioClient.emit('move-card-request', moveCardProps); @@ -341,9 +349,7 @@ describe(BoardCollaborationGateway.name, () => { const { columnBoardNode, columnNode } = await setup(); const moveColumnProps = { - columnId: columnNode.id, targetBoardId: columnBoardNode.id, - newIndex: 1, columnMove: { addedIndex: 1, removedIndex: 0, @@ -504,7 +510,7 @@ describe(BoardCollaborationGateway.name, () => { await setup(); const cardId = new ObjectId().toHexString(); - ioClient.emit('create-element-request', { cardId }); + ioClient.emit('create-element-request', { cardId, type: ContentElementType.RICH_TEXT }); const failure = await waitForEvent(ioClient, 'create-element-failure'); expect(failure).toBeDefined(); From 4c2ab51a9c2b42b49802fc6c82cc55b982e7b878 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Wed, 22 May 2024 13:54:18 +0200 Subject: [PATCH 15/19] fix: deleteElement tests --- .../api-test/board-collaboration.gateway.spec.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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 ed0c4f4df64..772b022d9e4 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 @@ -521,22 +521,24 @@ describe(BoardCollaborationGateway.name, () => { describe('delete element', () => { describe('when element exists', () => { it('should answer with success', async () => { - const { elementNodes } = await setup(); + const { cardNodes, elementNodes } = await setup(); + const cardId = cardNodes[0].id; const elementId = elementNodes[0].id; - ioClient.emit('delete-element-request', { elementId }); + ioClient.emit('delete-element-request', { cardId, elementId }); const success = await waitForEvent(ioClient, 'delete-element-success'); - expect(success).toEqual({ elementId }); + expect(success).toEqual({ cardId, elementId }); }); }); describe('when element does not exist', () => { it('should answer with failure', async () => { - await setup(); + const { cardNodes } = await setup(); + const cardId = cardNodes[0].id; const elementId = new ObjectId().toHexString(); - ioClient.emit('delete-element-request', { elementId }); + ioClient.emit('delete-element-request', { cardId, elementId }); const failure = await waitForEvent(ioClient, 'delete-element-failure'); expect(failure).toBeDefined(); From a25f1f4d7e8403c8082cf0b50e274a0376f5fc99 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Wed, 22 May 2024 16:44:31 +0200 Subject: [PATCH 16/19] chore: removed console.logs --- .../src/modules/board/gateway/board-collaboration.gateway.ts | 2 -- 1 file changed, 2 deletions(-) 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 c65a3cd4394..070d1407fe6 100644 --- a/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts +++ b/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts @@ -295,7 +295,6 @@ export class BoardCollaborationGateway { client.to(room).emit('create-element-success', responsePayload); client.emit('create-element-success', responsePayload); } catch (err) { - console.log('create-element-request', err); client.emit('create-element-failure', new Error('Failed to create element')); } } @@ -311,7 +310,6 @@ export class BoardCollaborationGateway { client.to(room).emit('update-element-success', data); client.emit('update-element-success', data); } catch (err) { - console.log('update-element-request', err); client.emit('update-element-failure', new Error('Failed to update element')); } } From 7cde1aeb179c8d5ebca8fb37ed64a8f13ee76ec0 Mon Sep 17 00:00:00 2001 From: Thomas Feldtkeller Date: Thu, 23 May 2024 10:34:31 +0200 Subject: [PATCH 17/19] test validation error --- .../api-test/board-collaboration.gateway.spec.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) 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 772b022d9e4..bd429663204 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 @@ -71,6 +71,17 @@ describe(BoardCollaborationGateway.name, () => { expect(ws).toBeDefined(); }); + describe('validation errors', () => { + it('should answer with failure', async () => { + await setup(); + ioClient.emit('create-card-request', { columnId: 'invalid' }); + + const failure = await waitForEvent(ioClient, 'exception'); + + expect(failure).toBeDefined(); + }); + }); + describe('create card', () => { describe('when column exists', () => { it('should answer with new card', async () => { From 0975573955665e5b1794c60a3cf524c8086afb03 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Thu, 23 May 2024 14:53:06 +0200 Subject: [PATCH 18/19] add isOwnAction-property to success actions --- .../gateway/board-collaboration.gateway.ts | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) 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 070d1407fe6..d0773466d73 100644 --- a/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts +++ b/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts @@ -64,8 +64,8 @@ export class BoardCollaborationGateway { const room = await this.ensureUserInRoom(client, data.boardId); await this.boardUc.deleteBoard(userId, data.boardId); - client.to(room).emit('delete-board-success', data); - client.emit('delete-board-success', data); + client.to(room).emit('delete-board-success', { ...data, isOwnAction: false }); + client.emit('delete-board-success', { ...data, isOwnAction: true }); } catch (err) { client.emit('delete-board-failure', new Error('Failed to delete board')); } @@ -79,8 +79,8 @@ export class BoardCollaborationGateway { await this.boardUc.updateBoardTitle(userId, data.boardId, data.newTitle); const room = await this.ensureUserInRoom(client, data.boardId); - client.to(room).emit('update-board-title-success', data); - client.emit('update-board-title-success', data); + client.to(room).emit('update-board-title-success', { ...data, isOwnAction: false }); + client.emit('update-board-title-success', { ...data, isOwnAction: true }); } catch (err) { client.emit('update-board-title-failure', new Error('Failed to update board title')); } @@ -94,8 +94,8 @@ export class BoardCollaborationGateway { await this.cardUc.updateCardTitle(userId, data.cardId, data.newTitle); const room = await this.ensureUserInRoom(client, data.cardId); - client.to(room).emit('update-card-title-success', data); - client.emit('update-card-title-success', data); + client.to(room).emit('update-card-title-success', { ...data, isOwnAction: false }); + client.emit('update-card-title-success', { ...data, isOwnAction: true }); } catch (err) { client.emit('update-card-title-failure', new Error('Failed to update card title')); } @@ -109,8 +109,8 @@ export class BoardCollaborationGateway { await this.cardUc.updateCardHeight(userId, data.cardId, data.newHeight); const room = await this.ensureUserInRoom(client, data.cardId); - client.to(room).emit('update-card-height-success', data); - client.emit('update-card-height-success', data); + client.to(room).emit('update-card-height-success', { ...data, isOwnAction: false }); + client.emit('update-card-height-success', { ...data, isOwnAction: true }); } catch (err) { client.emit('update-card-height-failure', new Error('Failed to update card height')); } @@ -124,8 +124,8 @@ export class BoardCollaborationGateway { const room = await this.ensureUserInRoom(client, data.cardId); await this.cardUc.deleteCard(userId, data.cardId); - client.to(room).emit('delete-card-success', data); - client.emit('delete-card-success', data); + client.to(room).emit('delete-card-success', { ...data, isOwnAction: false }); + client.emit('delete-card-success', { ...data, isOwnAction: true }); } catch (err) { client.emit('delete-card-failure', new Error('Failed to update card height')); } @@ -163,8 +163,8 @@ export class BoardCollaborationGateway { }; const room = await this.ensureUserInRoom(client, data.boardId); - client.to(room).emit('create-column-success', responsePayload); - client.emit('create-column-success', responsePayload); + client.to(room).emit('create-column-success', { ...responsePayload, isOwnAction: false }); + client.emit('create-column-success', { ...responsePayload, isOwnAction: true }); // payload needs to be returned to allow the client to do sequential operation // of createColumn and move the card into that column @@ -198,8 +198,8 @@ export class BoardCollaborationGateway { await this.columnUc.moveCard(userId, data.cardId, data.toColumnId, data.newIndex); const room = await this.ensureUserInRoom(client, data.cardId); - client.to(room).emit('move-card-success', data); - client.emit('move-card-success', data); + client.to(room).emit('move-card-success', { ...data, isOwnAction: false }); + client.emit('move-card-success', { ...data, isOwnAction: true }); } catch (err) { client.emit('move-card-failure', new Error('Failed to move card')); } @@ -213,8 +213,8 @@ export class BoardCollaborationGateway { await this.boardUc.moveColumn(userId, data.columnMove.columnId, data.targetBoardId, data.columnMove.addedIndex); const room = await this.ensureUserInRoom(client, data.targetBoardId); - client.to(room).emit('move-column-success', data); - client.emit('move-column-success', data); + client.to(room).emit('move-column-success', { ...data, isOwnAction: false }); + client.emit('move-column-success', { ...data, isOwnAction: true }); } catch (err) { client.emit('move-column-failure', new Error('Failed to move column')); } @@ -228,8 +228,8 @@ export class BoardCollaborationGateway { await this.columnUc.updateColumnTitle(userId, data.columnId, data.newTitle); const room = await this.ensureUserInRoom(client, data.columnId); - client.to(room).emit('update-column-title-success', data); - client.emit('update-column-title-success', data); + client.to(room).emit('update-column-title-success', { ...data, isOwnAction: false }); + client.emit('update-column-title-success', { ...data, isOwnAction: true }); } catch (err) { client.emit('update-column-title-failure', new Error('Failed to update column title')); } @@ -243,8 +243,8 @@ export class BoardCollaborationGateway { await this.boardUc.updateVisibility(userId, data.boardId, data.isVisible); const room = await this.ensureUserInRoom(client, data.boardId); - client.to(room).emit('update-board-visibility-success', data); - client.emit('update-board-visibility-success', data); + client.to(room).emit('update-board-visibility-success', { ...data, isOwnAction: false }); + client.emit('update-board-visibility-success', { ...data, isOwnAction: true }); } catch (err) { client.emit('update-board-visibility-failure', new Error('Failed to update board visibility')); } @@ -258,8 +258,8 @@ export class BoardCollaborationGateway { const room = await this.ensureUserInRoom(client, data.columnId); await this.columnUc.deleteColumn(userId, data.columnId); - client.to(room).emit('delete-column-success', data); - client.emit('delete-column-success', data); + client.to(room).emit('delete-column-success', { ...data, isOwnAction: false }); + client.emit('delete-column-success', { ...data, isOwnAction: true }); } catch (err) { client.emit('delete-column-failure', new Error('Failed to delete column')); } @@ -274,7 +274,7 @@ export class BoardCollaborationGateway { const cardResponses = cards.map((card) => CardResponseMapper.mapToResponse(card)); await this.ensureUserInRoom(client, data.cardIds[0]); - client.emit('fetch-card-success', { cards: cardResponses }); + client.emit('fetch-card-success', { cards: cardResponses, isOwnAction: true }); } catch (err) { client.emit('fetch-card-failure', new Error('Failed to fetch board')); } @@ -292,8 +292,8 @@ export class BoardCollaborationGateway { }; const room = await this.ensureUserInRoom(client, data.cardId); - client.to(room).emit('create-element-success', responsePayload); - client.emit('create-element-success', responsePayload); + client.to(room).emit('create-element-success', { ...responsePayload, isOwnAction: false }); + client.emit('create-element-success', { ...responsePayload, isOwnAction: true }); } catch (err) { client.emit('create-element-failure', new Error('Failed to create element')); } @@ -307,8 +307,8 @@ export class BoardCollaborationGateway { await this.elementUc.updateElement(userId, data.elementId, data.data.content); const room = await this.ensureUserInRoom(client, data.elementId); - client.to(room).emit('update-element-success', data); - client.emit('update-element-success', data); + client.to(room).emit('update-element-success', { ...data, isOwnAction: false }); + client.emit('update-element-success', { ...data, isOwnAction: true }); } catch (err) { client.emit('update-element-failure', new Error('Failed to update element')); } @@ -322,8 +322,8 @@ export class BoardCollaborationGateway { const room = await this.ensureUserInRoom(client, data.elementId); await this.elementUc.deleteElement(userId, data.elementId); - client.to(room).emit('delete-element-success', data); - client.emit('delete-element-success', data); + client.to(room).emit('delete-element-success', { ...data, isOwnAction: false }); + client.emit('delete-element-success', { ...data, isOwnAction: true }); } catch (err) { client.emit('delete-element-failure', new Error('Failed to delete element')); } @@ -337,8 +337,8 @@ export class BoardCollaborationGateway { await this.cardUc.moveElement(userId, data.elementId, data.toCardId, data.toPosition); const room = await this.ensureUserInRoom(client, data.elementId); - client.to(room).emit('move-element-success', data); - client.emit('move-element-success', data); + client.to(room).emit('move-element-success', { ...data, isOwnAction: false }); + client.emit('move-element-success', { ...data, isOwnAction: true }); } catch (err) { client.emit('move-element-failure', new Error('Failed to move element')); } From 3eb7b8da94ee02be7071326d596dbd1ade8a130c Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Thu, 23 May 2024 16:01:39 +0200 Subject: [PATCH 19/19] fix unit tests --- .../api-test/board-collaboration.gateway.spec.ts | 10 +++++----- .../board/gateway/board-collaboration.gateway.ts | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) 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 bd429663204..d7ccf66a707 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 @@ -371,7 +371,7 @@ describe(BoardCollaborationGateway.name, () => { ioClient.emit('move-column-request', moveColumnProps); const success = await waitForEvent(ioClient, 'move-column-success'); - expect(success).toEqual(moveColumnProps); + expect(success).toEqual(expect.objectContaining(moveColumnProps)); }); }); describe('when column does not exist', () => { @@ -485,7 +485,7 @@ describe(BoardCollaborationGateway.name, () => { ioClient.emit('delete-card-request', { cardId }); const success = await waitForEvent(ioClient, 'delete-card-success'); - expect(success).toEqual({ cardId }); + expect(success).toEqual(expect.objectContaining({ cardId })); }); }); @@ -539,7 +539,7 @@ describe(BoardCollaborationGateway.name, () => { ioClient.emit('delete-element-request', { cardId, elementId }); const success = await waitForEvent(ioClient, 'delete-element-success'); - expect(success).toEqual({ cardId, elementId }); + expect(success).toEqual(expect.objectContaining({ cardId, elementId })); }); }); @@ -574,7 +574,7 @@ describe(BoardCollaborationGateway.name, () => { ioClient.emit('update-element-request', payload); const success = await waitForEvent(ioClient, 'update-element-success'); - expect(success).toEqual(payload); + expect(success).toEqual(expect.objectContaining(payload)); }); }); @@ -606,7 +606,7 @@ describe(BoardCollaborationGateway.name, () => { ioClient.emit('move-element-request', data); const success = await waitForEvent(ioClient, 'move-element-success'); - expect(success).toEqual(data); + expect(success).toEqual(expect.objectContaining(data)); }); }); 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 d0773466d73..c14a8c26986 100644 --- a/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts +++ b/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts @@ -143,8 +143,8 @@ export class BoardCollaborationGateway { }; const room = await this.ensureUserInRoom(client, data.columnId); - client.to(room).emit('create-card-success', responsePayload); - client.emit('create-card-success', responsePayload); + client.to(room).emit('create-card-success', { ...responsePayload, isOwnAction: false }); + client.emit('create-card-success', { ...responsePayload, isOwnAction: true }); } catch (err) { client.emit('create-card-failure', new Error('Failed to create card')); }