Skip to content

Commit 570bae5

Browse files
committed
Fix terms duplicates (PM-2114), fixes group check during search to show challenges in matching groups to member (PM-2207)
1 parent da4e652 commit 570bae5

File tree

3 files changed

+169
-31
lines changed

3 files changed

+169
-31
lines changed

src/common/helper.js

Lines changed: 101 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -700,12 +700,13 @@ async function getCompleteUserGroupTreeIds(userId) {
700700
},
701701
});
702702

703+
const normalizedGroupIds = normalizeGroupIdsFromResponse(result.data);
703704
logger.debug(
704705
`helper.getCompleteUserGroupTreeIds: response status ${result.status} (groups=${
705-
_.get(result, "data.length", 0) || 0
706+
normalizedGroupIds.length
706707
})`
707708
);
708-
return result.data || [];
709+
return normalizedGroupIds;
709710
} catch (err) {
710711
logger.debug(
711712
`helper.getCompleteUserGroupTreeIds: error for user ${userId} - status ${
@@ -716,6 +717,79 @@ async function getCompleteUserGroupTreeIds(userId) {
716717
}
717718
}
718719

720+
/**
721+
* Normalize group payload into a flat list of group identifier strings.
722+
*
723+
* @param {Array|Object|String|Number} groupPayload raw response from Groups API
724+
* @returns {Array<String>} normalized group identifiers
725+
*/
726+
function normalizeGroupIdsFromResponse(groupPayload) {
727+
const ids = new Set();
728+
729+
const pushId = (value) => {
730+
if (_.isNil(value)) {
731+
return;
732+
}
733+
if (typeof value === "number" || typeof value === "bigint") {
734+
ids.add(value.toString());
735+
return;
736+
}
737+
if (typeof value === "string") {
738+
const trimmed = value.trim();
739+
if (!trimmed) {
740+
return;
741+
}
742+
const lowered = trimmed.toLowerCase();
743+
if (lowered === "null" || lowered === "undefined") {
744+
return;
745+
}
746+
ids.add(trimmed);
747+
}
748+
};
749+
750+
const visit = (payload, depth = 0) => {
751+
if (_.isNil(payload) || depth > 6) {
752+
return;
753+
}
754+
if (Array.isArray(payload)) {
755+
payload.forEach((item) => visit(item, depth + 1));
756+
return;
757+
}
758+
if (_.isPlainObject(payload)) {
759+
const identifierKeys = ["id", "groupId", "oldId", "legacyId", "uuid"];
760+
identifierKeys.forEach((key) => {
761+
if (!_.isNil(payload[key])) {
762+
pushId(payload[key]);
763+
}
764+
});
765+
766+
const nestedKeys = [
767+
"path",
768+
"pathIds",
769+
"pathGroupIds",
770+
"parentGroups",
771+
"ancestors",
772+
"ancestorGroupIds",
773+
"subGroups",
774+
"children",
775+
"groupIds",
776+
"membershipGroupIds",
777+
];
778+
nestedKeys.forEach((key) => {
779+
if (!_.isNil(payload[key])) {
780+
visit(payload[key], depth + 1);
781+
}
782+
});
783+
return;
784+
}
785+
786+
pushId(payload);
787+
};
788+
789+
visit(groupPayload);
790+
return Array.from(ids);
791+
}
792+
719793
/**
720794
* Get all subgroups for the given group ID
721795
* @param {String} groupId the group ID
@@ -938,19 +1012,41 @@ async function getProjectDefaultTerms(projectId) {
9381012
}
9391013
}
9401014

1015+
/**
1016+
* Remove duplicate term entries by "id" and "roleId" combination.
1017+
*
1018+
* @param {Array<Object>} terms list of challenge terms
1019+
* @returns {Array<Object>} unique term definitions
1020+
*/
1021+
function dedupeChallengeTerms(terms = []) {
1022+
if (!Array.isArray(terms)) {
1023+
return [];
1024+
}
1025+
1026+
return _.uniqBy(
1027+
terms.filter((term) => !_.isNil(term)),
1028+
(term) => {
1029+
const idKey = _.toString(term.id).trim();
1030+
const roleKey = _.isNil(term.roleId) ? "" : _.toString(term.roleId).trim();
1031+
return `${idKey}:${roleKey}`;
1032+
}
1033+
);
1034+
}
1035+
9411036
/**
9421037
* This function gets the challenge terms array with the terms data
9431038
* The terms data is retrieved from the terms API using the specified terms ids
9441039
*
9451040
* @param {Array<Object>} terms The array of terms {id, roleId} to retrieve from terms API
9461041
*/
9471042
async function validateChallengeTerms(terms = []) {
948-
if (terms.length === 0) {
1043+
const uniqueTerms = dedupeChallengeTerms(terms);
1044+
if (uniqueTerms.length === 0) {
9491045
return [];
9501046
}
9511047
const listOfTerms = [];
9521048
const token = await m2mHelper.getM2MToken();
953-
for (let term of terms) {
1049+
for (let term of uniqueTerms) {
9541050
// Get the terms details from the API
9551051
try {
9561052
await axios.get(`${config.TERMS_API_URL}/${term.id}`, {
@@ -1374,6 +1470,7 @@ module.exports = {
13741470
createResource,
13751471
getUserGroups,
13761472
ensureNoDuplicateOrNullElements,
1473+
dedupeChallengeTerms,
13771474
postBusEvent,
13781475
calculateChallengeEndDate,
13791476
listChallengesByMember,

src/common/prisma-helper.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const _ = require("lodash");
22
const Decimal = require("decimal.js");
33
const constants = require("../../app-constants");
44
const { PrizeSetTypeEnum } = require("@prisma/client");
5+
const { dedupeChallengeTerms } = require("./helper");
56
/**
67
* Convert phases data to prisma model.
78
*
@@ -346,7 +347,8 @@ function convertModelToResponse(ret) {
346347
delete ret.overviewTotalPrizes;
347348

348349
// convert terms
349-
ret.terms = _.map(ret.terms, (t) => ({ id: t.termId, roleId: t.roleId }));
350+
const serializedTerms = _.map(ret.terms, (t) => ({ id: t.termId, roleId: t.roleId }));
351+
ret.terms = dedupeChallengeTerms(serializedTerms);
350352
// convert skills - basic transformation, enrichment happens in service layer
351353
ret.skills = _.map(ret.skills, (s) => ({ id: s.skillId }));
352354
// convert attachments

src/services/ChallengeService.js

Lines changed: 65 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,33 @@ async function searchChallenges(currentUser, criteria) {
419419

420420
const _hasAdminRole = hasAdminRole(currentUser);
421421

422+
const normalizeGroupIdValue = (value) => {
423+
if (_.isNil(value)) {
424+
return null;
425+
}
426+
const normalized = _.toString(value).trim();
427+
if (!normalized) {
428+
return null;
429+
}
430+
const lowered = normalized.toLowerCase();
431+
if (lowered === "null" || lowered === "undefined") {
432+
return null;
433+
}
434+
return normalized;
435+
};
436+
437+
const normalizeGroupIdList = (list) => {
438+
if (_.isNil(list)) {
439+
return [];
440+
}
441+
const arrayValue = Array.isArray(list) ? list : [list];
442+
return _.uniq(
443+
arrayValue
444+
.map((value) => normalizeGroupIdValue(value))
445+
.filter((value) => !_.isNil(value))
446+
);
447+
};
448+
422449
let includedTrackIds = _.isArray(criteria.trackIds) ? criteria.trackIds : [];
423450
let includedTypeIds = _.isArray(criteria.typeIds) ? criteria.typeIds : [];
424451

@@ -745,52 +772,63 @@ async function searchChallenges(currentUser, criteria) {
745772

746773
let groupsToFilter = [];
747774
let accessibleGroups = [];
775+
let accessibleGroupsSet = new Set();
748776

749777
if (currentUser && !currentUser.isMachine && !_hasAdminRole) {
750-
accessibleGroups = await helper.getCompleteUserGroupTreeIds(currentUser.userId);
778+
const rawAccessibleGroups = await helper.getCompleteUserGroupTreeIds(currentUser.userId);
779+
accessibleGroups = normalizeGroupIdList(rawAccessibleGroups);
780+
accessibleGroupsSet = new Set(accessibleGroups);
751781
}
752782

753783
// Filter all groups from the criteria to make sure the user can access those
754784
if (!_.isUndefined(criteria.group) || !_.isUndefined(criteria.groups)) {
785+
const criteriaGroupsList = _.isNil(criteria.groups) ? [] : [].concat(criteria.groups);
786+
755787
// check group access
756788
if (_.isUndefined(currentUser)) {
757-
if (criteria.group) {
758-
const group = await helper.getGroupById(criteria.group);
789+
const normalizedGroup = normalizeGroupIdValue(criteria.group);
790+
if (normalizedGroup) {
791+
const group = await helper.getGroupById(normalizedGroup);
759792
if (group && !group.privateGroup) {
760-
groupsToFilter.push(criteria.group);
793+
groupsToFilter.push(normalizedGroup);
761794
}
762795
}
763-
if (criteria.groups && criteria.groups.length > 0) {
764-
const promises = [];
765-
_.each(criteria.groups, (g) => {
766-
promises.push(
767-
(async () => {
768-
const group = await helper.getGroupById(g);
769-
if (group && !group.privateGroup) {
770-
groupsToFilter.push(g);
771-
}
772-
})()
773-
);
796+
797+
if (criteriaGroupsList.length > 0) {
798+
const promises = criteriaGroupsList.map(async (groupValue) => {
799+
const normalized = normalizeGroupIdValue(groupValue);
800+
if (!normalized) {
801+
return;
802+
}
803+
const group = await helper.getGroupById(normalized);
804+
if (group && !group.privateGroup) {
805+
groupsToFilter.push(normalized);
806+
}
774807
});
775808
await Promise.all(promises);
776809
}
777810
} else if (!currentUser.isMachine && !_hasAdminRole) {
778-
if (accessibleGroups.includes(criteria.group)) {
779-
groupsToFilter.push(criteria.group);
811+
const normalizedGroup = normalizeGroupIdValue(criteria.group);
812+
if (normalizedGroup && accessibleGroupsSet.has(normalizedGroup)) {
813+
groupsToFilter.push(normalizedGroup);
780814
}
781-
if (criteria.groups && criteria.groups.length > 0) {
782-
_.each(criteria.groups, (g) => {
783-
if (accessibleGroups.includes(g)) {
784-
groupsToFilter.push(g);
815+
816+
if (criteriaGroupsList.length > 0) {
817+
criteriaGroupsList.forEach((groupValue) => {
818+
const normalized = normalizeGroupIdValue(groupValue);
819+
if (normalized && accessibleGroupsSet.has(normalized)) {
820+
groupsToFilter.push(normalized);
785821
}
786822
});
787823
}
788824
} else {
789-
groupsToFilter = [...(criteria.groups ? criteria.groups : [])];
790-
if (criteria.group) {
791-
groupsToFilter.push(criteria.group);
825+
groupsToFilter = normalizeGroupIdList(criteriaGroupsList);
826+
const normalizedGroup = normalizeGroupIdValue(criteria.group);
827+
if (normalizedGroup) {
828+
groupsToFilter.push(normalizedGroup);
792829
}
793830
}
831+
794832
groupsToFilter = _.uniq(groupsToFilter);
795833

796834
if (groupsToFilter.length === 0) {
@@ -2261,7 +2299,7 @@ async function updateChallenge(currentUser, challengeId, data) {
22612299
}
22622300

22632301
if (!_.isUndefined(data.terms)) {
2264-
await helper.validateChallengeTerms(data.terms);
2302+
data.terms = await helper.validateChallengeTerms(data.terms);
22652303
}
22662304

22672305
if (data.phases && data.phases.length > 0) {
@@ -2784,7 +2822,8 @@ function sanitizeChallenge(challenge) {
27842822
}));
27852823
}
27862824
if (challenge.terms) {
2787-
sanitized.terms = _.map(challenge.terms, (term) => _.pick(term, ["id", "roleId"]));
2825+
const uniqueTerms = helper.dedupeChallengeTerms(challenge.terms || []);
2826+
sanitized.terms = _.map(uniqueTerms, (term) => _.pick(term, ["id", "roleId"]));
27882827
}
27892828
if (challenge.attachments) {
27902829
sanitized.attachments = _.map(challenge.attachments, (attachment) =>

0 commit comments

Comments
 (0)