diff --git a/.gitignore b/.gitignore index 130655cf16d..a045507c02b 100644 --- a/.gitignore +++ b/.gitignore @@ -85,3 +85,5 @@ build /coverage /.nyc_output /.idea/ +/apps/server/src/modules/board/loadtest/**/*.html +/apps/server/src/modules/board/loadtest/artilleryreport.json 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 a04045d0b53..c9a6cc7163b 100644 --- a/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts +++ b/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts @@ -61,6 +61,10 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { trackExecutionTime(methodName: string, executionTimeMs: number) { if (this.metricsService) { this.metricsService.setExecutionTime(methodName, executionTimeMs); + this.metricsService.incrementActionCount(methodName); + this.metricsService.incrementActionGauge(methodName); + this.metricsService.incrementActionCount('all'); + this.metricsService.incrementActionGauge('all'); } } @@ -128,6 +132,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('update-card-height-request') + @TrackExecutionTime() @UseRequestContext() async updateCardHeight(socket: Socket, data: UpdateCardHeightMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'update-card-height' }); @@ -142,6 +147,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('delete-card-request') + @TrackExecutionTime() @UseRequestContext() async deleteCard(socket: Socket, data: DeleteCardMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'delete-card' }); @@ -178,6 +184,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('create-column-request') + @TrackExecutionTime() @UseRequestContext() async createColumn(socket: Socket, data: CreateColumnMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'create-column' }); @@ -219,6 +226,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('move-card-request') + @TrackExecutionTime() @UseRequestContext() async moveCard(socket: Socket, data: MoveCardMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'move-card' }); @@ -233,6 +241,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('move-column-request') + @TrackExecutionTime() @UseRequestContext() async moveColumn(socket: Socket, data: MoveColumnMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'move-column' }); @@ -267,6 +276,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('update-board-visibility-request') + @TrackExecutionTime() @UseRequestContext() async updateBoardVisibility(socket: Socket, data: UpdateBoardVisibilityMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'update-board-visibility' }); @@ -281,6 +291,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('delete-column-request') + @TrackExecutionTime() @UseRequestContext() async deleteColumn(socket: Socket, data: DeleteColumnMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'delete-column' }); @@ -312,6 +323,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('create-element-request') + @TrackExecutionTime() @UseRequestContext() async createElement(socket: Socket, data: CreateContentElementMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'create-element' }); @@ -346,6 +358,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('delete-element-request') + @TrackExecutionTime() @UseRequestContext() async deleteElement(socket: Socket, data: DeleteContentElementMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'delete-element' }); @@ -361,6 +374,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('move-element-request') + @TrackExecutionTime() @UseRequestContext() async moveElement(socket: Socket, data: MoveContentElementMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'move-element' }); diff --git a/apps/server/src/modules/board/loadtest/readme.md b/apps/server/src/modules/board/loadtest/readme.md new file mode 100644 index 00000000000..24cbd18605c --- /dev/null +++ b/apps/server/src/modules/board/loadtest/readme.md @@ -0,0 +1,76 @@ +# Loadtesting the boards + +The socket.io documentation suggests to use the tool artillery in order to load test a socket-io tool like our board-collaboration service. + +For defining scenarios you need to use/create Yaml-files that define which operations with which parameters need to be executed in which order. + +Some sceneraios were already prepared and are stored in the subfolder scenarios. + +## install artillery + +To run artillery from your local environment you need to install it first including an adapter that supports socketio-v3-websocket communication: + +```sh +npm install -g artillery artillery-engine-socketio-v3 +``` + +## manual execution + +To execute a scenario you can run artillery from the shell / commandline...: + +Using the `--variables` parameter it is possible to define several variables and there values that can be used in the scenerio-yaml-file: + +- **target**: defines the base url for all requests (REST and WebSocket) + e.g. `https://main.dbc.dbildungscloud.dev` +- **token**: a valid JWT for the targeted system +- **board_id**: id of an existing board the tests should be executed on + +```bash +npx artillery run --variables "{'target': 'https://main.dbc.dbildungscloud.dev', 'token': 'eJ....', 'board_id': '668d0e03bf3689d12e1e86fb' }" './scenarios/3users.yml' --output artilleryreport.json +``` + +On Windows Powershell, the variables value needs to be wrapped in singlequotes, and inside the json you need to use backslash-escaped doublequotes: + +```powershell +npx artillery run --variables '{\"target\": \"https://main.dbc.dbildungscloud.dev\", \"token\": \"eJ....\", \"board_id\": \"668d0e03bf3689d12e1e86fb\" }' './scenarios/3users.yml' --output artilleryreport.json +``` + +## visualizing the recorded results + +It is possible to generate a HTML-report based on the recorded data. + +```powershell +npx artillery report --output=$board_title.html artilleryreport.json +``` + +## automatic execution + +You can run one of the existing scenarios by executing: + +```bash +bash runScenario.sh +``` + +This will: + +1. let you choose from scenario-files +2. create a fresh JWT-webtoken +3. create a fresh board (in one of the courses) the user has access to +4. name the board by a combination of datetime and the scenario name. +5. output a link to the generated board (in order open and see the test live) +6. start the execution of the scenario against this newly created board +7. generate a html report in the end + +You can also provide the target as the first and the name of the scenario as the second parameter - to avoid the need to select those. Here is an example: + +```bash +bash runScenario.sh https://bc-6854-basic-load-tests.nbc.dbildungscloud.dev 3users +``` + +## password + +By typeing `export CARL_CORD_PASSWORD=realpassword` the script will not ask you anymore for the password to create a token. + +## Todos + +- [ ] enable optional parameter course_id diff --git a/apps/server/src/modules/board/loadtest/runScenario.sh b/apps/server/src/modules/board/loadtest/runScenario.sh new file mode 100644 index 00000000000..0187f1c579f --- /dev/null +++ b/apps/server/src/modules/board/loadtest/runScenario.sh @@ -0,0 +1,139 @@ +#!/bin/bash + +function select_target() { + declare -a targets=("https://main.nbc.dbildungscloud.dev" "https://bc-6854-basic-load-tests.nbc.dbildungscloud.dev") + echo "Please select the target for the test:" >&2 + select target in "${targets[@]}"; do + if [[ -n $target ]]; then + break + else + echo "Invalid selection. Please try again." >&2 + fi + done +} + +function select_scenario() { + # list files in the scenarios directory + scenarios_dir="./scenarios" + declare -a scenario_files=($(ls $scenarios_dir)) + + echo "Please select a scenario file for the test:" >&2 + select scenario_file in "${scenario_files[@]}"; do + if [[ -n $scenario_file ]]; then + echo "You have selected: $scenario_file" >&2 + break + else + echo "Invalid selection. Please try again." >&2 + fi + done + + scenario_name="${scenario_file%.*}" +} + +function get_credentials() { + if [ -z "$CARL_CORD_PASSWORD" ]; then + echo "Password for Carl Cord is unknown. Provide it as an enviroment variable (CARL_CORD_PASSWORD) or enter it:" + read CARL_CORD_PASSWORD + export CARL_CORD_PASSWORD + fi +} + +function get_token() { + response=$(curl -s -f -X 'POST' \ + "$target/api/v3/authentication/local" \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d "{ + \"username\": \"lehrer@schul-cloud.org\", + \"password\": \"$CARL_CORD_PASSWORD\" + }") + + if [ $? -ne 0 ]; then + echo "ERROR: Failed to get token. Please check your credentials and target URL." >&2 + exit 1 + fi + + token=$(echo $response | sed -n 's/.*"accessToken":"\([^"]*\)".*/\1/p') +} + +function get_course_id() { + response=$(curl -s -f -X 'GET' \ + "$target/api/v3/courses" \ + -H "Accept: application/json" \ + -H "Authorization: Bearer $token") + + if [ $? -ne 0 ]; then + echo "ERROR: Failed to get course list. Please check your credentials and target URL." >&2 + exit 1 + fi + + course_id=$(echo $response | sed -n 's/.*"id":"\([^"]*\)".*/\1/p') +} + +function create_board_title() { + current_date=$(date +%Y-%m-%d_%H:%M) + board_title="${current_date}_$1" +} + +function create_board() { + response=$(curl -s -f -X 'POST' \ + "$target/api/v3/boards" \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -H "Authorization: Bearer $token" \ + -d "{ + \"title\": \"$board_title\", + \"parentId\": \"$course_id\", + \"parentType\": \"course\", + \"layout\": \"columns\" + }") + + if [ $? -ne 0 ]; then + echo "ERROR: Failed to create a board." >&2 + exit 1 + fi + + board_id=$(echo $response | sed -n 's/.*"id":"\([^"]*\)".*/\1/p' ) +} + +if [ -z "$1" ]; then + select_target +else + target=$1 +fi +echo " " +echo "target: $target" + + +if [ -z "$2" ]; then + select_scenario + echo "scenario_name: $scenario_name" +else + scenario_name="$2" + scenario_name=${scenario_name//.yml/} +fi +echo "scenario_name: $scenario_name" + +get_credentials + +get_token +echo "token: ${token:0:50}..." +echo " " + +get_course_id +echo "course_id: $course_id" +echo " " + +create_board_title $scenario_name +echo "board_title: $board_title" + +create_board +echo "board_id $board_id" + +echo "board: $target/rooms/$board_id/board" +echo " " +echo "Running artillery test..." + +npx artillery run --variables "{\"target\": \"$target\", \"token\": \"$token\", \"board_id\": \"$board_id\" }" "./scenarios/$scenario_name.yml" --output artilleryreport.json + +npx artillery report --output=$board_title.html artilleryreport.json diff --git a/apps/server/src/modules/board/loadtest/scenarios/30users_5minutes.yml b/apps/server/src/modules/board/loadtest/scenarios/30users_5minutes.yml new file mode 100644 index 00000000000..567cbaf703a --- /dev/null +++ b/apps/server/src/modules/board/loadtest/scenarios/30users_5minutes.yml @@ -0,0 +1,75 @@ +config: + target: '{{ target }}' + engines: + socketio: + transport: ['websocket', 'polling'] + path: '/board-collaboration' + socketio-v3: + path: '/board-collaboration' + timeout: 1000000 + extraHeaders: + Cookie: 'jwt={{ token }}' + + phases: + - duration: 300 + arrivalRate: 10 + maxVusers: 30 + +scenarios: + - name: create card + engine: socketio-v3 + socketio-v3: + extraHeaders: + Cookie: 'jwt={{ token }}' + flow: + - think: 1 + + - emit: + channel: 'fetch-board-request' + data: + boardId: '{{ board_id }}' + + - think: 1 + + - emit: + channel: 'create-column-request' + data: + boardId: '{{ board_id }}' + response: + on: 'create-column-success' + capture: + - json: $.newColumn.id + as: columnId + + - think: 1 + + - loop: + - emit: + channel: 'create-card-request' + data: + columnId: '{{ columnId}}' + response: + on: 'create-card-success' + capture: + - json: $.newCard.id + as: cardId + + - think: 1 + + - emit: + channel: 'fetch-card-request' + data: + cardIds: + - '{{ cardId }}' + + - think: 2 + + - emit: + channel: 'update-card-title-request' + data: + cardId: '{{ cardId }}' + newTitle: 'Card {{ cardId}}' + + - think: 1 + + count: 20 diff --git a/apps/server/src/modules/board/loadtest/scenarios/3users.yml b/apps/server/src/modules/board/loadtest/scenarios/3users.yml new file mode 100644 index 00000000000..4fbeef037c8 --- /dev/null +++ b/apps/server/src/modules/board/loadtest/scenarios/3users.yml @@ -0,0 +1,57 @@ +config: + target: '{{ target }}' + engines: + socketio: + transport: ['websocket', 'polling'] + path: '/board-collaboration' + socketio-v3: + path: '/board-collaboration' + timeout: 1000000 + extraHeaders: + Cookie: 'jwt={{ token }}' + + phases: + - duration: 1 + arrivalRate: 3 + +scenarios: + - name: create card + engine: socketio-v3 + socketio-v3: + extraHeaders: + Cookie: 'jwt={{ token }}' + flow: + - log: '{{ target }}' + - think: 1 + + - emit: + channel: 'create-column-request' + data: + boardId: '{{ board_id }}' + response: + on: 'create-column-success' + capture: + - json: $.newColumn.id + as: columnId + + - think: 1 + + - emit: + channel: 'create-card-request' + data: + columnId: '{{ columnId}}' + response: + on: 'create-card-success' + capture: + - json: $.newCard.id + as: cardId + + - think: 1 + + - emit: + channel: 'update-card-title-request' + data: + cardId: '{{ cardId }}' + newTitle: 'One {{ cardId}}' + + - think: 2 diff --git a/apps/server/src/modules/board/loadtest/scenarios/6createCard_6UpdateTitle.yml b/apps/server/src/modules/board/loadtest/scenarios/6createCard_6UpdateTitle.yml new file mode 100644 index 00000000000..ad7e993f829 --- /dev/null +++ b/apps/server/src/modules/board/loadtest/scenarios/6createCard_6UpdateTitle.yml @@ -0,0 +1,70 @@ +config: + target: '{{ target }}' + engines: + socketio: + transport: ['websocket', 'polling'] + path: '/board-collaboration' + socketio-v3: + path: '/board-collaboration' + timeout: 1000000 + extraHeaders: + Cookie: 'jwt={{ token }}' + + phases: + - duration: 2 + arrivalRate: 50 + +scenarios: + - name: create card + engine: socketio-v3 + socketio: + extraHeaders: + Cookie: 'jwt={{ token }}' + socketio-v3: + extraHeaders: + Cookie: 'jwt={{ token }}' + flow: + - think: 1 + + - emit: + channel: 'create-column-request' + data: + boardId: '{{ board_id }}' + response: + on: 'create-column-success' + capture: + - json: $.newColumn.id + as: columnId + + - think: 2 + + - loop: + - emit: + channel: 'create-card-request' + data: + columnId: '{{ columnId}}' + response: + on: 'create-card-success' + capture: + - json: $.newCard.id + as: cardId + + - think: 1 + + - emit: + channel: 'fetch-card-request' + data: + cardIds: + - '{{ cardId }}' + + - think: 2 + + - emit: + channel: 'update-card-title-request' + data: + cardId: '{{ cardId }}' + newTitle: 'Card {{ cardId}}' + + - think: 1 + + count: 6 diff --git a/apps/server/src/modules/board/metrics/metrics.service.ts b/apps/server/src/modules/board/metrics/metrics.service.ts index 8019220b40e..b2a54d5ce75 100644 --- a/apps/server/src/modules/board/metrics/metrics.service.ts +++ b/apps/server/src/modules/board/metrics/metrics.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { UserDO } from '@shared/domain/domainobject'; import { RoleName } from '@shared/domain/interface'; import { UserService } from '@src/modules/user'; -import { Gauge, Summary, register } from 'prom-client'; +import { Gauge, Summary, register, Counter } from 'prom-client'; type ClientId = string; type Role = 'owner' | 'editor' | 'viewer'; @@ -19,6 +19,10 @@ export class MetricsService { private executionTimesSummary: Map> = new Map(); + private actionCounters: Map> = new Map(); + + private actionGauges: Map> = new Map(); + constructor(private readonly userService: UserService) { this.numberOfBoardroomsOnServerCounter = new Gauge({ name: 'sc_boards_rooms', @@ -88,13 +92,55 @@ export class MetricsService { summary = new Summary({ name: `sc_boards_execution_time_${actionName}`, help: 'Average execution time of a specific action in milliseconds', - maxAgeSeconds: 60, + maxAgeSeconds: 600, ageBuckets: 5, - percentiles: [0.01, 0.1, 0.9, 0.99], + percentiles: [0.01, 0.1, 0.5, 0.9, 0.99], + pruneAgedBuckets: true, }); this.executionTimesSummary.set(actionName, summary); register.registerMetric(summary); } + console.log(actionName, `executionTime: ${value.toFixed(3)} ms`); summary.observe(value); } + + public incrementActionCount(actionName: string): void { + let counter = this.actionCounters.get(actionName); + + if (!counter) { + counter = new Counter({ + name: `sc_boards_count_${actionName}`, + help: 'Number of calls for a specific action per minute', + // async collect() { + // // Invoked when the registry collects its metrics' values. + // const currentValue = await somethingAsync(); + // this.set(currentValue); + // }, + }); + this.actionCounters.set(actionName, counter); + register.registerMetric(counter); + } + counter.inc(); + console.log(actionName, `count increased`); + } + + public incrementActionGauge(actionName: string): void { + let counter = this.actionGauges.get(actionName); + + if (!counter) { + counter = new Gauge({ + name: `sc_boards_count2_${actionName}`, + help: 'Number of calls for a specific action per minute', + // async collect() { + // // Invoked when the registry collects its metrics' values. + // const currentValue = await somethingAsync(); + // this.set(currentValue); + // }, + }); + this.actionGauges.set(actionName, counter); + register.registerMetric(counter); + } + counter.inc(); + console.log(actionName, `count increased`); + } } 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 index bc9a846311f..b813761163b 100644 --- a/apps/server/src/modules/board/service/board-node-authorizable.service.ts +++ b/apps/server/src/modules/board/service/board-node-authorizable.service.ts @@ -1,7 +1,7 @@ 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 { AnyBoardNode, BoardNodeAuthorizable, UserWithBoardRoles } from '../domain'; import { BoardNodeRepo } from '../repo'; import { BoardContextService } from './internal/board-context.service'; import { BoardNodeService } from './board-node.service'; @@ -40,4 +40,51 @@ export class BoardNodeAuthorizableService implements AuthorizationLoaderService return boardNodeAuthorizable; } + + async getBoardAuthorizables(boardNodes: AnyBoardNode[]): Promise { + const rootIds = boardNodes.map((node) => node.rootId); + const parentIds = boardNodes.map((node) => node.parentId).filter((defined) => defined) as EntityId[]; + const boardNodeMap = await this.getBoardNodeMap([...rootIds, ...parentIds]); + const promises = boardNodes.map((boardNode) => { + const rootNode = boardNodeMap[boardNode.rootId]; + return this.boardContextService.getUsersWithBoardRoles(rootNode).then((users) => { + return { id: boardNode.id, users }; + }); + }); + + const results = await Promise.all(promises); + const usersMap = results.reduce((acc, { id, users }) => { + acc[id] = users; + return acc; + }, {} as Record); + + const boardNodeAuthorizables = boardNodes.map((boardNode) => { + const rootNode = boardNodeMap[boardNode.rootId]; + const parentNode = boardNode.parentId ? boardNodeMap[boardNode.parentId] : undefined; + const users = usersMap[boardNode.id]; + const boardNodeAuthorizable = new BoardNodeAuthorizable({ + users, + id: boardNode.id, + boardNode, + rootNode, + parentNode, + }); + return boardNodeAuthorizable; + }); + + return boardNodeAuthorizables; + } + + private async getBoardNodeMap(ids: EntityId[]): Promise> { + const idsUnique = Array.from(new Set(ids)); + const boardNodes = await this.boardNodeService.findByIds(idsUnique, 1); + const nodesMap: Record = boardNodes.reduce( + (map: Record, boardNode) => { + map[boardNode.id] = boardNode; + return map; + }, + {} as Record + ); + return nodesMap; + } } diff --git a/apps/server/src/modules/board/service/board-node.service.ts b/apps/server/src/modules/board/service/board-node.service.ts index bd8092a2e78..0ac96b627b1 100644 --- a/apps/server/src/modules/board/service/board-node.service.ts +++ b/apps/server/src/modules/board/service/board-node.service.ts @@ -76,6 +76,12 @@ export class BoardNodeService { return boardNode; } + async findByIds(ids: EntityId[], depth?: number): Promise { + const boardNode = this.boardNodeRepo.findByIds(ids, depth); + + return boardNode; + } + async findByClassAndId( Constructor: { new (props: S): T }, id: EntityId, 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 19a37861ea4..be5bba4afa8 100644 --- a/apps/server/src/modules/board/uc/card.uc.spec.ts +++ b/apps/server/src/modules/board/uc/card.uc.spec.ts @@ -1,5 +1,4 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ObjectId } from '@mikro-orm/mongodb'; import { Action, AuthorizationService } from '@modules/authorization'; import { Test, TestingModule } from '@nestjs/testing'; import { setupEntities, userFactory } from '@shared/testing'; @@ -73,14 +72,26 @@ describe(CardUc.name, () => { const cards = cardFactory.buildList(3); const cardIds = cards.map((c) => c.id); - boardNodeAuthorizableService.getBoardAuthorizable.mockResolvedValue( + boardNodeAuthorizableService.getBoardAuthorizables.mockResolvedValue([ new BoardNodeAuthorizable({ users: [], - id: new ObjectId().toHexString(), + id: cards[0].id, boardNode: cards[0], rootNode: columnBoardFactory.build(), - }) - ); + }), + new BoardNodeAuthorizable({ + users: [], + id: cards[1].id, + boardNode: cards[1], + rootNode: columnBoardFactory.build(), + }), + new BoardNodeAuthorizable({ + users: [], + id: cards[2].id, + boardNode: cards[2], + rootNode: columnBoardFactory.build(), + }), + ]); authorizationService.hasPermission.mockReturnValue(true); return { user, cards, cardIds }; @@ -109,7 +120,7 @@ describe(CardUc.name, () => { await uc.findCards(user.id, cardIds); - expect(boardNodeAuthorizableService.getBoardAuthorizable).toHaveBeenCalledTimes(3); + expect(boardNodeAuthorizableService.getBoardAuthorizables).toHaveBeenCalledTimes(1); }); it('should call the service to check the user permission', async () => { diff --git a/apps/server/src/modules/board/uc/card.uc.ts b/apps/server/src/modules/board/uc/card.uc.ts index 9c627d0aa99..6ab2f392766 100644 --- a/apps/server/src/modules/board/uc/card.uc.ts +++ b/apps/server/src/modules/board/uc/card.uc.ts @@ -3,7 +3,7 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain/types'; import { LegacyLogger } from '@src/core/logger'; -import { AnyContentElement, BoardNodeFactory, Card, ContentElementType } from '../domain'; +import { AnyBoardNode, AnyContentElement, BoardNodeFactory, Card, ContentElementType } from '../domain'; import { BoardNodeAuthorizableService, BoardNodePermissionService, BoardNodeService } from '../service'; @Injectable() @@ -29,19 +29,14 @@ export class CardUc { 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 }) => { + const boardAuthorizables = await this.boardNodeAuthorizableService.getBoardAuthorizables(cards); + + const allowedCards = boardAuthorizables.reduce((allowedNodes: AnyBoardNode[], boardNodeAuthorizable) => { if (this.authorizationService.hasPermission(user, boardNodeAuthorizable, context)) { - allowedNodes.push(boardNode); + allowedNodes.push(boardNodeAuthorizable.boardNode); } return allowedNodes; - }, []); + }, []) as Card[]; return allowedCards; } diff --git a/package-lock.json b/package-lock.json index 52c01244aa7..d2cbd72d353 100644 --- a/package-lock.json +++ b/package-lock.json @@ -118,7 +118,7 @@ "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "pdfmake": "^0.2.9", - "prom-client": "^13.1.0", + "prom-client": "^15.1.3", "qs": "^6.9.7", "read-chunk": "^3.0.0", "reflect-metadata": "^0.1.13", @@ -5407,6 +5407,14 @@ "node": ">=8" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@panva/asn1.js": { "version": "1.0.0", "license": "MIT", @@ -19281,13 +19289,15 @@ } }, "node_modules/prom-client": { - "version": "13.2.0", - "license": "Apache-2.0", + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz", + "integrity": "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==", "dependencies": { + "@opentelemetry/api": "^1.4.0", "tdigest": "^0.1.1" }, "engines": { - "node": ">=10" + "node": "^16 || ^18 || >=20" } }, "node_modules/promise-breaker": { diff --git a/package.json b/package.json index c064843247d..2cad05a7150 100644 --- a/package.json +++ b/package.json @@ -234,7 +234,7 @@ "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "pdfmake": "^0.2.9", - "prom-client": "^13.1.0", + "prom-client": "^15.1.3", "qs": "^6.9.7", "read-chunk": "^3.0.0", "reflect-metadata": "^0.1.13",