Skip to content

[PROD RELEASE] - AI Review/Updates & Fixes#101

Merged
kkartunov merged 44 commits into
masterfrom
develop
Mar 26, 2026
Merged

[PROD RELEASE] - AI Review/Updates & Fixes#101
kkartunov merged 44 commits into
masterfrom
develop

Conversation

@kkartunov

Copy link
Copy Markdown
Contributor

No description provided.

jmgasper and others added 30 commits February 25, 2026 21:52
fix(PM-4085): hide biography section when description is absent
fix(PM-4133):  Removed the availableForGigs for others
fix(PM-4202): Skills section completion logic update
fix(PM-4202): check for additional skills
Comment thread src/common/prisma.js
const createMmConfigClient = (dbUrl) => {
const Pool = getMmPoolConstructor()
const pool = new Pool({
connectionString: dbUrl

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 the connection pool creation to handle potential connection errors gracefully.

Comment thread src/common/prisma.js
throw new Error('MM config lookup requires at least one selected field')
}

const result = await pool.query(

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 use of template literals for SQL queries can lead to SQL injection vulnerabilities if not handled carefully. Ensure that all inputs are properly sanitized.

Comment thread src/common/prisma.js
WHERE "challengeId" = $1
LIMIT 1
`,
[String(challengeId)]

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 using parameterized queries to prevent SQL injection attacks. The current implementation directly interpolates values into the SQL string, which can be risky.

resolvedTrackName: getUnifiedTrackName(row.trackName || row.trackId),
resolvedTypeName: getUnifiedTypeName(row.typeName || row.typeId)
}))
.filter(row => _.includes(['DEVELOP', 'DESIGN', 'DATA_SCIENCE', 'COPILOT'], row.resolvedTrackName))

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 filter condition for validRows in buildUnifiedStatsResponse now includes 'COPILOT'. However, the subsequent logic does not handle 'COPILOT' in the same way as other track names. Ensure that 'COPILOT' is handled appropriately in the response construction.

resolvedTrackName: getUnifiedTrackName(row.trackName || row.trackId),
resolvedTypeName: getUnifiedTypeName(row.typeName || row.typeId)
}))
.filter(row => _.includes(['DEVELOP', 'DESIGN', 'DATA_SCIENCE'], row.resolvedTrackName))

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 filter condition for validRows in buildUnifiedStatsHistoryResponse excludes 'COPILOT', which is inconsistent with buildUnifiedStatsResponse. Verify if 'COPILOT' should be included or excluded in both functions for consistency.

subTrackItem.rank = rank
}
subTrackItem.rank = rank
item.DEVELOP.subTracks.push(subTrackItem)

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 assignment subTrackItem.rank = rank is unconditional, even if rank is an empty object. Consider omitting the rank property if it is empty to avoid unnecessary data in the response.

newSchoolRank: h.newSchoolRank
}, _.isNil)
})
})

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 history array in buildUnifiedStatsHistoryResponse is constructed with _.omitBy, which may include empty objects if all properties are omitted. Consider filtering out empty objects to ensure cleaner data.


// Biography Section
const biography = member.description || (basicInfo && basicInfo.shortBio)
const biography = member.description

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 removal of the fallback (basicInfo && basicInfo.shortBio) for the biography variable might lead to missing biography information if member.description is not available. Consider retaining the fallback to ensure the biography section is populated when possible.

const challenges = stat.challenges ?? 0
const valueText = `${wins} ${wins === 1 ? 'win' : 'wins'}, ${submissions} ${submissions === 1 ? 'submission' : 'submissions'}, ${challenges} ${challenges === 1 ? 'challenge' : 'challenges'}`
const isCompetitiveProgramming = stat.trackName === 'Competitive Programming'
const rating = stat.rating == null ? 0 : stat.rating

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 == null for checking stat.rating, stat.wins, stat.competitions, stat.submissions, and stat.challenges could lead to unintended behavior if these values are undefined. Consider using === null || stat.rating === undefined to explicitly check for both null and undefined.

Comment thread src/common/reviewDb.js
const config = require('config')
const { Pool } = require('pg')

const reviewDb = config.REVIEW_DB_URL

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 current implementation initializes the Pool only if REVIEW_DB_URL is defined. However, if REVIEW_DB_URL is misconfigured or incorrect, this will silently fail by setting reviewDb to null. Consider adding error handling to log or throw an error if REVIEW_DB_URL is not properly set, to avoid silent failures.

LIMIT 1
`)

const schemaName = result.rows[0] && result.rows[0].schemaName

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]
Accessing result.rows[0] without checking if result.rows is non-empty could lead to a runtime error if the query returns no rows. Consider checking result.rows.length before accessing the first element.


const schemaName = result.rows[0] && result.rows[0].schemaName
if (!schemaName) {
throw new Error('REVIEW_DB_URL does not expose challengeResult. Verify REVIEW_DB_URL points to the review-api database and that review-api-v6 migrations have been deployed.')

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 error message could be more informative by including the actual REVIEW_DB_URL value or additional context to aid debugging.


function addLookupEntry (map, key, value) {
const normalizedKey = normalizeLookupKey(key)
if (!normalizedKey || value === null || value === undefined || map.has(normalizedKey)) {

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 map.has(normalizedKey) prevents overwriting existing entries, which is good for preventing accidental overwrites. However, consider logging or handling cases where a duplicate key is detected to ensure that the system behaves as expected and to aid in debugging.

try {
return await cachedLookupPromise
} catch (error) {
cachedLookupPromise = 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.

[⚠️ maintainability]
Consider logging the error before rethrowing it. This can help with debugging by providing more context about what went wrong when the promise fails.

* @param {Object} res the response
*/
async function refreshMemberStats (req, res) {
const result = await service.refreshMemberStats(req.authUser, req.params.handle, req.body)

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 the service.refreshMemberStats call to ensure that any errors are properly caught and handled, preventing potential unhandled promise rejections.

* @param {Object} res the response
*/
async function rerateMemberStats (req, res) {
const result = await service.rerateMemberStats(req.authUser, req.params.handle, req.body)

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 the service.rerateMemberStats call to ensure that any errors are properly caught and handled, preventing potential unhandled promise rejections.

return value
}

if (typeof global.BigInt !== 'function') {

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]
Using global.BigInt directly may not be necessary. Consider using BigInt directly unless there's a specific reason to access it via global. This could improve readability and maintainability.

async function rerateDevTrack (membersClient, challengeClient, reviewDbClient, userId, fromChallengeId) {
if (!reviewDbClient) {
throw new Error('REVIEW_DB_URL must be configured to rerate development stats')
}

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 error message 'REVIEW_DB_URL must be configured to rerate development stats' suggests a missing configuration. Ensure that this configuration is validated earlier in the application lifecycle to prevent runtime errors.


let startIndex = 0
if (fromChallengeId) {
startIndex = targetHistory.findIndex((entry) => entry.challengeId === String(fromChallengeId))

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 assumes fromChallengeId is always valid if provided. Consider validating fromChallengeId before using it to find the start index to prevent potential errors.

Comment thread src/ratings/developRatingEngine.js Outdated
for (let index = startIndex; index < targetHistory.length; index += 1) {
const historyEntry = targetHistory[index]
const participantRows = (await fetchParticipantsForChallenge(reviewDbClient, historyEntry.challengeId))
.filter((row) => parseBooleanLike(row.rated) !== false)

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 participantRows using parseBooleanLike(row.rated) !== false could lead to unexpected results if row.rated is undefined. Consider explicitly checking for true to ensure clarity.

ratingsUpdated += 1
recomputedMaxRating = Math.max(recomputedMaxRating, updatedTarget.rating)

await membersClient.$transaction(async (tx) => {

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 transaction logic is correct, but consider adding error handling within the transaction to manage potential failures gracefully.

* memberStatsHistory, and memberMaxRating.
*/

'use strict'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[💡 style]
The use of 'use strict' is not consistent with the rest of the codebase which uses double quotes for strings. Consider using double quotes for consistency.

return value
}

if (typeof global.BigInt !== 'function') {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[💡 style]
Using global.BigInt is unnecessary here. You can directly use BigInt as it is a global object in Node.js environments.

* @returns {Date|null} normalized Date or null when parsing fails
*/
function normalizeDate (value, fallbackValue) {
const date = value ? new Date(value) : new Date(fallbackValue)

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 normalizeDate function creates a new Date object even if the value is falsy and fallbackValue is also falsy, which will result in Invalid Date. Consider checking if both value and fallbackValue are falsy before creating a Date object.

}

const challengeTimestamp = challengeEventDate.getTime()
let seedIndex = -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 variable seedIndex is initialized to -1 and updated in the loop, but it is not clear if it should remain -1 if no valid index is found. Ensure that this behavior is intentional.

}

if (
scoringConfig &&

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 scoringConfig.relativeScoringEnabled === false is redundant since relativeScoringEnabled is already set to true by default if config is not found. Consider simplifying this condition.

}

const scoreDate = normalizeDate(row.reviewedDate, row.createdAt)
const eventDate = normalizeDate(challenge.endDate, scoreDate)

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 normalizeDate function is called twice with the same parameters for eventDate and endDate. Consider storing the result in a variable to avoid redundant function calls.

throw new Error('MM_DB_URL must be configured to rerate marathon match stats')
}

const normalizedUserId = toBigIntUserId(userId)

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 toBigIntUserId function is called on userId which might already be a BigInt. Consider checking the type before conversion to avoid unnecessary operations.

tx,
normalizedUserId,
historyEntry.challengeId,
targetStateBeforeRun.numRatings > 0 ? targetStateBeforeRun.rating : 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.

[💡 readability]
The ternary operation targetStateBeforeRun.numRatings > 0 ? targetStateBeforeRun.rating : null could be simplified by ensuring targetStateBeforeRun.rating is always a number, possibly by initializing it to 0 in createDefaultState.

}

function getWeight (rating, timesPlayed) {
let weight = 1 / (1 - ((INITIAL_WEIGHT - FINAL_WEIGHT) / (timesPlayed + 1) + FINAL_WEIGHT)) - 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 calculation of weight could potentially result in a division by zero if timesPlayed is a large negative number. Consider adding a check to ensure timesPlayed is non-negative before performing this calculation.

return
}

const actualRankMap = buildActualRankMap(comparisonParticipants)

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 map(createPreparedParticipant) call on comparisonParticipants is unnecessary since comparisonParticipants is already a list of participants. This could lead to redundant processing and should be optimized.

}

function getRatingColor (rating) {
const numericRating = Number(rating)

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 rating to a number using Number(rating) could lead to unexpected results if rating is not a valid number. Consider validating rating before conversion to ensure it is a valid number.

Comment thread src/routes.js
DELETE_USER_SCOPE
} = require('config')
const constants = require('../app-constants')
const { STATS_REFRESH, STATS_RERATE } = MEMBERS

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 destructuring of STATS_REFRESH and STATS_RERATE from MEMBERS assumes these constants are always defined. Consider adding validation to ensure these constants exist to prevent potential runtime errors.

Comment thread src/routes.js
controller: 'StatisticsController',
method: 'refreshMemberStats',
auth: 'jwt',
scopes: [STATS_REFRESH, MEMBERS.ALL],

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 scopes array includes STATS_REFRESH and MEMBERS.ALL. Ensure that STATS_REFRESH is correctly defined and intended for use here, as incorrect scope definitions can lead to unauthorized access.

Comment thread src/routes.js
controller: 'StatisticsController',
method: 'rerateMemberStats',
auth: 'jwt',
scopes: [STATS_RERATE, MEMBERS.ALL],

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 scopes array includes STATS_RERATE and MEMBERS.ALL. Verify that STATS_RERATE is correctly defined and intended for use here to prevent potential security issues.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR advances the unified stats migration by adding admin/M2M write endpoints (refresh + rerate), introducing reusable dimension/rating helpers, and expanding migration verification and PDF reporting to incorporate unified competitive programming ratings.

Changes:

  • Added stats dimension normalization/lookup helpers plus shared review DB relation resolver and client.
  • Implemented Qubits-based rerating engines for Development (Challenge) and Data Science (Marathon Match), and exposed new /members/:handle/stats/refresh + /members/:handle/stats/rerate routes/controllers with new scopes.
  • Expanded migration verification tooling/docs (scripts + runbooks), updated PDF activity stats to treat DATA_SCIENCE as “Competitive Programming”, and added extensive unit tests.

Reviewed changes

Copilot reviewed 28 out of 30 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/common/statsDimensionHelper.js Canonicalizes track/type labels and builds cached lookups between UUIDs, labels, abbreviations, and legacy ids.
src/common/reviewDbHelper.js / src/common/reviewDb.js Adds cached resolution of challengeResult relation and shared review DB pool.
src/ratings/qubitsAlgorithm.js Implements Qubits rating math and rating color bands for rerate engines.
src/ratings/developRatingEngine.js / src/ratings/mmRatingEngine.js Adds rerate engines that replay challenge history and persist stats/history/max rating updates.
src/routes.js / src/controllers/StatisticsController.js / config/default.js Exposes refresh/rerate endpoints with new M2M scopes and required env vars.
src/scripts/verifyStatsMigration.js Enhances migration parity checks (ratings, history counts/order, rank completeness, mostRecent sanity).
src/services/MemberService.js / src/common/profileTemplate.js Updates PDF stats display (Competitive Programming) and merges unified stats ratings into PDF output.
docs/swagger.yaml Documents new endpoints and clarifies unified stats/history fallback behavior.
test/unit/* Adds unit coverage for dimension helpers, refresh/rerate behaviors, rerate engines, and recalc script helpers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/ratings/developRatingEngine.js Outdated
* @returns {boolean} true when the participant should be included in rerating
*/
function isParticipantEligibleForRating (row) {
const passedReview = parseBooleanLike(row && row.passedReview)

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 parseBooleanLike here assumes that row.passedReview can be a string or boolean. Ensure that row.passedReview is consistently typed to avoid unexpected behavior.

return true
}

const placement = Number(row && row.placement)

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 conversion of row.placement to a number and subsequent check for integer status could be simplified by directly checking if row.placement is a positive integer. Consider using Number.isInteger(row.placement) && row.placement > 0 for clarity.

* @returns {boolean} true when the participant should be included in rerating
*/
function isParticipantEligibleForRating (row) {
const finalScore = Number(row && row.finalScore)

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 row.finalScore to a Number without checking if row is defined could lead to unexpected results if row is null or undefined. Consider adding a check to ensure row is defined before accessing row.finalScore.

}

const placement = Number(row && row.placement)
return Number.isInteger(placement) && placement > 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 conversion of row.placement to a Number without checking if row is defined could lead to unexpected results if row is null or undefined. Consider adding a check to ensure row is defined before accessing row.placement.

should.equal(statsRow.volatility, expectedTarget.volatility)
should.equal(historyRow.oldRating, null)
should.equal(historyRow.newRating, expectedTarget.rating)
})

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 check to ensure statsRow and historyRow are not undefined before accessing their properties. This will prevent potential runtime errors if the expected rows are not found.

should.equal(historyRow.newRating === historyRow.oldRating, false)
should.equal(historyRow.newRating < historyRow.oldRating, true)
should.equal(statsRow.rating, historyRow.newRating)
})

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]
Ensure that statsRow and historyRow are not undefined before accessing their properties. This will prevent potential runtime errors if the expected rows are not found.

}

const numericValue = helper.bigIntToNumber(value)
return Number.isFinite(Number(numericValue)) ? Number(numericValue) : 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 conversion of numericValue to a Number is done twice. Consider storing the result of Number(numericValue) in a variable to avoid redundant conversion.

return value.getTime()
}

const timestamp = new Date(value).getTime()

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 conversion of value to a Date object is done twice. Consider storing the Date object in a variable to avoid redundant conversion.

* @returns {Object|null} normalized max rating candidate for API responses
*/
function resolveCurrentMaxRating (maxRating, statsRows) {
if (!Array.isArray(statsRows) || statsRows.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 function resolveCurrentMaxRating assumes that statsRows is an array of objects with specific properties. If statsRows can be undefined or contain unexpected structures, consider adding validation or error handling to ensure robustness.

member.maxRating = _.omit(member.maxRating,
['id', 'userId', ...auditFields])
}
const statsRows = _.isArray(member.memberStats) ? member.memberStats : 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.

[💡 style]
The variable statsRows is assigned undefined if member.memberStats is not an array. Consider using null instead of undefined for consistency with other parts of the code.

}
if (member.maxRating) {
item.maxRating = _.pick(member.maxRating, ['rating', 'track', 'subTrack', 'ratingColor'])
const maxRating = buildCurrentMaxRatingResponse(member.maxRating, validRows)

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 function buildCurrentMaxRatingResponse is called with validRows as the second argument. Ensure that validRows contains the necessary fields expected by resolveCurrentMaxRating to avoid potential runtime errors.

}
if (_.includes(selectFields, 'maxRating')) {
prismaFilter.include.maxRating = true
prismaFilter.include.memberStats = {

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]
Including memberStats when maxRating is selected could potentially lead to performance issues if memberStats contains a large amount of data. Consider evaluating if this inclusion is necessary or if it can be optimized.

const pageMembers = await prisma.member.findMany({
where: { userId: { in: pageUserIds } },
include: { maxRating: true, addresses: true }
include: {

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 addition of memberStats with a select clause in the include statement could potentially increase the complexity and size of the query result. Ensure that prismaHelper.currentMaxRatingStatsSelect is optimized to select only necessary fields to avoid performance degradation.

maxRating: true,
addresses: true
addresses: true,
memberStats: {

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 inclusion of memberStats with a select clause in the include statement may lead to larger query results. Verify that prismaHelper.currentMaxRatingStatsSelect is selecting only essential fields to maintain query performance.

ratingColor: true
}
},
memberStats: {

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]
Adding memberStats with a select clause in the include statement can increase the data size returned by the query. Ensure that prismaHelper.currentMaxRatingStatsSelect is optimized to select only necessary fields to prevent performance issues.

firstName: member.firstName || '',
lastName: member.lastName || '',
maxRating: member.maxRating ? _.pick(member.maxRating, ['rating', 'track', 'subTrack', 'ratingColor']) : null
maxRating: prismaHelper.buildCurrentMaxRatingResponse(member.maxRating, member.memberStats)

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 function prismaHelper.buildCurrentMaxRatingResponse now takes memberStats as an additional parameter. Ensure that this function handles cases where memberStats might be null or undefined to avoid potential runtime errors.


prismaHelper.convertMember(member)

member.maxRating.should.deep.equal({

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 assumes that convertMember will modify member.maxRating.ratingColor to '#9D9FA0', but this value is hardcoded in the test without explanation. Ensure that the logic in convertMember correctly derives this value, or clarify why this specific color is expected.

ratingColor: '#9D9FA0'
})
should.equal(member.userId, 100000218)
should.equal(member.createdAt, new Date('2026-03-26T00:00:00.000Z').getTime())

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 comparison of member.createdAt and member.updatedAt using getTime() could lead to false negatives if the convertMember function modifies these properties to a different Date object with the same timestamp. Consider comparing the timestamps directly or ensuring the test accounts for object identity if relevant.

)

should.equal(response.length, 1)
response[0].maxRating.should.deep.equal({

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 expects maxRating.ratingColor to be '#9D9FA0', but the mock data returns '#FCD617' for maxRating.ratingColor. Ensure the mock data aligns with the expected test outcome to avoid false positives.

]
}

throw new Error(`Unexpected query: ${query}`)

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]
Throwing a generic Error with a message may not provide enough context for debugging. Consider using a custom error type or including more details in the error message to aid in troubleshooting.

deleteMany: (args) => ({ action: 'deleteMany', args }),
upsert: (args) => ({ action: 'upsert', args })
},
$transaction: async (queries) => {

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]
Ensure that the $transaction method correctly handles rollback scenarios in case of an error during the transaction. This is crucial for maintaining data integrity.

}
})
transactionCalls[0][1].action.should.equal('upsert')
transactionCalls[0][1].args.where.userId.should.equal(global.BigInt(123))

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 upsert operation uses a hardcoded ratingColor. Consider deriving this value dynamically based on the rating to ensure consistency and avoid potential mismatches.

