Skip to content

PM-1504 create edit scorecard #20

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 20, 2025

Conversation

vas3a
Copy link
Collaborator

@vas3a vas3a commented Aug 19, 2025

https://topcoder.atlassian.net/browse/PM-1504 - Create/Edit Scorecard UI

  • added implementation for add & update scorecard
  • added validation for the scorecard request DTOs

NOTE: I used new ValidationPipe in the controller method, because I needed to pass whitelist: true to strip off any non-wanted props.
By default, the global pipe is using whitelist: false, and since we're pretty far in the development process with the api, I didn't want to turn it on globally (though it would be better).

data: {
...(mapScorecardRequestForCreate({
...body,
createdBy: user.isMachine ? 'System' : (user.userId as string),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using a type guard or type assertion to ensure user.userId is of type string before casting, as casting without checking can lead to runtime errors if userId is not a string.

...(mapScorecardRequestForCreate({
...body,
createdBy: user.isMachine ? 'System' : (user.userId as string),
updatedBy: user.isMachine ? 'System' : (user.userId as string),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the previous comment, ensure user.userId is of type string before casting to avoid potential runtime errors.

@@ -44,7 +53,7 @@ export class ScoreCardService {
},
});

return data as ScorecardWithGroupResponseDto;
return data as unknown as ScorecardWithGroupResponseDto;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Casting data as unknown before casting to ScorecardWithGroupResponseDto is generally unnecessary unless there is a specific reason to bypass TypeScript's type checking. Consider directly casting data to ScorecardWithGroupResponseDto if possible, or provide a rationale for this approach in the documentation or code comments.

): Promise<ScorecardWithGroupResponseDto> {
const data = await this.prisma.scorecard
.update({
where: { id },
data: mapScorecardRequestToDto(body),
data: mapScorecardRequestToDto({

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type assertion as any is used here. Consider refining the types to avoid using any, which can lead to runtime errors and makes the code less type-safe. Ensure that mapScorecardRequestToDto returns the correct type.

// });
async cloneScorecard(
id: string,
user: { userId?: string; isMachine: boolean },

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user parameter is destructured to access userId and isMachine, but userId is marked as optional. Consider handling the case where userId might be undefined to avoid potential runtime errors.

})),
},
}),
) as any;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The as any type assertion is used here. Consider refining the types to avoid using any, which can lead to less type safety and potential runtime errors.

guidelines: string;

@ApiProperty({ description: 'The weight of the question', example: 10 })
@IsNumber()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a validation decorator like @IsPositive() or @Min(0) to ensure that the weight property is a positive number, as negative weights might not make sense in this context.

requiresUpload: boolean;

@ApiProperty({
description: 'Minimum scale value (if applicable)',
example: 0,
required: false,
})
@IsOptional()
@IsNumber()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider specifying the @IsNumber() decorator options to ensure scaleMin is validated correctly. For example, you can use { allowNaN: false, allowInfinity: false } to prevent invalid number values.

scaleMin?: number;

@ApiProperty({
description: 'Maximum scale value (if applicable)',
example: 9,
required: false,
})
@IsOptional()
@IsNumber()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding validation options to @IsNumber() to specify constraints like allowNaN or allowInfinity if applicable, to ensure the scaleMax value is strictly a valid number.

@@ -79,16 +109,24 @@ export class ScorecardQuestionResponseDto extends ScorecardQuestionBaseDto {
}

export class ScorecardSectionBaseDto {
@ApiProperty({ description: 'The id of the section', example: 'abc' })
@IsOptional()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using @IsUUID() if the id is expected to be a UUID. This will ensure the id is validated as a UUID string.

name: string;

@ApiProperty({ description: 'The weight of the section', example: 20 })
@IsNumber()
weight: number;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add the @IsOptional() decorator to weight if it is not a required field. Otherwise, consider adding @IsNotEmpty() to ensure it is not left empty.

weight: number;

@ApiProperty({ description: 'Sort order of the section', example: 1 })
@IsNumber()
sortOrder: number;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add the @IsOptional() decorator to sortOrder if it is not a required field. Otherwise, consider adding @IsNotEmpty() to ensure it is not left empty.

weight: number;

@ApiProperty({ description: 'Sort order of the section', example: 1 })
@IsNumber()
sortOrder: number;

questions: any[];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider specifying the type of elements in the questions array for better type safety. For example, questions: QuestionDto[]; if QuestionDto is the expected type.

@ApiProperty({ description: 'The id of the group', example: 'abc' })
@IsOptional()
@IsString()
id: string;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a validation decorator such as @IsUUID() if the id is expected to be a UUID. This will ensure that the id field adheres to a specific format.

name: string;

@ApiProperty({ description: 'The weight of the group', example: 30 })
@IsNumber()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a validation decorator such as @IsInt() if the weight is expected to be an integer. This will ensure that the weight field is validated as an integer value.

weight: number;

@ApiProperty({ description: 'Sort order of the group', example: 1 })
@IsNumber()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a validation decorator such as @IsInt() if the sortOrder is expected to be an integer. This will ensure that the sortOrder field is validated as an integer value.

weight: number;

@ApiProperty({ description: 'Sort order of the group', example: 1 })
@IsNumber()
sortOrder: number;

sections: any[];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider specifying the type of elements in the sections array for better type safety and clarity. For example, if sections is an array of objects of a specific type, you can define it as sections: SpecificType[];.

version: string;

@ApiProperty({ description: 'The minimum score', example: 0 })
@IsNumber()
@Min(0)
@IsSmallerThan('maxScore')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decorator @IsSmallerThan('maxScore') is not a standard class-validator decorator. Consider using @Max() with a dynamic value or implementing a custom validation logic.

minScore: number;

@ApiProperty({ description: 'The maximum score', example: 100 })
@IsNumber()
@IsGreaterThan('minScore')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decorator @IsGreaterThan('minScore') is not a standard class-validator decorator. Consider using @Min() with a dynamic value or implementing a custom validation logic.

description: 'The creation timestamp',
example: '2023-10-01T00:00:00Z',
})
/**

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment suggests that createdAt, createdBy, updatedAt, and updatedBy should not be editable via API, but they are still part of the DTO. Consider removing these properties from the DTO if they should not be exposed or editable.

@@ -218,6 +265,9 @@ export class ScorecardRequestDto extends ScorecardBaseWithGroupsDto {
description: 'The list of groups associated with the scorecard',
type: [ScorecardGroupRequestDto],
})
@IsArray()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a validation decorator such as @ArrayNotEmpty() to ensure that the scorecardGroups array is not empty, if this is a requirement for the scorecard.

const userFields = {
createdBy: request.createdBy,
...(request.createdBy ? { createdBy: request.createdBy } : {}),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of the spread operator with a conditional expression is correct, but it might be clearer to explicitly state the intention. Consider adding a brief explanation in the documentation or elsewhere in the codebase to clarify why createdBy is conditionally included.

@@ -302,3 +352,76 @@ export function mapScorecardRequestToDto(request: ScorecardRequestDto) {
},
};
}

export function mapScorecardRequestToDto(request: ScorecardRequestDto) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function mapScorecardRequestToDto is defined twice in the same file, which can lead to confusion and potential errors. Consider removing the duplicate definition or renaming one of the functions if they serve different purposes.

...userFields,
scorecardGroups: {
upsert: request.scorecardGroups.map((group) => ({
where: { id: (group as any).id as string },

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using as any for type casting can lead to runtime errors and makes the code harder to maintain. Consider defining proper TypeScript interfaces or types for group, section, and question to ensure type safety.

updatedBy: request.updatedBy,
questions: {
upsert: section.questions.map((question) => ({
where: { id: (question as any).id as string },

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using as any for type casting can lead to runtime errors and makes the code harder to maintain. Consider defining proper TypeScript interfaces or types for question to ensure type safety.

constraints: [relatedPropertyName, comparatorFn],
options: validationOptions,
validator: {
validate(value: Date, args: ValidationArguments) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using a more specific type for value instead of Date, as the comparatorFn seems to handle numbers. This could prevent potential runtime errors if the wrong type is passed.

string,
ComparatorFn,
];
const relatedValue = (args.object as any)[relatedPropertyName];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using (args.object as any) can lead to potential type safety issues. Consider defining an interface for args.object to ensure type safety and improve code readability.

];
const relatedValue = (args.object as any)[relatedPropertyName];

if (typeof comparatorFn !== 'function') {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check for typeof comparatorFn !== 'function' should ideally be done before calling registerDecorator to avoid registering an invalid decorator.

Copy link
Contributor

@kkartunov kkartunov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good

@kkartunov kkartunov merged commit ce08f33 into feat/scorecards Aug 20, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants