From d510f57db86f28b69516119860942d12c14982ab Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 20 Aug 2025 17:30:29 +0200 Subject: [PATCH 1/6] fix: qa feedbacks on search scorecards API --- src/api/scorecard/scorecard.controller.ts | 38 +++---------- src/api/scorecard/scorecard.service.ts | 16 +++--- src/dto/scorecard.dto.ts | 68 +++++++++++++++++++---- 3 files changed, 73 insertions(+), 49 deletions(-) diff --git a/src/api/scorecard/scorecard.controller.ts b/src/api/scorecard/scorecard.controller.ts index c547cb0..d6b7d21 100644 --- a/src/api/scorecard/scorecard.controller.ts +++ b/src/api/scorecard/scorecard.controller.ts @@ -28,6 +28,7 @@ import { ScorecardRequestDto, ScorecardResponseDto, ScorecardWithGroupResponseDto, + SearchScorecardQuery, } from 'src/dto/scorecard.dto'; import { ChallengeTrack } from 'src/shared/enums/challengeTrack.enum'; import { ScoreCardService } from './scorecard.service'; @@ -139,8 +140,6 @@ export class ScorecardController { } @Get() - @Roles(UserRole.Admin) - @Scopes(Scope.ReadScorecard) @ApiOperation({ summary: 'Search scorecards', description: @@ -198,40 +197,19 @@ export class ScorecardController { }) @UseInterceptors(PaginationHeaderInterceptor) async searchScorecards( - @Query('challengeTrack') challengeTrack?: ChallengeTrack | ChallengeTrack[], - @Query('challengeType') challengeType?: string | string[], - @Query('status') status?: $Enums.ScorecardStatus | $Enums.ScorecardStatus[], - @Query('scorecardType') - scorecardType?: $Enums.ScorecardType | $Enums.ScorecardType[], - @Query('name') name?: string, - @Query('page') page: number = 1, - @Query('perPage') perPage: number = 10, + @Query() query: SearchScorecardQuery ): Promise { - const challengeTrackArray = Array.isArray(challengeTrack) - ? challengeTrack - : challengeTrack - ? [challengeTrack] - : []; - const challengeTypeArray = Array.isArray(challengeType) - ? challengeType - : challengeType - ? [challengeType] - : []; - const scorecardTypesArray = Array.isArray(scorecardType) - ? scorecardType - : scorecardType - ? [scorecardType] - : []; - const statusArray = Array.isArray(status) ? status : status ? [status] : []; + const { challengeTrack = [], challengeType = [], status = [], scorecardType = [], name, page, perPage} = query; + const result = await this.scorecardService.getScoreCards({ - challengeTrack: challengeTrackArray, - challengeType: challengeTypeArray, + challengeTrack, + challengeType, name, page, perPage, - scorecardTypesArray, - statusArray, + scorecardType, + status, }); return result; } diff --git a/src/api/scorecard/scorecard.service.ts b/src/api/scorecard/scorecard.service.ts index b7f52a2..a1ed59f 100644 --- a/src/api/scorecard/scorecard.service.ts +++ b/src/api/scorecard/scorecard.service.ts @@ -9,12 +9,12 @@ import { mapScorecardRequestToDto, ScorecardGroupBaseDto, ScorecardPaginatedResponseDto, - ScorecardQueryDto, ScorecardQuestionBaseDto, ScorecardRequestDto, ScorecardResponseDto, ScorecardSectionBaseDto, ScorecardWithGroupResponseDto, + SearchScorecardQuery, } from 'src/dto/scorecard.dto'; import { JwtUser } from 'src/shared/modules/global/jwt.service'; import { PrismaService } from 'src/shared/modules/global/prisma.service'; @@ -165,15 +165,15 @@ export class ScoreCardService { * @returns response dto */ async getScoreCards( - query: ScorecardQueryDto, + query: SearchScorecardQuery, ): Promise { const { page = 1, perPage = 10, challengeTrack, challengeType, - scorecardTypesArray, - statusArray, + scorecardType, + status, name, } = query; const skip = (page - 1) * perPage; @@ -188,14 +188,14 @@ export class ScoreCardService { in: challengeType, }, }), - ...(scorecardTypesArray?.length && { + ...(scorecardType?.length && { type: { - in: scorecardTypesArray, + in: scorecardType, }, }), - ...(statusArray?.length && { + ...(status?.length && { status: { - in: statusArray, + in: status, }, }), ...(name && { name: { contains: name, mode: 'insensitive' } }), diff --git a/src/dto/scorecard.dto.ts b/src/dto/scorecard.dto.ts index e111a04..0eaff4d 100644 --- a/src/dto/scorecard.dto.ts +++ b/src/dto/scorecard.dto.ts @@ -1,10 +1,11 @@ import { ApiProperty } from '@nestjs/swagger'; import { $Enums } from '@prisma/client'; -import { Type } from 'class-transformer'; +import { Transform, Type } from 'class-transformer'; import { IsArray, IsBoolean, IsEnum, + IsInt, IsNotEmpty, IsNumber, IsOptional, @@ -288,6 +289,61 @@ export class ScorecardResponseDto extends ScorecardBaseDto { id: string; } +function toArray(value: unknown): T[] | undefined { + if (value === undefined || value === null || value === '') return undefined; + const asArray = Array.isArray(value) ? value : [value]; + return asArray + .flatMap((v) => String(v).split(',')) + .map((s) => s.trim()) + .filter(Boolean) as T[]; +} + +export class SearchScorecardQuery { + @IsOptional() + @Transform(({ value }) => toArray(value)) + @IsArray() + @IsEnum(ChallengeTrack, { each: true }) + challengeTrack?: ChallengeTrack[]; + + @IsOptional() + @Transform(({ value }) => toArray(value)) + @IsArray() + @IsString({ each: true }) + challengeType?: string[]; + + @IsOptional() + @Transform(({ value }) => toArray<$Enums.ScorecardStatus>(value)) + @IsArray() + @IsEnum($Enums.ScorecardStatus, { + each: true, + message: (args) => + `Invalid value "${args.value}" for "${args.property}".` + }) + status?: $Enums.ScorecardStatus[]; + + @IsOptional() + @Transform(({ value }) => toArray<$Enums.ScorecardType>(value)) + @IsArray() + @IsEnum($Enums.ScorecardType, { each: true }) + scorecardType?: $Enums.ScorecardType[]; + + @IsOptional() + @IsString() + name?: string; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + page: number = 1; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + perPage: number = 10; +} + export class ScorecardWithGroupResponseDto extends ScorecardBaseDto { @ApiProperty({ description: 'The ID of the scorecard', example: 'abc123' }) id: string; @@ -324,16 +380,6 @@ export class ScorecardPaginatedResponseDto { scoreCards: ScorecardResponseDto[]; } -export class ScorecardQueryDto { - challengeTrack?: ChallengeTrack[]; - challengeType?: string[]; - name?: string; - page?: number; - perPage?: number; - statusArray?: $Enums.ScorecardStatus[]; - scorecardTypesArray?: $Enums.ScorecardType[]; -} - export function mapScorecardRequestForCreate(request: ScorecardRequestDto) { const userFields = { ...(request.createdBy ? { createdBy: request.createdBy } : {}), From 8d6f902ae8874a60f6b026f67750c61003a8682c Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 20 Aug 2025 17:31:46 +0200 Subject: [PATCH 2/6] fix: qa feedbacks on search scorecards API --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 5692065..ad0bd53 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -77,6 +77,7 @@ workflows: - feat/ai-workflows - feat/scorecards - pm-1503 + - pm-1585_1 - 'build-prod': context: org-global filters: From a68c813f3df64b1e9dda269e98202c095b7e4811 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 20 Aug 2025 17:43:49 +0200 Subject: [PATCH 3/6] fix: lint --- src/api/scorecard/scorecard.controller.ts | 14 ++++++++++---- src/dto/scorecard.dto.ts | 3 +-- src/shared/decorators/user.decorator.ts | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/api/scorecard/scorecard.controller.ts b/src/api/scorecard/scorecard.controller.ts index d6b7d21..ac68115 100644 --- a/src/api/scorecard/scorecard.controller.ts +++ b/src/api/scorecard/scorecard.controller.ts @@ -33,7 +33,6 @@ import { import { ChallengeTrack } from 'src/shared/enums/challengeTrack.enum'; import { ScoreCardService } from './scorecard.service'; import { PaginationHeaderInterceptor } from 'src/interceptors/PaginationHeaderInterceptor'; -import { $Enums } from '@prisma/client'; import { User } from 'src/shared/decorators/user.decorator'; import { JwtUser } from 'src/shared/modules/global/jwt.service'; @@ -197,10 +196,17 @@ export class ScorecardController { }) @UseInterceptors(PaginationHeaderInterceptor) async searchScorecards( - @Query() query: SearchScorecardQuery + @Query() query: SearchScorecardQuery, ): Promise { - const { challengeTrack = [], challengeType = [], status = [], scorecardType = [], name, page, perPage} = query; - + const { + challengeTrack = [], + challengeType = [], + status = [], + scorecardType = [], + name, + page, + perPage, + } = query; const result = await this.scorecardService.getScoreCards({ challengeTrack, diff --git a/src/dto/scorecard.dto.ts b/src/dto/scorecard.dto.ts index 0eaff4d..28d9992 100644 --- a/src/dto/scorecard.dto.ts +++ b/src/dto/scorecard.dto.ts @@ -316,8 +316,7 @@ export class SearchScorecardQuery { @IsArray() @IsEnum($Enums.ScorecardStatus, { each: true, - message: (args) => - `Invalid value "${args.value}" for "${args.property}".` + message: (args) => `Invalid value "${args.value}" for "${args.property}".`, }) status?: $Enums.ScorecardStatus[]; diff --git a/src/shared/decorators/user.decorator.ts b/src/shared/decorators/user.decorator.ts index 6a78f27..7a622f8 100644 --- a/src/shared/decorators/user.decorator.ts +++ b/src/shared/decorators/user.decorator.ts @@ -5,7 +5,7 @@ export const User = createParamDecorator( (data: string, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest(); const user = request.user; - // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return (data ? user?.[data] : user) as JwtUser; }, ); From 146558cc9784aab5d55274caf43369b9623c5b06 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 20 Aug 2025 18:02:34 +0200 Subject: [PATCH 4/6] fix: lint --- src/api/scorecard/scorecard.controller.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/api/scorecard/scorecard.controller.ts b/src/api/scorecard/scorecard.controller.ts index ac68115..fa8da8c 100644 --- a/src/api/scorecard/scorecard.controller.ts +++ b/src/api/scorecard/scorecard.controller.ts @@ -139,6 +139,8 @@ export class ScorecardController { } @Get() + @Roles(UserRole.Admin) + @Scopes(Scope.ReadScorecard) @ApiOperation({ summary: 'Search scorecards', description: From b77640b42aa84f1628a19c1a2ead23b7c47cfb5c Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 20 Aug 2025 22:49:42 +0200 Subject: [PATCH 5/6] fix: changed error message --- src/api/scorecard/scorecard.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/scorecard/scorecard.service.ts b/src/api/scorecard/scorecard.service.ts index a1ed59f..191630b 100644 --- a/src/api/scorecard/scorecard.service.ts +++ b/src/api/scorecard/scorecard.service.ts @@ -153,7 +153,7 @@ export class ScoreCardService { throw new NotFoundException({ message: `Scorecard not found.` }); } throw new InternalServerErrorException({ - message: `Error: ${error.code}`, + message: `Invalid scorecard id`, }); }); return data as ScorecardWithGroupResponseDto; From 3069047d418510822694c892357f57040c22cc91 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 20 Aug 2025 22:50:02 +0200 Subject: [PATCH 6/6] fix: changed error message --- src/api/scorecard/scorecard.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/scorecard/scorecard.service.ts b/src/api/scorecard/scorecard.service.ts index 191630b..9275912 100644 --- a/src/api/scorecard/scorecard.service.ts +++ b/src/api/scorecard/scorecard.service.ts @@ -153,7 +153,7 @@ export class ScoreCardService { throw new NotFoundException({ message: `Scorecard not found.` }); } throw new InternalServerErrorException({ - message: `Invalid scorecard id`, + message: `Invalid scorecard id - ${id}`, }); }); return data as ScorecardWithGroupResponseDto;