if (options.fetchStub) {
global.fetch = options.fetchStub
} else {
delete global.fetch

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]
Deleting global.fetch when options.fetchStub is not provided could lead to unexpected behavior if other parts of the application rely on fetch. Consider setting it to a no-op function instead.

if (typeof originalFetch === 'undefined') {
delete global.fetch
} else {
global.fetch = originalFetch

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]
Restoring global.fetch to originalFetch is correct, but ensure that originalFetch is always defined. If global.fetch was initially undefined, this could introduce unexpected behavior.

Comment on lines +116 to +119
return String(value)
.replace(/&quot;/g, '"')
.replace(/&#39;/g, '\'')
.replace(/&amp;/g, '&')

Check failure

Code scanning / CodeQL

Double escaping or unescaping High

This replacement may produce '&' characters that are double-unescaped
here
.

Copilot Autofix

AI 3 months ago

In general, to avoid double unescaping when decoding HTML entities, you must unescape the escape character (&) last. That way, substrings like &amp;quot; remain as-is until after all other entity patterns (&quot;, &#39;, etc.) have been processed and will not accidentally be turned into a fresh entity that is then decoded again.

The best minimal fix here is to change the order of .replace calls in decodeBasicHtmlEntities so that &amp; is handled after all the other entities. Functionality remains the same for correctly encoded strings (they still end up decoded to the same characters), but inputs like &amp;quot; will now decode to &quot; instead of ", avoiding double-unescape. Concretely, inside src/services/StatisticsService.js, in the decodeBasicHtmlEntities function (lines 111–124), move .replace(/&amp;/g, '&') to come after the .replace calls for &lt;, &gt;, &nbsp;, &quot;, and &#39;. No new methods or imports are needed.

Suggested changeset 1
src/services/StatisticsService.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/services/StatisticsService.js b/src/services/StatisticsService.js
--- a/src/services/StatisticsService.js
+++ b/src/services/StatisticsService.js
@@ -116,10 +116,10 @@
   return String(value)
     .replace(/&quot;/g, '"')
     .replace(/&#39;/g, '\'')
-    .replace(/&amp;/g, '&')
     .replace(/&lt;/g, '<')
     .replace(/&gt;/g, '>')
     .replace(/&nbsp;/g, ' ')
+    .replace(/&amp;/g, '&')
     .trim()
 }
 
EOF
@@ -116,10 +116,10 @@
return String(value)
.replace(/&quot;/g, '"')
.replace(/&#39;/g, '\'')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.trim()
}

Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
@kkartunov kkartunov merged commit d9476c5 into master Mar 26, 2026
6 of 7 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.

6 participants