Skip to content

Prod deploy for new reports portal#65

Merged
jmgasper merged 60 commits intomasterfrom
develop
Mar 25, 2026
Merged

Prod deploy for new reports portal#65
jmgasper merged 60 commits intomasterfrom
develop

Conversation

@jmgasper
Copy link
Copy Markdown
Collaborator

No description provided.

jmgasper and others added 30 commits February 27, 2026 08:00
…les-report

PM-4158 completed profiles report
…les-report

PM-4158 - Update profiles report
…les-report

PM-4158fix user role for completed profile report
… into PM-4198_customer-portal-completed-profiles-report-updates
WITH challenge_context AS (
SELECT
c.id,
(ct.name = 'Marathon Match') AS is_marathon_match
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[💡 readability]
Using a boolean expression (ct.name = 'Marathon Match') directly in the SELECT clause can be unclear. Consider using a CASE statement for better readability and maintainability.

SELECT rs."aggregateScore"
FROM reviews."reviewSummation" AS rs
WHERE rs."submissionId" = s.id
AND COALESCE(rs."isFinal", TRUE) = TRUE
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
The use of COALESCE(rs."isFinal", TRUE) = TRUE assumes that isFinal can be NULL and defaults it to TRUE. Ensure this logic is intentional and aligns with business rules.

END AS "finalRank"
FROM submitter_members AS sm
LEFT JOIN identity."user" AS u
ON sm."memberId" ~ '^[0-9]+$'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ maintainability]
The regular expression sm."memberId" ~ '^[0-9]+$' is used multiple times. Consider extracting this logic into a common CTE or function to improve maintainability.

ON sm."memberId" ~ '^[0-9]+$'
AND mem."userId" = sm."memberId"::bigint
LEFT JOIN lookups."Country" AS home_code
ON UPPER(home_code."countryCode") = UPPER(mem."homeCountryCode")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ performance]
Using UPPER() for case-insensitive comparison can lead to performance issues if the column is not indexed accordingly. Consider ensuring the column is indexed with a case-insensitive collation or using a more efficient method.

final_review."aggregateScore" AS final_score_raw,
(
passing_review.is_passing IS TRUE
OR COALESCE(s."finalScore"::double precision, 0) > 98
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ maintainability]
The condition COALESCE(s."finalScore"::double precision, 0) > 98 assumes a threshold of 98 for a valid submission. Ensure this threshold is appropriate and consistent with business rules, as hardcoding such values can lead to maintenance challenges if the criteria change.

FROM submission_metrics
WHERE is_valid_submission = TRUE
),
valid_submitter_members AS MATERIALIZED (
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ performance]
Consider whether the use of MATERIALIZED for the valid_submitter_members CTE is necessary. While it can improve performance by caching the result, it may also increase memory usage. Evaluate the trade-offs based on the expected size of the data.

END AS "finalRank"
FROM valid_submitter_members AS vsm
LEFT JOIN identity."user" AS u
ON vsm."memberId" ~ '^[0-9]+$'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ maintainability]
The regular expression vsm."memberId" ~ '^[0-9]+$' is used multiple times to check if memberId is numeric. Consider extracting this logic into a separate CTE or function to avoid repetition and improve maintainability.

LEFT JOIN mm_ranked_scores AS mrs
ON mrs."memberId" = vsm."memberId"
ORDER BY
CASE
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
The ORDER BY clause uses multiple CASE statements with NULLS LAST. Ensure that the ordering logic aligns with the intended business requirements, as complex ordering can lead to unexpected results if not thoroughly validated.

