Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ workflows:
only:
- develop
- pm-1127_1
- pm-4288_2
- pm-4203

# Production builds are exectuted only on tagged commits to the
# master branch.
Expand Down
4 changes: 3 additions & 1 deletion sql/reports/topcoder/completed-profiles-count.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
WITH member_skills AS (
SELECT
us.user_id,
COUNT(*) AS skill_count
COUNT(*) AS skill_count,
ARRAY_AGG(DISTINCT us.skill_id::uuid) AS skill_ids
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 ARRAY_AGG(DISTINCT us.skill_id::uuid) can potentially lead to performance issues if the dataset is large, as it requires sorting to remove duplicates. Consider evaluating the performance impact and whether this aggregation is necessary for the query's purpose.

FROM skills.user_skill us
GROUP BY us.user_id
HAVING COUNT(*) >= 3
Expand All @@ -16,6 +17,7 @@ WHERE m.description IS NOT 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.

[⚠️ performance]
The condition ms.skill_ids && $3::uuid[] uses the array overlap operator, which can be inefficient for large arrays. Ensure that the skill_ids array is indexed appropriately to optimize this operation.

AND (
$2::boolean IS NULL
OR m."availableForGigs" = $2::boolean
Expand Down
4 changes: 3 additions & 1 deletion sql/reports/topcoder/completed-profiles.sql
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
WITH member_skills AS (
SELECT
us.user_id,
COUNT(*) AS skill_count
COUNT(*) AS skill_count,
ARRAY_AGG(DISTINCT us.skill_id::uuid) AS skill_ids
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 ARRAY_AGG(DISTINCT us.skill_id::uuid) can potentially lead to performance issues if the dataset is large, as it requires deduplication and aggregation. Consider evaluating the performance impact and if necessary, optimize by ensuring indexes on skill_id or using alternative methods for deduplication.

FROM skills.user_skill us
GROUP BY us.user_id
HAVING COUNT(*) >= 3 -- Filter early to reduce dataset
Expand Down Expand Up @@ -59,6 +60,7 @@ WHERE m.description IS NOT NULL
AND m."photoURL" <> ''
AND m."homeCountryCode" IS NOT NULL
AND ($1::text IS NULL OR COALESCE(m."homeCountryCode", m."competitionCountryCode") = $1)
AND ($5::uuid[] IS NULL OR ms.skill_ids && $5::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 condition ms.skill_ids && $5::uuid[] checks for any overlap between the arrays, which is correct for filtering by skills. However, ensure that $5 is always provided as an array, even if empty, to avoid unexpected behavior. Consider adding validation or default handling for this parameter.

AND (
$2::boolean IS NULL
OR m."availableForGigs" = $2::boolean
Expand Down
18 changes: 18 additions & 0 deletions src/reports/topcoder/dto/completed-profiles.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,24 @@ export class CompletedProfilesQueryDto {
@IsBoolean()
openToWork?: boolean;

@ApiPropertyOptional({
name: "skillId",
description: "Filter by member skill IDs",
type: String,
isArray: true,
example: ["4b0f5f0a-1234-5678-9abc-def012345678"],
})
@IsOptional()
@Transform(({ value }) => {
if (value === undefined || value === null) {
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.

[💡 readability]
The transformation logic for skillId could be improved by explicitly checking for empty strings after trimming. Currently, it filters out empty strings, but it might be clearer to explicitly handle cases where v.trim() results in an empty string. Consider using a more explicit condition to improve readability.

})
@IsString({ each: true })
skillId?: string[];

@ApiPropertyOptional({
description: "Page number (1-based)",
example: "1",
Expand Down
10 changes: 9 additions & 1 deletion src/reports/topcoder/topcoder-reports.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,15 +266,23 @@ export class TopcoderReportsController {
summary: "List of members with 100% completed profiles",
})
getCompletedProfiles(@Query() query: CompletedProfilesQueryDto) {
const { countryCode, page, perPage, openToWork } = query;
const { countryCode, page, perPage, openToWork, skillId } = query;
const parsedPage = Math.max(Number(page || 1), 1);
const parsedPerPage = Math.min(Math.max(Number(perPage || 50), 1), 200);

const rawSkillIds = Array.isArray(skillId)
? skillId
: skillId !== undefined && skillId !== null
? [skillId]
: [];
const skillIds = rawSkillIds.filter((id) => id && id.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.

[⚠️ correctness]
The filtering of skillIds using id && id.trim().length > 0 assumes that id is a string. If skillId can be a non-string type (e.g., number), this could lead to unexpected behavior. Consider ensuring skillId is always a string before this operation or adjusting the logic to handle different types appropriately.


return this.reports.getCompletedProfiles(
countryCode,
parsedPage,
parsedPerPage,
openToWork,
skillIds.length > 0 ? skillIds : undefined,
);
}
}
6 changes: 6 additions & 0 deletions src/reports/topcoder/topcoder-reports.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -660,13 +660,17 @@ export class TopcoderReportsService {
page = 1,
perPage = 50,
openToWork?: boolean,
skillIds?: string[],
) {
const safePage = Number.isFinite(page) ? Math.max(Math.floor(page), 1) : 1;
const safePerPage = Number.isFinite(perPage)
? Math.min(Math.max(Math.floor(perPage), 1), 200)
: 50;
const offset = (safePage - 1) * safePerPage;

const hasSkillIds = Array.isArray(skillIds) && skillIds.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.

[💡 maintainability]
The variable hasSkillIds is used only once to determine the value of skillIdsParam. Consider inlining this variable to simplify the code.

const skillIdsParam = hasSkillIds ? skillIds : 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.

[❗❗ security]
The skillIdsParam is set to null if skillIds is not an array or is empty. Ensure that the SQL query handles null values for this parameter correctly to avoid potential SQL injection or logic errors.


const countQuery = this.sql.load(
"reports/topcoder/completed-profiles-count.sql",
);
Expand All @@ -675,6 +679,7 @@ export class TopcoderReportsService {
[
countryCode || null,
typeof openToWork === "boolean" ? openToWork : null,
skillIdsParam,
],
);
const total = Number(countRows?.[0]?.total ?? 0);
Expand All @@ -685,6 +690,7 @@ export class TopcoderReportsService {
typeof openToWork === "boolean" ? openToWork : null,
safePerPage,
offset,
skillIdsParam,
]);

const data = rows.map((row) => ({
Expand Down
Loading