WITH challenge_context AS (
SELECT
c.id,
(ct.name = 'Marathon Match') AS is_marathon_match
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
The expression (ct.name = 'Marathon Match') AS is_marathon_match relies on a string comparison to determine the challenge type. Consider using a more robust method, such as a type ID or enum, to avoid potential issues with string mismatches or typos.

JOIN reviews."submission" AS s
ON s."challengeId" = cc.id
AND s."memberId" IS NOT NULL
LEFT JOIN LATERAL (
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ performance]
Using LEFT JOIN LATERAL with ORDER BY COALESCE(rs."reviewedDate", rs."createdAt") DESC NULLS LAST, rs.id DESC could potentially lead to performance issues if the reviewSummation table is large and not properly indexed on these columns. Ensure that appropriate indexes are in place to optimize this query.

END AS "finalRank"
FROM winner_members AS wm
LEFT JOIN identity."user" AS u
ON wm."memberId" ~ '^[0-9]+$'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[💡 maintainability]
The condition wm."memberId" ~ '^[0-9]+$' is used multiple times to check if memberId is numeric. Consider extracting this logic into a common expression or function to improve maintainability and reduce repetition.

LEFT JOIN members."member" AS mem
ON wm."memberId" ~ '^[0-9]+$'
AND mem."userId" = wm."memberId"::bigint
LEFT JOIN lookups."Country" AS home_code
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ performance]
The use of UPPER() for case-insensitive comparison in joins with the Country table could lead to performance degradation if the table is large. Ensure that the countryCode and id columns are indexed appropriately to support case-insensitive lookups.

LEFT JOIN groups."User" gu
ON gu.id = gm."memberId"
JOIN identity."user" u
ON u.user_id::text = (
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ performance]
The use of u.user_id::text in the join condition could lead to performance issues due to type casting on every row. Consider ensuring that gm."memberId" and gu."universalUID" are stored as the same type as u.user_id to avoid casting.

LEFT JOIN identity.email e
ON e.user_id = u.user_id
AND e.primary_ind = 1
WHERE ($1::text IS NOT NULL OR $2::text IS NOT NULL)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ performance]
The condition ($1::text IS NOT NULL OR $2::text IS NOT NULL) in the WHERE clause could lead to a full table scan if both parameters are NULL. Consider adding an index or restructuring the query to handle cases where both parameters are NULL more efficiently.

)
AND (
$2::text IS NULL
OR LOWER(g.name) = LOWER($2::text)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ performance]
Using LOWER() on both sides of the comparison in the WHERE clause can be inefficient, especially if the table is large. Consider creating a case-insensitive index on g.name and g.description to improve query performance.

) AS "country"
FROM input_handles AS ih
LEFT JOIN identity."user" AS u
ON LOWER(u.handle) = LOWER(ih.handle_input)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ performance]
The use of LOWER for case-insensitive comparison can lead to performance issues if the handle column is not indexed with a case-insensitive index. Consider adding an appropriate index to improve query performance.

SELECT e.address
FROM identity.email AS e
WHERE e.user_id = u.user_id
AND NULLIF(BTRIM(e.address), '') IS NOT NULL
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
The condition NULLIF(BTRIM(e.address), '') IS NOT NULL is used to filter out empty email addresses. Ensure that this logic aligns with the business requirements, as it may exclude valid but empty email addresses.

FROM identity.role r
JOIN identity.role_assignment ra
ON r.id = ra.role_id
JOIN identity."user" u
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[💡 readability]
Using double quotes for the table name "user" is necessary due to it being a reserved keyword in SQL. However, consider renaming the table to avoid the need for quoting, which can improve readability and reduce potential for errors.

LEFT JOIN identity.email e
ON e.user_id = u.user_id
AND e.primary_ind = 1
WHERE ($1::int IS NOT NULL OR $2::text IS NOT NULL)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ performance]
The condition ($1::int IS NOT NULL OR $2::text IS NOT NULL) in the WHERE clause could lead to a full table scan if both parameters are NULL. Consider adding an index on the role_id and name columns to improve query performance.

WHERE ($1::int IS NOT NULL OR $2::text IS NOT NULL)
AND ra.subject_type = 1
AND ($1::int IS NULL OR r.id = $1::int)
AND ($2::text IS NULL OR LOWER(r.name) = LOWER($2::text));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ performance]
Using LOWER(r.name) = LOWER($2::text) can prevent the use of an index on r.name if one exists. Consider using a case-insensitive collation or a functional index to optimize this condition.

WITH challenge_context AS (
SELECT
c.id,
(LOWER(ct.name) = LOWER('Marathon Match')) AS is_marathon_match
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ performance]
Using LOWER(ct.name) = LOWER('Marathon Match') for case-insensitive comparison is functional but can be inefficient if the dataset is large. Consider using ILIKE for case-insensitive comparisons if supported by your SQL dialect.

FROM challenges."Challenge" AS c
JOIN challenges."ChallengeType" AS ct
ON ct.id = c."typeId"
WHERE c.id = $1::text
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[❗❗ security]
Ensure that $1::text is properly sanitized and validated to prevent SQL injection vulnerabilities, especially if this value comes from user input.

SELECT
st.user_id,
COALESCE(
jsonb_agg(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ performance]
The use of jsonb_agg with a FILTER clause is correct, but be aware that this could lead to performance issues if the dataset is large. Consider evaluating the performance impact if this query is expected to handle a significant amount of data.

SELECT
st.user_id,
COALESCE(
NULLIF(TRIM(mem.handle), ''),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ performance]
The use of NULLIF(TRIM(mem.handle), '') is a good practice to handle empty strings, but ensure that mem.handle is indexed if this query is expected to run on large datasets to avoid performance degradation.

AND res."memberId" = st.user_id::text
) AS handle_fallback ON TRUE
LEFT JOIN lookups."Country" AS home_code
ON UPPER(home_code."countryCode") = UPPER(mem."homeCountryCode")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ performance]
Using UPPER for case-insensitive joins can lead to performance issues if the countryCode fields are not indexed. Consider adding functional indexes if this query is expected to run frequently on large datasets.

AND m."photoURL" IS NOT NULL
AND m."photoURL" <> ''
AND m."homeCountryCode" IS NOT NULL
AND ($1::text IS NULL OR COALESCE(m."homeCountryCode", m."competitionCountryCode") = $1)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
Using COALESCE with m."homeCountryCode" and m."competitionCountryCode" assumes that if homeCountryCode is NULL, competitionCountryCode will be used. Ensure this logic aligns with the intended behavior, as it might lead to unexpected results if both fields can be NULL.

AND m."photoURL" <> ''
AND m."homeCountryCode" IS NOT NULL
AND ($1::text IS NULL OR COALESCE(m."homeCountryCode", m."competitionCountryCode") = $1)
AND ($3::uuid[] IS NULL OR ms.skill_ids @> $3::uuid[])
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
The use of the @> operator assumes that ms.skill_ids contains all elements of $3::uuid[]. Ensure that this behavior is intended, as it might exclude members with additional skills not in the filter.

AND mtp.key = 'openToWork'
AND mtp.value IS NOT NULL
AND (
NOT (mtp.value::jsonb ? 'availability')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
The condition NOT (mtp.value::jsonb ? 'availability') might lead to unexpected behavior if availability is a required field in some contexts. Double-check the logic to ensure it aligns with the business requirements.

SELECT
mtp.value AS open_to_work_value,
(
mtp.value::jsonb ? 'availability'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
The use of btrim(mtp.value->>'availability') <> '' could be problematic if availability is not a string or is missing. Consider using a safer check to ensure availability is a string and exists before trimming.

AND m."photoURL" IS NOT NULL
AND m."photoURL" <> ''
AND m."homeCountryCode" IS NOT NULL
AND ($1::text IS NULL OR COALESCE(m."homeCountryCode", m."competitionCountryCode") = $1)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
The use of COALESCE(m."homeCountryCode", m."competitionCountryCode") assumes that if homeCountryCode is null, competitionCountryCode will be a valid fallback. Ensure that this logic aligns with business requirements, as it may lead to unexpected results if both are null.

-- Check engagement availability exists
AND ot.open_to_work_value IS NOT NULL
AND (
NOT (ot.open_to_work_value::jsonb ? 'availability')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[❗❗ correctness]
The logic here checks for the presence of availability and preferredRoles in open_to_work_value. If open_to_work_value is not guaranteed to be a JSON object, this could lead to runtime errors. Consider adding a type check or validation step.

) AS max_rating ON TRUE
LEFT JOIN LATERAL (
SELECT mmar.*
SELECT COUNT(*) AS competitions
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ performance]
The use of COUNT(*) without a specific column can lead to performance issues if the table is large. Consider counting a specific indexed column instead, such as msh."userId", to potentially improve performance.

AND ms."typeId" = 'MARATHON_MATCH'
ORDER BY
CASE WHEN ms."isPrivate" THEN 1 ELSE 0 END,
ms."isPrivate" ASC,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
The change from CASE WHEN ms."isPrivate" THEN 1 ELSE 0 END to ms."isPrivate" ASC is correct but ensure that the column ms."isPrivate" is of a boolean type. If it's not, this could lead to unexpected ordering behavior.

};

export const UserRoles = {
export const AdminRoles = {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ maintainability]
Renaming UserRoles to AdminRoles could lead to confusion since UserRoles is still used elsewhere in the code. Consider ensuring that the naming is consistent and clear to avoid misunderstandings about the role definitions.

UserRoles.TalentManager,
] as const;

export const ScopeRoleAccess: Record<string, readonly string[]> = {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
The ScopeRoleAccess object uses string keys derived from Scopes. If any scope key changes, it could lead to runtime errors. Consider using TypeScript enums or constants to ensure compile-time safety and reduce the risk of typos.

if (this.hasRequiredScope(scopes, requiredScopes)) {
return true;
}
if (hasAccessToScopes(authUser, requiredScopes)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
The refactoring to use hasAccessToScopes simplifies the logic, but ensure that this utility function correctly handles both machine and user roles, as the previous implementation had distinct logic for each.

authUser,
}),
}),
} as unknown as ExecutionContext;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ maintainability]
Casting the return value to ExecutionContext using as unknown as ExecutionContext could hide potential type mismatches. Consider using a more explicit type assertion or ensuring the object structure matches ExecutionContext to improve type safety.

SCOPES_KEY,
[context.getHandler(), context.getClass()],
) ?? [Scopes.TopcoderReports, Scopes.AllReports];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ security]
The default scopes [Scopes.TopcoderReports, Scopes.AllReports] are used if this.reflector.getAllAndOverride returns null or undefined. Ensure this default behavior is intended, as it might grant broader access than expected if the decorator is missing or misconfigured.

private isAdmin(roles: string[]): boolean {
return roles.some((role) =>
TopcoderReportsGuard.adminRoles.has(role?.toLowerCase()),
return getNormalizedRoles(authUser).some((role) =>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[❗❗ correctness]
The method getNormalizedRoles is used to check roles against completedProfilesRoles. Ensure that getNormalizedRoles correctly normalizes roles to match the format used in completedProfilesRoles. Any mismatch could lead to incorrect access control.


export type AuthUserLike = {
isMachine?: boolean;
roles?: string[] | string;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ design]
The roles and role properties in AuthUserLike are both optional and can be either a string or an array of strings. This could lead to confusion or errors if both are provided with different values. Consider consolidating these into a single property or ensuring consistent usage across the codebase.

* Scope comparisons are case-insensitive.
*/
export function hasRequiredScope(
scopes: readonly string[] | string | undefined,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
The hasRequiredScope function does not handle cases where scopes is undefined or an empty string effectively, as it will return false early. Consider adding a check to handle these cases explicitly to avoid unexpected behavior.

* Returns true when the caller has one of the configured administrator roles.
*/
export function hasAdminRole(
roles: readonly string[] | string | undefined,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
The hasAdminRole function assumes that roles will always be defined. Consider adding a check for undefined or empty roles to prevent potential runtime errors.

* Returns true when any required scope is mapped to one of the caller's roles.
*/
export function hasRequiredRoleAccess(
roles: readonly string[] | string | undefined,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
In hasRequiredRoleAccess, the roles parameter is not checked for undefined or empty values before normalization. This could lead to unexpected results if roles is not provided. Consider adding a guard clause to handle these cases.

status: 200,
description: "Export successful.",
})
async getRegisteredUsers(@Param() params: ChallengeUsersPathParamDto) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ security]
Consider adding validation to ensure challengeId is a valid format before passing it to the service. This can prevent potential errors or security issues if invalid data is passed.

status: 200,
description: "Export successful.",
})
async getSubmitters(@Param() params: ChallengeUsersPathParamDto) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ security]
Consider adding validation to ensure challengeId is a valid format before passing it to the service. This can prevent potential errors or security issues if invalid data is passed.

status: 200,
description: "Export successful.",
})
async getValidSubmitters(@Param() params: ChallengeUsersPathParamDto) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ security]
Consider adding validation to ensure challengeId is a valid format before passing it to the service. This can prevent potential errors or security issues if invalid data is passed.

status: 200,
description: "Export successful.",
})
async getWinners(@Param() params: ChallengeUsersPathParamDto) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ security]
Consider adding validation to ensure challengeId is a valid format before passing it to the service. This can prevent potential errors or security issues if invalid data is passed.


return results;
} catch (e) {
this.logger.error(e);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[💡 maintainability]
Consider adding more context to the error log message to help with debugging. Currently, only the error object is logged, which might not provide enough information about the context in which the error occurred.


return this.formatChallengeUserReport(results);
} catch (e) {
this.logger.error(e);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[💡 maintainability]
Consider adding more context to the error log message to help with debugging. Currently, only the error object is logged, which might not provide enough information about the context in which the error occurred.


return this.formatChallengeUserReport(results);
} catch (e) {
this.logger.error(e);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[💡 maintainability]
Consider adding more context to the error log message to help with debugging. Currently, only the error object is logged, which might not provide enough information about the context in which the error occurred.


return this.formatChallengeUserReport(results);
} catch (e) {
this.logger.error(e);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[💡 maintainability]
Consider adding more context to the error log message to help with debugging. Currently, only the error object is logged, which might not provide enough information about the context in which the error occurred.

* latest submission and finalRank by current effective score, breaking ties by
* earlier submission time.
*/
export interface ChallengeUserRecordDto {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[💡 maintainability]
Consider using readonly for properties in ChallengeUserRecordDto if they are not meant to be modified after creation. This can help prevent accidental mutations and improve the immutability of the data structure.

export class UsersByRoleQueryDto {
@ApiProperty({ required: false })
@IsOptional()
@IsInt()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
Consider adding a validation to ensure roleId is a positive integer if applicable, as negative IDs might not be valid in your domain.

* Shared identity user payload returned by identity report endpoints.
*/
export interface IdentityUserDto {
userId: number | null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
The userId field is defined as number | null. Ensure that the rest of the application logic correctly handles the null case to avoid potential runtime errors.

*/
export interface IdentityUserDto {
userId: number | null;
handle: string;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
Consider adding validation to ensure that handle is not an empty string, as this might be an invalid state for a user handle.

export interface IdentityUserDto {
userId: number | null;
handle: string;
email: string | null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
The email field is defined as string | null. Ensure that the rest of the application logic correctly handles the null case to avoid potential runtime errors.

userId: number | null;
handle: string;
email: string | null;
country: string | null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
The country field is defined as string | null. Ensure that the rest of the application logic correctly handles the null case to avoid potential runtime errors.

*/
@Post("/users-by-handles")
@UseGuards(PermissionsGuard)
@UseInterceptors(FileInterceptor("file"))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[❗❗ security]
Using FileInterceptor with @UploadedFile() suggests that file uploads are expected. Ensure that the file size and type are validated to prevent potential security risks such as large file uploads or malicious file types.

@ApiResponse({ status: 200, description: "Export successful." })
async getUsersByHandles(
@Body(
new ValidationPipe({
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
The ValidationPipe is configured with skipMissingProperties: true, which means missing properties in the DTO will not trigger validation errors. Ensure this behavior is intended, as it might allow incomplete data to pass through.


return results;
} catch (e) {
this.logger.error(e);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[💡 maintainability]
Consider providing more context in the error logging to help with debugging. For example, include the filters used in the query.


return results;
} catch (e) {
this.logger.error(e);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[💡 maintainability]
Consider providing more context in the error logging to help with debugging. For example, include the filters used in the query.

country: alpha3ToCountryName(row.country) ?? row.country,
}));
} catch (e) {
this.logger.error(e);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[💡 maintainability]
Consider providing more context in the error logging to help with debugging. For example, include the handles being queried.

*/
private parseHandlesFromFile(file: UploadedHandlesFile): string[] {
const extension = extname(file.originalname ?? "").toLowerCase();
if (!SUPPORTED_HANDLES_UPLOAD_EXTENSIONS.has(extension)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
The extname function may return an empty string if the file has no extension. Consider handling this case explicitly to improve error messaging.

});

it("returns an empty directory when no JWT user is present", () => {
expect(getAccessibleReportsDirectory()).toEqual({});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
The test case for when no JWT user is present does not provide any input to getAccessibleReportsDirectory. If the function relies on certain properties being present, this could lead to unexpected behavior. Consider explicitly passing an empty object or a default parameter to ensure clarity and prevent potential issues.

};

export type ReportsDirectory = Record<ReportGroupKey, ReportGroup>;
export type ReportsDirectory = Partial<Record<ReportGroupKey, ReportGroup>>;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
Changing ReportsDirectory to a Partial<Record<ReportGroupKey, ReportGroup>> means that some report groups might be missing. Ensure that this change is intentional and that the rest of the codebase can handle potentially missing groups.


type RegisteredReportsDirectory = Record<ReportGroupKey, RegisteredReportGroup>;

const report = (
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
The report function defaults the method to "GET". Ensure that this is the intended behavior for all reports using this function, as it might lead to incorrect HTTP method usage if not explicitly specified.

requiredScopes,
});

const postReport = (
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
The postReport function defaults the method to "POST". Ensure that this is the intended behavior for all reports using this function, as it might lead to incorrect HTTP method usage if not explicitly specified.

location: "query",
};

const handlesBodyParam: ReportParameter = {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
The handlesBodyParam is marked as required: true and location: "body". Ensure that the API endpoints using this parameter are correctly handling body parameters, as this differs from typical query or path parameters.

location: "query",
};

const REGISTERED_REPORTS_DIRECTORY: RegisteredReportsDirectory = {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ maintainability]
The REGISTERED_REPORTS_DIRECTORY now includes new report groups such as member and admin. Ensure that these additions are correctly integrated with the rest of the system, particularly in terms of access control and data handling.

),
);

export const REPORTS_DIRECTORY: ReportsDirectory = Object.fromEntries(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
The REPORTS_DIRECTORY is now constructed using Object.fromEntries. Ensure that all necessary report groups are included in REGISTERED_REPORTS_DIRECTORY, as missing entries will result in missing report groups in REPORTS_DIRECTORY.

* Returns the subset of the report catalog that the authenticated caller can run.
* Empty groups are omitted from the response.
*/
export function getAccessibleReportsDirectory(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
The getAccessibleReportsDirectory function uses flatMap to filter accessible reports. Ensure that this logic correctly handles cases where a user has partial access to reports within a group, as it may affect the returned structure.

@Get()
@UseGuards(PermissionsGuard)
@Scopes(AppScopes.AllReports)
@Scopes(AppScopes.AllReports, ...REPORTS_DIRECTORY_REQUIRED_SCOPES)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[❗❗ security]
The addition of ...REPORTS_DIRECTORY_REQUIRED_SCOPES to the @Scopes decorator increases the complexity of the permissions required. Ensure that this change aligns with the intended access control policy and that all necessary scopes are correctly defined in REPORTS_DIRECTORY_REQUIRED_SCOPES.

@Get("/directory")
@UseGuards(PermissionsGuard)
@Scopes(AppScopes.AllReports)
@Scopes(AppScopes.AllReports, ...REPORTS_DIRECTORY_REQUIRED_SCOPES)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[❗❗ security]
Similar to the change on line 18, verify that the inclusion of ...REPORTS_DIRECTORY_REQUIRED_SCOPES in the @Scopes decorator is consistent with the desired security model and that it does not inadvertently restrict or expand access beyond what is intended.

example: true,
})
@IsOptional()
@Transform(({ value }) =>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
The transformation logic for openToWork could be improved for clarity. Consider using value === 'true' directly, as value === true will not match if the input is a string, which is likely the case when dealing with query parameters.

return undefined;
}
const values = Array.isArray(value) ? value : [value];
return values.map((v) => String(v)).filter((v) => v.trim().length > 0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[💡 performance]
The transformation logic for skillId could be optimized by using filter(Boolean) instead of filter((v) => v.trim().length > 0), which is more concise and achieves the same result.

@UseGuards(TopcoderReportsGuard)
@UseInterceptors(CsvResponseInterceptor)
@Controller("/topcoder")
@RequiredScopes(AppScopes.AllReports, AppScopes.TopcoderReports)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ security]
The @RequiredScopes decorator is applied to the entire controller, which might be too broad if different endpoints require different scopes. Consider applying it to individual methods if scope requirements vary.

}

@Get("/member-payment-accrual")
@Get("/admin/member-payment-accrual")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ design]
The route /admin/member-payment-accrual is inconsistent with the other routes prefixed with /topcoder. Ensure this is intentional and aligns with your API design.

@ApiOperation({
summary: "List of members with 100% completed profiles",
})
getCompletedProfiles(@Query() query: CompletedProfilesQueryDto) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
The getCompletedProfiles method parses page and perPage parameters to ensure they are within a valid range. However, it does not handle cases where page or perPage might be non-numeric strings. Consider adding validation to ensure these parameters are numeric.

email: row.email ?? null,
country: row.country ?? null,
place: this.toNullableNumber(row.place),
provisionalScores: this.toNullableNumberArray(row.provisionalScores),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
The toNullableNumberArray function is used to convert provisionalScores to a number array. However, the type of provisionalScores is unknown, which could lead to unexpected behavior if the input is not a JSON string or an array. Consider adding validation or type-checking to ensure the input is in the expected format.

skillIdsParam,
],
);
const total = Number(countRows?.[0]?.total ?? 0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
The total variable is derived from countRows?.[0]?.total. If countRows is empty or total is not a valid number, this could lead to incorrect pagination. Consider adding a check to ensure total is a valid number.

]);

const data = rows.map((row) => ({
userId: row.userId ? Number(row.userId) : null,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
The conversion of userId to a number assumes that row.userId is a valid number or a string representation of a number. If row.userId is not a valid number, this could lead to unexpected results. Consider adding validation to ensure userId is a valid number before conversion.

countryCode: row.countryCode || undefined,
countryName: row.countryName || undefined,
city: row.city || undefined,
skillCount:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
The conversion of skillCount to a number assumes that row.skillCount is a valid number or a string representation of a number. If row.skillCount is not a valid number, this could lead to unexpected results. Consider adding validation to ensure skillCount is a valid number before conversion.

* @param fileName snapshot file name under `data/statistics/mm`
* @returns parsed JSON payload exposed by the statistics endpoints
*/
private loadJson<T = any>(fileName: string): T {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[❗❗ correctness]
Consider adding error handling for fs.readFileSync and JSON.parse to manage potential file read errors or JSON parsing errors. This will improve the robustness of the method.

*/
private loadJson<T = any>(fileName: string): T {
const fullPath = path.join(this.baseDir, fileName);
const raw = fs.readFileSync(fullPath, "utf-8");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[⚠️ correctness]
Consider adding error handling for fs.readFileSync and JSON.parse to manage potential file read errors or JSON parsing errors gracefully. This will improve the robustness of the service.

@jmgasper jmgasper merged commit 4d562a9 into master Mar 25, 2026
9 checks 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.

4 participants