From 9b68f91b128a906c5fe373b0b44c16b677336cf0 Mon Sep 17 00:00:00 2001 From: Rakib Ansary Date: Thu, 23 Mar 2023 20:16:03 +0600 Subject: [PATCH 01/22] fix: merge phase-helper updates --- src/common/challenge-helper.js | 16 ++++++++++++++++ src/common/helper.js | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/common/challenge-helper.js b/src/common/challenge-helper.js index 59e5b232..69d8b7e5 100644 --- a/src/common/challenge-helper.js +++ b/src/common/challenge-helper.js @@ -98,6 +98,22 @@ class ChallengeHelper { // check groups authorization await helper.ensureAccessibleByGroupsAccess(currentUser, challenge); } + + async validateChallengeUpdateRequest(currentUser, challenge, data) { + await helper.ensureUserCanModifyChallenge(currentUser, challenge); + + helper.ensureNoDuplicateOrNullElements(data.tags, "tags"); + helper.ensureNoDuplicateOrNullElements(data.groups, "groups"); + + if (data.projectId) { + await ensureProjectExist(data.projectId, currentUser); + } + + // check groups access to be updated group values + if (data.groups) { + await ensureAcessibilityToModifiedGroups(currentUser, data, challenge); + } + } } module.exports = new ChallengeHelper(); diff --git a/src/common/helper.js b/src/common/helper.js index be969a9d..2b1692cd 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -1123,7 +1123,7 @@ async function ensureUserCanViewChallenge(currentUser, challenge) { * * @param {Object} currentUser the user who perform operation * @param {Object} challenge the challenge to check - * @returns {undefined} + * @returns {Promise} */ async function ensureUserCanModifyChallenge(currentUser, challenge) { // check groups authorization From e062c362a0c597805662872f3ff3a27d134699d7 Mon Sep 17 00:00:00 2001 From: Rakib Ansary Date: Fri, 24 Mar 2023 01:05:42 +0600 Subject: [PATCH 02/22] refactor: challenge update --- src/common/challenge-helper.js | 73 ++++++ src/common/group-helper.js | 36 +++ src/common/helper.js | 5 +- src/common/project-helper.js | 1 + src/services/ChallengeService.js | 436 +++++-------------------------- 5 files changed, 182 insertions(+), 369 deletions(-) create mode 100644 src/common/group-helper.js diff --git a/src/common/challenge-helper.js b/src/common/challenge-helper.js index 69d8b7e5..a05ac7ff 100644 --- a/src/common/challenge-helper.js +++ b/src/common/challenge-helper.js @@ -10,6 +10,7 @@ const constants = require("../../app-constants"); const axios = require("axios"); const { getM2MToken } = require("./m2m-helper"); const { hasAdminRole } = require("./role-helper"); +const { ensureAcessibilityToModifiedGroups } = require("./group-helper"); class ChallengeHelper { /** @@ -113,6 +114,78 @@ class ChallengeHelper { if (data.groups) { await ensureAcessibilityToModifiedGroups(currentUser, data, challenge); } + + // Ensure unchangeable fields are not changed + if ( + _.get(challenge, "legacy.track") && + _.get(data, "legacy.track") && + _.get(challenge, "legacy.track") !== _.get(data, "legacy.track") + ) { + throw new errors.ForbiddenError("Cannot change legacy.track"); + } + + if ( + _.get(challenge, "trackId") && + _.get(data, "trackId") && + _.get(challenge, "trackId") !== _.get(data, "trackId") + ) { + throw new errors.ForbiddenError("Cannot change trackId"); + } + + if ( + _.get(challenge, "typeId") && + _.get(data, "typeId") && + _.get(challenge, "typeId") !== _.get(data, "typeId") + ) { + throw new errors.ForbiddenError("Cannot change typeId"); + } + + if ( + _.get(challenge, "legacy.pureV5Task") && + _.get(data, "legacy.pureV5Task") && + _.get(challenge, "legacy.pureV5Task") !== _.get(data, "legacy.pureV5Task") + ) { + throw new errors.ForbiddenError("Cannot change legacy.pureV5Task"); + } + + if ( + _.get(challenge, "legacy.pureV5") && + _.get(data, "legacy.pureV5") && + _.get(challenge, "legacy.pureV5") !== _.get(data, "legacy.pureV5") + ) { + throw new errors.ForbiddenError("Cannot change legacy.pureV5"); + } + + if ( + _.get(challenge, "legacy.selfService") && + _.get(data, "legacy.selfService") && + _.get(challenge, "legacy.selfService") !== _.get(data, "legacy.selfService") + ) { + throw new errors.ForbiddenError("Cannot change legacy.selfService"); + } + + if ( + (challenge.status === constants.challengeStatuses.Completed || + challenge.status === constants.challengeStatuses.Cancelled) && + data.status && + data.status !== challenge.status && + data.status !== constants.challengeStatuses.CancelledClientRequest + ) { + throw new errors.BadRequestError( + `Cannot change ${challenge.status} challenge status to ${data.status} status` + ); + } + + if ( + data.winners && + data.winners.length > 0 && + challenge.status !== constants.challengeStatuses.Completed && + data.status !== constants.challengeStatuses.Completed + ) { + throw new errors.BadRequestError( + `Cannot set winners for challenge with non-completed ${challenge.status} status` + ); + } } } diff --git a/src/common/group-helper.js b/src/common/group-helper.js new file mode 100644 index 00000000..7ef2911d --- /dev/null +++ b/src/common/group-helper.js @@ -0,0 +1,36 @@ +const _ = require("lodash"); +const errors = require("./errors"); +const helper = require("./helper"); + +const { hasAdminRole } = require("./role-helper"); + +class GroupHelper { + /** + * Ensure the user can access the groups being updated to + * @param {Object} currentUser the user who perform operation + * @param {Object} data the challenge data to be updated + * @param {String} challenge the original challenge data + */ + async ensureAcessibilityToModifiedGroups(currentUser, data, challenge) { + const needToCheckForGroupAccess = !currentUser + ? true + : !currentUser.isMachine && !hasAdminRole(currentUser); + if (!needToCheckForGroupAccess) { + return; + } + const userGroups = await helper.getUserGroups(currentUser.userId); + const userGroupsIds = _.map(userGroups, (group) => group.id); + const updatedGroups = _.difference( + _.union(challenge.groups, data.groups), + _.intersection(challenge.groups, data.groups) + ); + const filtered = updatedGroups.filter((g) => !userGroupsIds.includes(g)); + if (filtered.length > 0) { + throw new errors.ForbiddenError( + "ensureAcessibilityToModifiedGroups :: You don't have access to this group!" + ); + } + } +} + +module.exports = new GroupHelper(); diff --git a/src/common/helper.js b/src/common/helper.js index 2b1692cd..6d672297 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -450,16 +450,19 @@ axiosRetry(axios, { * @param {String} token The token * @returns */ -async function createSelfServiceProject(name, description, type, token) { +async function createSelfServiceProject(name, description, type) { const projectObj = { name, description, type, }; + + const token = await m2mHelper.getM2MToken(); const url = `${config.PROJECTS_API_URL}`; const res = await axios.post(url, projectObj, { headers: { Authorization: `Bearer ${token}` }, }); + return _.get(res, "data.id"); } diff --git a/src/common/project-helper.js b/src/common/project-helper.js index 6a966088..e344267d 100644 --- a/src/common/project-helper.js +++ b/src/common/project-helper.js @@ -5,6 +5,7 @@ const config = require("config"); const HttpStatus = require("http-status-codes"); const m2mHelper = require("./m2m-helper"); const { hasAdminRole } = require("./role-helper"); +const errors = require("./errors"); class ProjectHelper { /** diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index 334ff226..c2854b97 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -36,6 +36,8 @@ const esClient = helper.getESClient(); const { ChallengeDomain } = require("@topcoder-framework/domain-challenge"); const { hasAdminRole } = require("../common/role-helper"); +const { ensureAcessibilityToModifiedGroups } = require("../common/group-helper"); +const { validateChallengeUpdateRequest } = require("../common/challenge-helper"); const challengeDomain = new ChallengeDomain(GRPC_CHALLENGE_SERVER_HOST, GRPC_CHALLENGE_SERVER_PORT); @@ -177,33 +179,6 @@ async function ensureAccessibleByGroupsAccess(currentUser, challenge) { } } -/** - * Ensure the user can access the groups being updated to - * @param {Object} currentUser the user who perform operation - * @param {Object} data the challenge data to be updated - * @param {String} challenge the original challenge data - */ -async function ensureAcessibilityToModifiedGroups(currentUser, data, challenge) { - const needToCheckForGroupAccess = !currentUser - ? true - : !currentUser.isMachine && !hasAdminRole(currentUser); - if (!needToCheckForGroupAccess) { - return; - } - const userGroups = await helper.getUserGroups(currentUser.userId); - const userGroupsIds = _.map(userGroups, (group) => group.id); - const updatedGroups = _.difference( - _.union(challenge.groups, data.groups), - _.intersection(challenge.groups, data.groups) - ); - const filtered = updatedGroups.filter((g) => !userGroupsIds.includes(g)); - if (filtered.length > 0) { - throw new errors.ForbiddenError( - "ensureAcessibilityToModifiedGroups :: You don't have access to this group!" - ); - } -} - /** * Search challenges by legacyId * @param {Object} currentUser the user who perform operation @@ -1069,8 +1044,7 @@ async function createChallenge(currentUser, challenge, userToken) { challenge.projectId = await helper.createSelfServiceProject( selfServiceProjectName, "N/A", - config.NEW_SELF_SERVICE_PROJECT_TYPE, - userToken + config.NEW_SELF_SERVICE_PROJECT_TYPE ); } @@ -1263,7 +1237,6 @@ async function createChallenge(currentUser, challenge, userToken) { return ret; } - createChallenge.schema = { currentUser: Joi.any(), challenge: Joi.object() @@ -1480,7 +1453,6 @@ async function getChallenge(currentUser, id, checkIfExists) { return challenge; } - getChallenge.schema = { currentUser: Joi.any(), id: Joi.id(), @@ -1602,36 +1574,34 @@ async function validateWinners(winners, challengeId) { * @param {Boolean} isFull the flag indicate it is a fully update operation. * @returns {Object} the updated challenge */ -async function update(currentUser, challengeId, data, isFull) { +async function update(currentUser, challengeId, data) { + const challenge = await challengeDomain.lookup(getLookupCriteria("id", challengeId)); + + validateChallengeUpdateRequest(currentUser, challenge, data); + + const projectId = _.get(challenge, "projectId"); const cancelReason = _.cloneDeep(data.cancelReason); delete data.cancelReason; + let dynamicDescription = _.cloneDeep(data.description || challenge.description); let sendActivationEmail = false; let sendSubmittedEmail = false; let sendCompletedEmail = false; let sendRejectedEmail = false; - if (!_.isUndefined(_.get(data, "legacy.reviewType"))) { - _.set(data, "legacy.reviewType", _.toUpper(_.get(data, "legacy.reviewType"))); - } - if (data.projectId) { - await challengeHelper.ensureProjectExist(data.projectId, currentUser); - } - - helper.ensureNoDuplicateOrNullElements(data.tags, "tags"); - helper.ensureNoDuplicateOrNullElements(data.groups, "groups"); - // helper.ensureNoDuplicateOrNullElements(data.gitRepoURLs, 'gitRepoURLs') - - const challenge = await challengeDomain.lookup(getLookupCriteria("id", challengeId)); + const { billingAccountId, markup } = await projectHelper.getProjectBillingInformation(projectId); - let dynamicDescription = _.cloneDeep(data.description || challenge.description); + if (billingAccountId && _.isUndefined(_.get(challenge, "billing.billingAccountId"))) { + _.set(data, "billing.billingAccountId", billingAccountId); + _.set(data, "billing.markup", markup || 0); + } + /* BEGIN self-service stuffs */ if (challenge.legacy.selfService && data.metadata && data.metadata.length > 0) { for (const entry of data.metadata) { const regexp = new RegExp(`{{${entry.name}}}`, "g"); dynamicDescription = dynamicDescription.replace(regexp, entry.value); } - data.description = dynamicDescription; } if ( challenge.legacy.selfService && @@ -1682,27 +1652,19 @@ async function update(currentUser, challengeId, data, isFull) { } } - const { billingAccountId, markup } = await projectHelper.getProjectBillingInformation( - _.get(challenge, "projectId") - ); - if (billingAccountId && _.isUndefined(_.get(challenge, "billing.billingAccountId"))) { - _.set(data, "billing.billingAccountId", billingAccountId); - _.set(data, "billing.markup", markup || 0); - } - if ( - billingAccountId && - _.includes(config.TOPGEAR_BILLING_ACCOUNTS_ID, _.toString(billingAccountId)) - ) { - if (_.isEmpty(data.metadata)) { - data.metadata = []; - } - if (!_.find(data.metadata, (e) => e.name === "postMortemRequired")) { - data.metadata.push({ - name: "postMortemRequired", - value: "false", - }); + if (challenge.legacy.selfService && data.status === constants.challengeStatuses.Draft) { + try { + await helper.updateSelfServiceProjectInfo( + challenge.projectId, + data.endDate || challenge.endDate, + currentUser + ); + } catch (e) { + logger.debug(`There was an error trying to update the project: ${e.message}`); } } + /* END self-service stuffs */ + let isChallengeBeingActivated = false; if (data.status) { if (data.status === constants.challengeStatuses.Active) { @@ -1751,81 +1713,6 @@ async function update(currentUser, challengeId, data, isFull) { } } - // FIXME: Tech Debt - if ( - _.get(challenge, "legacy.track") && - _.get(data, "legacy.track") && - _.get(challenge, "legacy.track") !== _.get(data, "legacy.track") - ) { - throw new errors.ForbiddenError("Cannot change legacy.track"); - } - if ( - _.get(challenge, "trackId") && - _.get(data, "trackId") && - _.get(challenge, "trackId") !== _.get(data, "trackId") - ) { - throw new errors.ForbiddenError("Cannot change trackId"); - } - if ( - _.get(challenge, "typeId") && - _.get(data, "typeId") && - _.get(challenge, "typeId") !== _.get(data, "typeId") - ) { - throw new errors.ForbiddenError("Cannot change typeId"); - } - - if ( - _.get(challenge, "legacy.useSchedulingAPI") && - _.get(data, "legacy.useSchedulingAPI") && - _.get(challenge, "legacy.useSchedulingAPI") !== _.get(data, "legacy.useSchedulingAPI") - ) { - throw new errors.ForbiddenError("Cannot change legacy.useSchedulingAPI"); - } - if ( - _.get(challenge, "legacy.pureV5Task") && - _.get(data, "legacy.pureV5Task") && - _.get(challenge, "legacy.pureV5Task") !== _.get(data, "legacy.pureV5Task") - ) { - throw new errors.ForbiddenError("Cannot change legacy.pureV5Task"); - } - if ( - _.get(challenge, "legacy.pureV5") && - _.get(data, "legacy.pureV5") && - _.get(challenge, "legacy.pureV5") !== _.get(data, "legacy.pureV5") - ) { - throw new errors.ForbiddenError("Cannot change legacy.pureV5"); - } - if ( - _.get(challenge, "legacy.selfService") && - _.get(data, "legacy.selfService") && - _.get(challenge, "legacy.selfService") !== _.get(data, "legacy.selfService") - ) { - throw new errors.ForbiddenError("Cannot change legacy.selfService"); - } - - if (!_.isUndefined(challenge.legacy) && !_.isUndefined(data.legacy)) { - _.extend(challenge.legacy, data.legacy); - } - - if (!_.isUndefined(challenge.billing) && !_.isUndefined(data.billing)) { - _.extend(challenge.billing, data.billing); - } else if (_.isUndefined(challenge.billing) && !_.isUndefined(data.billing)) { - challenge.billing = data.billing; - } - - await helper.ensureUserCanModifyChallenge(currentUser, challenge); - - // check groups access to be updated group values - if (data.groups) { - await ensureAcessibilityToModifiedGroups(currentUser, data, challenge); - } - let newAttachments; - if (isFull || !_.isUndefined(data.attachments)) { - newAttachments = data.attachments; - } - - await ensureAccessibleForChallenge(currentUser, challenge); - // Only M2M can update url and options of discussions if (data.discussions && data.discussions.length > 0) { if (challenge.discussions && challenge.discussions.length > 0) { @@ -1860,52 +1747,8 @@ async function update(currentUser, challengeId, data, isFull) { } } - // Validate the challenge terms - let newTermsOfUse; - if (!_.isUndefined(data.terms)) { - // helper.ensureNoDuplicateOrNullElements(data.terms, 'terms') - - // Get the project default terms - const defaultTerms = await helper.getProjectDefaultTerms(challenge.projectId); - - if (defaultTerms) { - // Make sure that the default project terms were not removed - // TODO - there are no default terms returned by v5 - // the terms array is objects with a roleId now, so this _.difference won't work - // const removedTerms = _.difference(defaultTerms, data.terms) - // if (removedTerms.length !== 0) { - // throw new errors.BadRequestError(`Default project terms ${removedTerms} should not be removed`) - // } - } - // newTermsOfUse = await helper.validateChallengeTerms(_.union(data.terms, defaultTerms)) - newTermsOfUse = await helper.validateChallengeTerms(data.terms); - } - await challengeHelper.validateAndGetChallengeTypeAndTrack(data); - if ( - (challenge.status === constants.challengeStatuses.Completed || - challenge.status === constants.challengeStatuses.Cancelled) && - data.status && - data.status !== challenge.status && - data.status !== constants.challengeStatuses.CancelledClientRequest - ) { - throw new errors.BadRequestError( - `Cannot change ${challenge.status} challenge status to ${data.status} status` - ); - } - - if ( - data.winners && - data.winners.length > 0 && - challenge.status !== constants.challengeStatuses.Completed && - data.status !== constants.challengeStatuses.Completed - ) { - throw new errors.BadRequestError( - `Cannot set winners for challenge with non-completed ${challenge.status} status` - ); - } - // TODO: Fix this Tech Debt once legacy is turned off const finalStatus = data.status || challenge.status; const finalTimelineTemplateId = data.timelineTemplateId || challenge.timelineTemplateId; @@ -1924,6 +1767,14 @@ async function update(currentUser, challengeId, data, isFull) { challenge.phases = []; timelineTemplateChanged = true; } + /* + 1) create draft challenge -> challenge.legacy is FULLY populated + 2) Create new challenge -> mark challenge as draft -> challenge.legacy WILL BE populdated db DC + */ + + if (data.legacy.xyz) { + challenge.legacy.xyz = data.legacy.xyz; + } if (data.prizeSets && data.prizeSets.length > 0) { if ( @@ -1982,21 +1833,6 @@ async function update(currentUser, challengeId, data, isFull) { data.endDate = helper.calculateChallengeEndDate(challenge, data); } - // PUT HERE - if (data.status) { - if (challenge.legacy.selfService && data.status === constants.challengeStatuses.Draft) { - try { - await helper.updateSelfServiceProjectInfo( - challenge.projectId, - data.endDate || challenge.endDate, - currentUser - ); - } catch (e) { - logger.debug(`There was an error trying to update the project: ${e.message}`); - } - } - } - if (data.winners && data.winners.length && data.winners.length > 0) { await validateWinners(data.winners, challengeId); } @@ -2051,170 +1887,6 @@ async function update(currentUser, challengeId, data, isFull) { data.updated = moment().utc(); data.updatedBy = currentUser.handle || currentUser.sub; - const updateDetails = {}; - let phasesHaveBeenModified = false; - _.each(data, (value, key) => { - let op; - if (key === "metadata") { - if ( - _.isUndefined(challenge[key]) || - challenge[key].length !== value.length || - _.differenceWith(challenge[key], value, _.isEqual).length !== 0 - ) { - op = "$PUT"; - } - } else if (key === "phases") { - // always consider a modification if the property exists - phasesHaveBeenModified = true; - logger.info("update phases"); - op = "$PUT"; - } else if (key === "prizeSets") { - if (isDifferentPrizeSets(challenge[key], value)) { - logger.info("update prize sets"); - op = "$PUT"; - } - } else if (key === "tags") { - if ( - _.isUndefined(challenge[key]) || - challenge[key].length !== value.length || - _.intersection(challenge[key], value).length !== value.length - ) { - op = "$PUT"; - } - } else if (key === "attachments") { - const oldIds = _.map(challenge.attachments || [], (a) => a.id); - if ( - oldIds.length !== value.length || - _.intersection( - oldIds, - _.map(value, (a) => a.id) - ).length !== value.length - ) { - op = "$PUT"; - } - } else if (key === "groups") { - if ( - _.isUndefined(challenge[key]) || - challenge[key].length !== value.length || - _.intersection(challenge[key], value).length !== value.length - ) { - op = "$PUT"; - } - // } else if (key === 'gitRepoURLs') { - // if (_.isUndefined(challenge[key]) || challenge[key].length !== value.length || - // _.intersection(challenge[key], value).length !== value.length) { - // op = '$PUT' - // } - } else if (key === "winners") { - if ( - _.isUndefined(challenge[key]) || - challenge[key].length !== value.length || - _.intersectionWith(challenge[key], value, _.isEqual).length !== value.length - ) { - op = "$PUT"; - } - } else if (key === "terms") { - const oldIds = _.map(challenge.terms || [], (t) => t.id); - const newIds = _.map(value || [], (t) => t.id); - if ( - oldIds.length !== newIds.length || - _.intersection(oldIds, newIds).length !== value.length - ) { - op = "$PUT"; - } - } else if (key === "billing" || key === "legacy") { - // make sure that's always being udpated - op = "$PUT"; - } else if (_.isUndefined(challenge[key]) || challenge[key] !== value) { - op = "$PUT"; - } else if (_.get(challenge, "legacy.pureV5Task") && key === "task") { - // always update task for pureV5 challenges - op = "$PUT"; - } - - if (op) { - if (_.isUndefined(updateDetails[op])) { - updateDetails[op] = {}; - } - if (key === "attachments") { - updateDetails[op].attachments = newAttachments; - } else if (key === "terms") { - updateDetails[op].terms = newTermsOfUse; - } else { - updateDetails[op][key] = value; - } - if (key !== "updated" && key !== "updatedBy") { - let oldValue; - let newValue; - if (key === "attachments") { - oldValue = challenge.attachments ? JSON.stringify(challenge.attachments) : "NULL"; - newValue = JSON.stringify(newAttachments); - } else if (key === "terms") { - oldValue = challenge.terms ? JSON.stringify(challenge.terms) : "NULL"; - newValue = JSON.stringify(newTermsOfUse); - } else { - oldValue = challenge[key] ? JSON.stringify(challenge[key]) : "NULL"; - newValue = JSON.stringify(value); - } - } - } - }); - - if (isFull && _.isUndefined(data.metadata) && challenge.metadata) { - updateDetails["$DELETE"] = { metadata: null }; - delete challenge.metadata; - // send null to Elasticsearch to clear the field - data.metadata = null; - } - if (isFull && _.isUndefined(data.attachments) && challenge.attachments) { - if (!updateDetails["$DELETE"]) { - updateDetails["$DELETE"] = {}; - } - updateDetails["$DELETE"].attachments = null; - delete challenge.attachments; - // send null to Elasticsearch to clear the field - data.attachments = null; - } - if (isFull && _.isUndefined(data.groups) && challenge.groups) { - if (!updateDetails["$DELETE"]) { - updateDetails["$DELETE"] = {}; - } - updateDetails["$DELETE"].groups = null; - delete challenge.groups; - // send null to Elasticsearch to clear the field - data.groups = null; - } - // if (isFull && _.isUndefined(data.gitRepoURLs) && challenge.gitRepoURLs) { - // if (!updateDetails['$DELETE']) { - // updateDetails['$DELETE'] = {} - // } - // updateDetails['$DELETE'].gitRepoURLs = null - // auditLogs.push({ - // id: uuid(), - // challengeId, - // fieldName: 'gitRepoURLs', - // oldValue: JSON.stringify(challenge.gitRepoURLs), - // newValue: 'NULL', - // created: moment().utc(), - // createdBy: currentUser.handle || currentUser.sub, - // memberId: currentUser.userId || null - // }) - // delete challenge.gitRepoURLs - // // send null to Elasticsearch to clear the field - // data.gitRepoURLs = null - // } - if (isFull && _.isUndefined(data.legacyId) && challenge.legacyId) { - data.legacyId = challenge.legacyId; - } - if (isFull && _.isUndefined(data.winners) && challenge.winners) { - if (!updateDetails["$DELETE"]) { - updateDetails["$DELETE"] = {}; - } - updateDetails["$DELETE"].winners = null; - delete challenge.winners; - // send null to Elasticsearch to clear the field - data.winners = null; - } const { track, type } = await validateChallengeData(_.pick(challenge, ["trackId", "typeId"])); @@ -2241,10 +1913,35 @@ async function update(currentUser, challengeId, data, isFull) { } } - logger.debug(`Challenge.update id: ${challengeId} Details: ${JSON.stringify(updateDetails)}`); + let newAttachments = isFull ? data.attachments : challenge.attachments || []; + // if (isFull || !_.isUndefined(data.attachments)) { + // newAttachments = data.attachments; + // } + + // Validate the challenge terms + let newTermsOfUse; + if (!_.isUndefined(data.terms)) { + // helper.ensureNoDuplicateOrNullElements(data.terms, 'terms') + + // Get the project default terms + const defaultTerms = await helper.getProjectDefaultTerms(challenge.projectId); + + if (defaultTerms) { + // Make sure that the default project terms were not removed + // TODO - there are no default terms returned by v5 + // the terms array is objects with a roleId now, so this _.difference won't work + // const removedTerms = _.difference(defaultTerms, data.terms) + // if (removedTerms.length !== 0) { + // throw new errors.BadRequestError(`Default project terms ${removedTerms} should not be removed`) + // } + } + // newTermsOfUse = await helper.validateChallengeTerms(_.union(data.terms, defaultTerms)) + newTermsOfUse = await helper.validateChallengeTerms(data.terms); + } delete data.attachments; delete data.terms; + _.assign(challenge, data); if (!_.isUndefined(newAttachments)) { challenge.attachments = newAttachments; @@ -2261,7 +1958,6 @@ async function update(currentUser, challengeId, data, isFull) { } // Populate challenge.track and challenge.type based on the track/type IDs - if (track) { challenge.track = track.name; } @@ -2289,6 +1985,7 @@ async function update(currentUser, challengeId, data, isFull) { } catch (e) { throw e; } + // post bus event logger.debug(`Post Bus Event: ${constants.Topics.ChallengeUpdated} ${JSON.stringify(challenge)}`); const options = {}; @@ -2296,21 +1993,22 @@ async function update(currentUser, challengeId, data, isFull) { options.key = `${challenge.id}:${challenge.status}`; } await helper.postBusEvent(constants.Topics.ChallengeUpdated, challenge, options); - if (phasesHaveBeenModified === true && _.get(challenge, "legacy.useSchedulingAPI")) { - await helper.postBusEvent(config.SCHEDULING_TOPIC, { id: challengeId }); - } + if (challenge.phases && challenge.phases.length > 0) { challenge.currentPhase = challenge.phases .slice() .reverse() .find((phase) => phase.isOpen); challenge.endDate = helper.calculateChallengeEndDate(challenge); + const registrationPhase = _.find(challenge.phases, (p) => p.name === "Registration"); const submissionPhase = _.find(challenge.phases, (p) => p.name === "Submission"); + challenge.currentPhaseNames = _.map( _.filter(challenge.phases, (p) => p.isOpen === true), "name" ); + if (registrationPhase) { challenge.registrationStartDate = registrationPhase.actualStartDate || registrationPhase.scheduledStartDate; @@ -2324,6 +2022,7 @@ async function update(currentUser, challengeId, data, isFull) { submissionPhase.actualEndDate || submissionPhase.scheduledEndDate; } } + // Update ES await esClient.update({ index: config.get("ES.ES_INDEX"), @@ -2381,6 +2080,7 @@ async function update(currentUser, challengeId, data, isFull) { ); } } + return challenge; } From 1fd1ea472da4842dd30cdbea8863b146f5545762 Mon Sep 17 00:00:00 2001 From: Rakib Ansary Date: Fri, 24 Mar 2023 02:54:39 +0600 Subject: [PATCH 03/22] refactor: only update whats necessary in challenge:update Signed-off-by: Rakib Ansary --- package.json | 4 +- src/common/challenge-helper.js | 53 +++++ src/services/ChallengeService.js | 364 +++++++++---------------------- yarn.lock | 40 ++-- 4 files changed, 182 insertions(+), 279 deletions(-) diff --git a/package.json b/package.json index fd85d29a..627ab6bd 100644 --- a/package.json +++ b/package.json @@ -42,8 +42,8 @@ "dependencies": { "@grpc/grpc-js": "^1.8.12", "@opensearch-project/opensearch": "^2.2.0", - "@topcoder-framework/domain-challenge": "^0.7.3", - "@topcoder-framework/lib-common": "^0.7.3", + "@topcoder-framework/domain-challenge": "^0.8.0", + "@topcoder-framework/lib-common": "^0.8.0", "aws-sdk": "^2.1145.0", "axios": "^0.19.0", "axios-retry": "^3.4.0", diff --git a/src/common/challenge-helper.js b/src/common/challenge-helper.js index a05ac7ff..7801f32a 100644 --- a/src/common/challenge-helper.js +++ b/src/common/challenge-helper.js @@ -187,6 +187,59 @@ class ChallengeHelper { ); } } + + enrichChallengeForResponse(challenge) { + if (challenge.phases && challenge.phases.length > 0) { + const registrationPhase = _.find(challenge.phases, (p) => p.name === "Registration"); + const submissionPhase = _.find(challenge.phases, (p) => p.name === "Submission"); + + challenge.currentPhase = challenge.phases + .slice() + .reverse() + .find((phase) => phase.isOpen); + + challenge.currentPhaseNames = _.map( + _.filter(ret.phases, (p) => p.isOpen === true), + "name" + ); + + if (registrationPhase) { + challenge.registrationStartDate = + registrationPhase.actualStartDate || registrationPhase.scheduledStartDate; + challenge.registrationEndDate = + registrationPhase.actualEndDate || registrationPhase.scheduledEndDate; + } + if (submissionPhase) { + challenge.submissionStartDate = + submissionPhase.actualStartDate || submissionPhase.scheduledStartDate; + + challenge.submissionEndDate = + submissionPhase.actualEndDate || submissionPhase.scheduledEndDate; + } + } + + challenge.created = new Date(challenge.created).toISOString(); + challenge.updated = new Date(challenge.updated).toISOString(); + challenge.startDate = new Date(challenge.startDate).toISOString(); + challenge.endDate = new Date(challenge.endDate).toISOString(); + + if (track) { + challenge.track = track.name; + } + + if (type) { + challenge.type = type.name; + } + + challenge.metadata = challenge.metadata.map((m) => { + try { + m.value = JSON.stringify(JSON.parse(m.value)); // when we update how we index data, make this a JSON field + } catch (err) { + // do nothing + } + return m; + }); + } } module.exports = new ChallengeHelper(); diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index c2854b97..5bd87b04 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -36,60 +36,13 @@ const esClient = helper.getESClient(); const { ChallengeDomain } = require("@topcoder-framework/domain-challenge"); const { hasAdminRole } = require("../common/role-helper"); -const { ensureAcessibilityToModifiedGroups } = require("../common/group-helper"); -const { validateChallengeUpdateRequest } = require("../common/challenge-helper"); +const { + validateChallengeUpdateRequest, + enrichChallengeForResponse, +} = require("../common/challenge-helper"); const challengeDomain = new ChallengeDomain(GRPC_CHALLENGE_SERVER_HOST, GRPC_CHALLENGE_SERVER_PORT); -/** - * Validate the challenge data. - * @param {Object} challenge the challenge data - */ -async function validateChallengeData(challenge) { - let type; - let track; - if (challenge.typeId) { - try { - type = await ChallengeTypeService.getChallengeType(challenge.typeId); - } catch (e) { - if (e.name === "NotFoundError") { - const error = new errors.BadRequestError( - `No challenge type found with id: ${challenge.typeId}.` - ); - throw error; - } else { - throw e; - } - } - } - if (challenge.trackId) { - try { - track = await ChallengeTrackService.getChallengeTrack(challenge.trackId); - } catch (e) { - if (e.name === "NotFoundError") { - const error = new errors.BadRequestError( - `No challenge track found with id: ${challenge.trackId}.` - ); - throw error; - } else { - throw e; - } - } - } - if (challenge.timelineTemplateId) { - const template = await TimelineTemplateService.getTimelineTemplate( - challenge.timelineTemplateId - ); - if (!template.isActive) { - const error = new errors.BadRequestError( - `The timeline template with id: ${challenge.timelineTemplateId} is inactive` - ); - throw error; - } - } - return { type, track }; -} - /** * Check if user can perform modification/deletion to a challenge * @@ -1168,47 +1121,7 @@ async function createChallenge(currentUser, challenge, userToken) { ret.numOfSubmissions = 0; ret.numOfRegistrants = 0; - if (ret.phases && ret.phases.length > 0) { - const registrationPhase = _.find(ret.phases, (p) => p.name === "Registration"); - const submissionPhase = _.find(ret.phases, (p) => p.name === "Submission"); - ret.currentPhaseNames = _.map( - _.filter(ret.phases, (p) => p.isOpen === true), - "name" - ); - if (registrationPhase) { - ret.registrationStartDate = - registrationPhase.actualStartDate || registrationPhase.scheduledStartDate; - ret.registrationEndDate = - registrationPhase.actualEndDate || registrationPhase.scheduledEndDate; - } - if (submissionPhase) { - ret.submissionStartDate = - submissionPhase.actualStartDate || submissionPhase.scheduledStartDate; - ret.submissionEndDate = submissionPhase.actualEndDate || submissionPhase.scheduledEndDate; - } - } - - ret.created = new Date(ret.created).toISOString(); - ret.updated = new Date(ret.updated).toISOString(); - ret.startDate = new Date(ret.startDate).toISOString(); - ret.endDate = new Date(ret.endDate).toISOString(); - - if (track) { - ret.track = track.name; - } - - if (type) { - ret.type = type.name; - } - - ret.metadata = ret.metadata.map((m) => { - try { - m.value = JSON.stringify(JSON.parse(m.value)); // when we update how we index data, make this a JSON field - } catch (err) { - // do nothing - } - return m; - }); + enrichChallengeForResponse(ret); // Create in ES await esClient.create({ @@ -1580,10 +1493,7 @@ async function update(currentUser, challengeId, data) { validateChallengeUpdateRequest(currentUser, challenge, data); const projectId = _.get(challenge, "projectId"); - const cancelReason = _.cloneDeep(data.cancelReason); - delete data.cancelReason; - let dynamicDescription = _.cloneDeep(data.description || challenge.description); let sendActivationEmail = false; let sendSubmittedEmail = false; let sendCompletedEmail = false; @@ -1597,72 +1507,75 @@ async function update(currentUser, challengeId, data) { } /* BEGIN self-service stuffs */ - if (challenge.legacy.selfService && data.metadata && data.metadata.length > 0) { - for (const entry of data.metadata) { - const regexp = new RegExp(`{{${entry.name}}}`, "g"); - dynamicDescription = dynamicDescription.replace(regexp, entry.value); + + // TODO: At some point in the future this should be moved to a Self-Service Challenge Helper + + if (challenge.legacy.selfService) { + // prettier-ignore + sendSubmittedEmail = data.status === constants.challengeStatuses.Draft && challenge.status !== constants.challengeStatuses.Draft; + + if (data.metadata && data.metadata.length > 0) { + let dynamicDescription = _.cloneDeep(data.description || challenge.description); + for (const entry of data.metadata) { + const regexp = new RegExp(`{{${entry.name}}}`, "g"); + dynamicDescription = dynamicDescription.replace(regexp, entry.value); + } + data.description = dynamicDescription; } - } - if ( - challenge.legacy.selfService && - data.status === constants.challengeStatuses.Draft && - challenge.status !== constants.challengeStatuses.Draft - ) { - sendSubmittedEmail = true; - } - // check if it's a self service challenge and project needs to be activated first - if ( - challenge.legacy.selfService && - (data.status === constants.challengeStatuses.Approved || - data.status === constants.challengeStatuses.Active) && - challenge.status !== constants.challengeStatuses.Active - ) { - try { - const selfServiceProjectName = `Self service - ${challenge.createdBy} - ${challenge.name}`; - const workItemSummary = _.get( - _.find(_.get(challenge, "metadata", []), (m) => m.name === "websitePurpose.description"), - "value", - "N/A" - ); - await helper.activateProject( - challenge.projectId, - currentUser, - selfServiceProjectName, - workItemSummary - ); - if (data.status === constants.challengeStatuses.Active) { - sendActivationEmail = true; + + // check if it's a self service challenge and project needs to be activated first + if ( + (data.status === constants.challengeStatuses.Approved || + data.status === constants.challengeStatuses.Active) && + challenge.status !== constants.challengeStatuses.Active + ) { + try { + const selfServiceProjectName = `Self service - ${challenge.createdBy} - ${challenge.name}`; + const workItemSummary = _.get( + _.find(_.get(challenge, "metadata", []), (m) => m.name === "websitePurpose.description"), + "value", + "N/A" + ); + await helper.activateProject( + projectId, + currentUser, + selfServiceProjectName, + workItemSummary + ); + + sendActivationEmail = data.status === constants.challengeStatuses.Active; + } catch (e) { + await update( + currentUser, + challengeId, + { + ...data, + status: constants.challengeStatuses.CancelledPaymentFailed, + cancelReason: `Failed to activate project. Error: ${e.message}. JSON: ${JSON.stringify( + e + )}`, + }, + false + ); + throw new errors.BadRequestError( + "Failed to activate the challenge! The challenge has been canceled!" + ); } - } catch (e) { - await update( - currentUser, - challengeId, - { - ...data, - status: constants.challengeStatuses.CancelledPaymentFailed, - cancelReason: `Failed to activate project. Error: ${e.message}. JSON: ${JSON.stringify( - e - )}`, - }, - false - ); - throw new errors.BadRequestError( - "Failed to activate the challenge! The challenge has been canceled!" - ); } - } - if (challenge.legacy.selfService && data.status === constants.challengeStatuses.Draft) { - try { - await helper.updateSelfServiceProjectInfo( - challenge.projectId, - data.endDate || challenge.endDate, - currentUser - ); - } catch (e) { - logger.debug(`There was an error trying to update the project: ${e.message}`); + if (data.status === constants.challengeStatuses.Draft) { + try { + await helper.updateSelfServiceProjectInfo( + projectId, + data.endDate || challenge.endDate, + currentUser + ); + } catch (e) { + logger.debug(`There was an error trying to update the project: ${e.message}`); + } } } + /* END self-service stuffs */ let isChallengeBeingActivated = false; @@ -1690,6 +1603,7 @@ async function update(currentUser, challengeId, data) { isChallengeBeingActivated = true; } } + if ( data.status === constants.challengeStatuses.CancelledRequirementsInfeasible || data.status === constants.challengeStatuses.CancelledPaymentFailed @@ -1701,6 +1615,7 @@ async function update(currentUser, challengeId, data) { } sendRejectedEmail = true; } + if (data.status === constants.challengeStatuses.Completed) { if ( !_.get(challenge, "legacy.pureV5Task") && @@ -1747,8 +1662,6 @@ async function update(currentUser, challengeId, data) { } } - await challengeHelper.validateAndGetChallengeTypeAndTrack(data); - // TODO: Fix this Tech Debt once legacy is turned off const finalStatus = data.status || challenge.status; const finalTimelineTemplateId = data.timelineTemplateId || challenge.timelineTemplateId; @@ -1767,14 +1680,6 @@ async function update(currentUser, challengeId, data) { challenge.phases = []; timelineTemplateChanged = true; } - /* - 1) create draft challenge -> challenge.legacy is FULLY populated - 2) Create new challenge -> mark challenge as draft -> challenge.legacy WILL BE populdated db DC - */ - - if (data.legacy.xyz) { - challenge.legacy.xyz = data.legacy.xyz; - } if (data.prizeSets && data.prizeSets.length > 0) { if ( @@ -1791,13 +1696,13 @@ async function update(currentUser, challengeId, data) { _.get(challenge, "overview.totalPrizes") ) { // remove the totalPrizes if challenge prizes are empty - challenge.overview = _.omit(challenge.overview, ["totalPrizes"]); + data.overview = challenge.overview = _.omit(challenge.overview, ["totalPrizes"]); } else { const totalPrizes = helper.sumOfPrizes( prizeSetsGroup[constants.prizeSetTypes.ChallengePrizes][0].prizes ); - logger.debug(`re-calculate total prizes, current value is ${totalPrizes.value}`); _.assign(challenge, { overview: { totalPrizes } }); + _.assign(data, { overview: { totalPrizes } }); } } @@ -1885,10 +1790,11 @@ async function update(currentUser, challengeId, data) { logger.info(`${challengeId} is not a pureV5 challenge or has no winners set yet.`); } - data.updated = moment().utc(); - data.updatedBy = currentUser.handle || currentUser.sub; - - const { track, type } = await validateChallengeData(_.pick(challenge, ["trackId", "typeId"])); + const { track, type } = await challengeHelper.validateAndGetChallengeTypeAndTrack({ + typeId: challenge.typeId, + trackId: challenge.trackId, + timelineTemplateId: challenge.timelineTemplateId, + }); if (_.get(type, "isTask")) { if (!_.isEmpty(_.get(data, "task.memberId"))) { @@ -1913,115 +1819,59 @@ async function update(currentUser, challengeId, data) { } } - let newAttachments = isFull ? data.attachments : challenge.attachments || []; - // if (isFull || !_.isUndefined(data.attachments)) { - // newAttachments = data.attachments; - // } - - // Validate the challenge terms - let newTermsOfUse; if (!_.isUndefined(data.terms)) { - // helper.ensureNoDuplicateOrNullElements(data.terms, 'terms') - - // Get the project default terms - const defaultTerms = await helper.getProjectDefaultTerms(challenge.projectId); - - if (defaultTerms) { - // Make sure that the default project terms were not removed - // TODO - there are no default terms returned by v5 - // the terms array is objects with a roleId now, so this _.difference won't work - // const removedTerms = _.difference(defaultTerms, data.terms) - // if (removedTerms.length !== 0) { - // throw new errors.BadRequestError(`Default project terms ${removedTerms} should not be removed`) - // } - } - // newTermsOfUse = await helper.validateChallengeTerms(_.union(data.terms, defaultTerms)) - newTermsOfUse = await helper.validateChallengeTerms(data.terms); + await helper.validateChallengeTerms(data.terms); } - delete data.attachments; - delete data.terms; - - _.assign(challenge, data); - if (!_.isUndefined(newAttachments)) { - challenge.attachments = newAttachments; - data.attachments = newAttachments; - } - - if (!_.isUndefined(newTermsOfUse)) { - challenge.terms = newTermsOfUse; - data.terms = newTermsOfUse; - } - - if (challenge.phases && challenge.phases.length > 0) { - await getPhasesAndPopulate(challenge); + if (data.phases && data.phases.length > 0) { + await getPhasesAndPopulate(data); } // Populate challenge.track and challenge.type based on the track/type IDs if (track) { - challenge.track = track.name; + data.track = track.name; } if (type) { - challenge.type = type.name; + data.type = type.name; } try { logger.debug( - `ChallengeDomain.update id: ${challengeId} Details: ${JSON.stringify(challenge)}` + `ChallengeDomain.update id: ${challengeId} Details: ${JSON.stringify(data, null, 2)}` ); - const { items } = await challengeDomain.update({ - filterCriteria: getScanCriteria({ - id: challengeId, - }), - updateInput: { - ...challenge, + + const grpcMetadata = new GrpcMetadata(); + + grpcMetadata.set("handle", currentUser.handle); + grpcMetadata.set("userId", currentUser.userId); + + const { items } = await challengeDomain.update( + { + filterCriteria: getScanCriteria({ + id: challengeId, + }), + updateInput: { + ...data, + }, }, - }); - if (items.length > 0) { - if (!challenge.legacyId) { - challenge.legacyId = items[0].legacyId; - } - } + grpcMetadata + ); + + console.log("Items", items); } catch (e) { throw e; } + challenge = await challengeDomain.lookup({ id: challengeId }); + // post bus event logger.debug(`Post Bus Event: ${constants.Topics.ChallengeUpdated} ${JSON.stringify(challenge)}`); - const options = {}; - if (challenge.status === "Completed") { - options.key = `${challenge.id}:${challenge.status}`; - } - await helper.postBusEvent(constants.Topics.ChallengeUpdated, challenge, options); - - if (challenge.phases && challenge.phases.length > 0) { - challenge.currentPhase = challenge.phases - .slice() - .reverse() - .find((phase) => phase.isOpen); - challenge.endDate = helper.calculateChallengeEndDate(challenge); - - const registrationPhase = _.find(challenge.phases, (p) => p.name === "Registration"); - const submissionPhase = _.find(challenge.phases, (p) => p.name === "Submission"); - challenge.currentPhaseNames = _.map( - _.filter(challenge.phases, (p) => p.isOpen === true), - "name" - ); + await helper.postBusEvent(constants.Topics.ChallengeUpdated, challenge, { + key: challenge.status === "Completed" ? `${challenge.id}:${challenge.status}` : undefined, + }); - if (registrationPhase) { - challenge.registrationStartDate = - registrationPhase.actualStartDate || registrationPhase.scheduledStartDate; - challenge.registrationEndDate = - registrationPhase.actualEndDate || registrationPhase.scheduledEndDate; - } - if (submissionPhase) { - challenge.submissionStartDate = - submissionPhase.actualStartDate || submissionPhase.scheduledStartDate; - challenge.submissionEndDate = - submissionPhase.actualEndDate || submissionPhase.scheduledEndDate; - } - } + enrichChallengeForResponse(challenge); // Update ES await esClient.update({ diff --git a/yarn.lock b/yarn.lock index bea6ab1e..ecc5a0a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -245,35 +245,35 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== -"@topcoder-framework/client-relational@^0.7.3": - version "0.7.3" - resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/client-relational/-/client-relational-0.7.3.tgz#c1ded75b4e00dc93e4ff05abd01dacbee971b484" - integrity sha512-6QF2yjp4NG6Qdw6W6WmAj9N4y2+ZZd3gL7oTr78y03v9qDKzUP11EwFCLEjL2oiqDZbyB5QV7OQyf77buvcDyw== +"@topcoder-framework/client-relational@^0.8.0": + version "0.8.0" + resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/client-relational/-/client-relational-0.8.0.tgz#66725c1b82498297aac911298a2cf1aeaed991c8" + integrity sha512-jSyyqDQuBC5PQ/QOh9amaqNmrnztnDp/zMC7MAhSAUa5mSJdoJcn7t3fTpi6wJ5BHT8hkOOAQbsFb4NkhYNhkw== dependencies: "@grpc/grpc-js" "^1.8.0" - "@topcoder-framework/lib-common" "^0.7.3" - topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.30" + "@topcoder-framework/lib-common" "^0.8.0" + topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.31" tslib "^2.4.1" -"@topcoder-framework/domain-challenge@^0.7.3": - version "0.7.3" - resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/domain-challenge/-/domain-challenge-0.7.3.tgz#0fdc82e9277eab953cd12521b286900d3b7f27bb" - integrity sha512-eeRbf/gt6BRoIzeB3Vmt2/AHLoHM8w9BWcs+FGsQfmtLzyZbs0TK4D3vJNQX1dVRymKYxP6apWHKWH0lAsUgAQ== +"@topcoder-framework/domain-challenge@^0.8.0": + version "0.8.0" + resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/domain-challenge/-/domain-challenge-0.8.0.tgz#67074caf11edbf48e9cef29313b3ceb2b21ee757" + integrity sha512-IWgyGBlMOBl48CsmE37Z7dghvtfjcfGyo0PJVpRsYzdlnvHd6nY+tlq/bEKY2c9JasnlMeni/i4rUWeXG8JZyw== dependencies: "@grpc/grpc-js" "^1.8.0" - "@topcoder-framework/client-relational" "^0.7.3" - "@topcoder-framework/lib-common" "^0.7.3" - topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.30" + "@topcoder-framework/client-relational" "^0.8.0" + "@topcoder-framework/lib-common" "^0.8.0" + topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.31" tslib "^2.4.1" -"@topcoder-framework/lib-common@^0.7.3": - version "0.7.3" - resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/lib-common/-/lib-common-0.7.3.tgz#67893108d36580716875283ff72c71c6fd2dfa38" - integrity sha512-CqoQJhUPjTp4sDZ6SirujuHxbnUduL5ZRiaeEyZN6jrcpRaScB95vE7rzvUFrd4FVzGd4BhcA4QMhNPPnO1mKw== +"@topcoder-framework/lib-common@^0.8.0": + version "0.8.0" + resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/lib-common/-/lib-common-0.8.0.tgz#3f29daace074734d74c638e860204011d5e83a21" + integrity sha512-xuyU3kQGiNOPPzwZLXzrTbu6o9FC2SVei7FWBwzL4HraD6aj3fDqME7dappY32QWB/lFxqyTm00SeekmkbCr1w== dependencies: "@grpc/grpc-js" "^1.8.0" rimraf "^3.0.2" - topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.30" + topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.31" tslib "^2.4.1" "@types/body-parser@*": @@ -3596,9 +3596,9 @@ topcoder-bus-api-wrapper@topcoder-platform/tc-bus-api-wrapper.git: superagent "^3.8.3" tc-core-library-js appirio-tech/tc-core-library-js.git#v2.6.4 -"topcoder-interface@github:topcoder-platform/plat-interface-definition#v0.0.30": +"topcoder-interface@github:topcoder-platform/plat-interface-definition#v0.0.31": version "1.0.0" - resolved "https://codeload.github.com/topcoder-platform/plat-interface-definition/tar.gz/704a8d8ed31bb5f7edfd328aeaedaa3d36d56e33" + resolved "https://codeload.github.com/topcoder-platform/plat-interface-definition/tar.gz/8fde0bf4ab310b887383c7b37b6ae919cd407388" topo@3.x.x: version "3.0.3" From 396db3607ec9ec531b618620e1c1f8157d43f4f0 Mon Sep 17 00:00:00 2001 From: Rakib Ansary Date: Fri, 24 Mar 2023 02:55:23 +0600 Subject: [PATCH 04/22] ci: deploy to dev Signed-off-by: Rakib Ansary --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index d8a6649a..945f8788 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -82,6 +82,7 @@ workflows: branches: only: - refactor/domain-challenge-dev + - refactor/challenge-update - "build-qa": context: org-global From 704f88d75f84cff63272646726a0f78abe2fdd94 Mon Sep 17 00:00:00 2001 From: Rakib Ansary Date: Fri, 24 Mar 2023 04:12:25 +0600 Subject: [PATCH 05/22] fix: missing variables Signed-off-by: Rakib Ansary --- src/common/challenge-helper.js | 4 ++-- src/services/ChallengeService.js | 36 ++++++++++++++++---------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/common/challenge-helper.js b/src/common/challenge-helper.js index 7801f32a..34ba072f 100644 --- a/src/common/challenge-helper.js +++ b/src/common/challenge-helper.js @@ -188,7 +188,7 @@ class ChallengeHelper { } } - enrichChallengeForResponse(challenge) { + enrichChallengeForResponse(challenge, track, type) { if (challenge.phases && challenge.phases.length > 0) { const registrationPhase = _.find(challenge.phases, (p) => p.name === "Registration"); const submissionPhase = _.find(challenge.phases, (p) => p.name === "Submission"); @@ -199,7 +199,7 @@ class ChallengeHelper { .find((phase) => phase.isOpen); challenge.currentPhaseNames = _.map( - _.filter(ret.phases, (p) => p.isOpen === true), + _.filter(challenge.phases, (p) => p.isOpen === true), "name" ); diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index 5bd87b04..4d3d1adc 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -1121,29 +1121,29 @@ async function createChallenge(currentUser, challenge, userToken) { ret.numOfSubmissions = 0; ret.numOfRegistrants = 0; - enrichChallengeForResponse(ret); + enrichChallengeForResponse(ret, track, type); // Create in ES - await esClient.create({ - index: config.get("ES.ES_INDEX"), - type: config.get("ES.OPENSEARCH") == "false" ? config.get("ES.ES_TYPE") : undefined, - refresh: config.get("ES.ES_REFRESH"), - id: ret.id, - body: ret, - }); + // await esClient.create({ + // index: config.get("ES.ES_INDEX"), + // type: config.get("ES.OPENSEARCH") == "false" ? config.get("ES.ES_TYPE") : undefined, + // refresh: config.get("ES.ES_REFRESH"), + // id: ret.id, + // body: ret, + // }); // If the challenge is self-service, add the creating user as the "client manager", *not* the manager // This is necessary for proper handling of the vanilla embed on the self-service work item dashboard - if (challenge.legacy.selfService) { - if (currentUser.handle) { - await helper.createResource(ret.id, ret.createdBy, config.CLIENT_MANAGER_ROLE_ID); - } - } else { - if (currentUser.handle) { - await helper.createResource(ret.id, ret.createdBy, config.MANAGER_ROLE_ID); - } - } + // if (challenge.legacy.selfService) { + // if (currentUser.handle) { + // await helper.createResource(ret.id, ret.createdBy, config.CLIENT_MANAGER_ROLE_ID); + // } + // } else { + // if (currentUser.handle) { + // await helper.createResource(ret.id, ret.createdBy, config.MANAGER_ROLE_ID); + // } + // } // post bus event await helper.postBusEvent(constants.Topics.ChallengeCreated, ret); @@ -1871,7 +1871,7 @@ async function update(currentUser, challengeId, data) { key: challenge.status === "Completed" ? `${challenge.id}:${challenge.status}` : undefined, }); - enrichChallengeForResponse(challenge); + enrichChallengeForResponse(challenge, track, type); // Update ES await esClient.update({ From c44d4d4e6ecbbf2e5cb1defff6eb3df2a848ace8 Mon Sep 17 00:00:00 2001 From: Rakib Ansary Date: Fri, 24 Mar 2023 04:18:33 +0600 Subject: [PATCH 06/22] fix: enable indexing and resource creation Signed-off-by: Rakib Ansary --- src/services/ChallengeService.js | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index 4d3d1adc..87fd9e80 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -1124,26 +1124,26 @@ async function createChallenge(currentUser, challenge, userToken) { enrichChallengeForResponse(ret, track, type); // Create in ES - // await esClient.create({ - // index: config.get("ES.ES_INDEX"), - // type: config.get("ES.OPENSEARCH") == "false" ? config.get("ES.ES_TYPE") : undefined, - // refresh: config.get("ES.ES_REFRESH"), - // id: ret.id, - // body: ret, - // }); + await esClient.create({ + index: config.get("ES.ES_INDEX"), + type: config.get("ES.OPENSEARCH") == "false" ? config.get("ES.ES_TYPE") : undefined, + refresh: config.get("ES.ES_REFRESH"), + id: ret.id, + body: ret, + }); // If the challenge is self-service, add the creating user as the "client manager", *not* the manager // This is necessary for proper handling of the vanilla embed on the self-service work item dashboard - // if (challenge.legacy.selfService) { - // if (currentUser.handle) { - // await helper.createResource(ret.id, ret.createdBy, config.CLIENT_MANAGER_ROLE_ID); - // } - // } else { - // if (currentUser.handle) { - // await helper.createResource(ret.id, ret.createdBy, config.MANAGER_ROLE_ID); - // } - // } + if (challenge.legacy.selfService) { + if (currentUser.handle) { + await helper.createResource(ret.id, ret.createdBy, config.CLIENT_MANAGER_ROLE_ID); + } + } else { + if (currentUser.handle) { + await helper.createResource(ret.id, ret.createdBy, config.MANAGER_ROLE_ID); + } + } // post bus event await helper.postBusEvent(constants.Topics.ChallengeCreated, ret); From c1945c3c0ce3e6d685b467d371a67e6ac693a335 Mon Sep 17 00:00:00 2001 From: Rakib Ansary Date: Fri, 24 Mar 2023 05:15:36 +0600 Subject: [PATCH 07/22] wip --- src/common/challenge-helper.js | 6 ++--- src/common/m2m-helper.js | 8 ++++--- src/services/ChallengeService.js | 38 ++++++++++++++++++-------------- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/common/challenge-helper.js b/src/common/challenge-helper.js index 34ba072f..21e9690e 100644 --- a/src/common/challenge-helper.js +++ b/src/common/challenge-helper.js @@ -46,7 +46,7 @@ class ChallengeHelper { * @param {String} projectId the project id * @param {String} currentUser the user */ - async ensureProjectExist(projectId, currentUser) { + static async ensureProjectExist(projectId, currentUser) { let token = await getM2MToken(); const url = `${config.PROJECTS_API_URL}/${projectId}`; try { @@ -101,13 +101,13 @@ class ChallengeHelper { } async validateChallengeUpdateRequest(currentUser, challenge, data) { - await helper.ensureUserCanModifyChallenge(currentUser, challenge); + // await helper.ensureUserCanModifyChallenge(currentUser, challenge); helper.ensureNoDuplicateOrNullElements(data.tags, "tags"); helper.ensureNoDuplicateOrNullElements(data.groups, "groups"); if (data.projectId) { - await ensureProjectExist(data.projectId, currentUser); + await ChallengeHelper.ensureProjectExist(data.projectId, currentUser); } // check groups access to be updated group values diff --git a/src/common/m2m-helper.js b/src/common/m2m-helper.js index 6620ded6..62c51678 100644 --- a/src/common/m2m-helper.js +++ b/src/common/m2m-helper.js @@ -3,15 +3,17 @@ const config = require("config"); const m2mAuth = require("tc-core-library-js").auth.m2m; class M2MHelper { + static m2m = null; + constructor() { - this.m2m = m2mAuth(_.pick(config, ["AUTH0_URL", "AUTH0_AUDIENCE", "TOKEN_CACHE_TIME"])); + M2MHelper.m2m = m2mAuth(_.pick(config, ["AUTH0_URL", "AUTH0_AUDIENCE", "TOKEN_CACHE_TIME"])); } /** * Get M2M token. * @returns {Promise} the M2M token */ - async getM2MToken() { - return this.m2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET); + getM2MToken() { + return M2MHelper.m2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET); } } diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index 87fd9e80..9aed9820 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -1124,26 +1124,26 @@ async function createChallenge(currentUser, challenge, userToken) { enrichChallengeForResponse(ret, track, type); // Create in ES - await esClient.create({ - index: config.get("ES.ES_INDEX"), - type: config.get("ES.OPENSEARCH") == "false" ? config.get("ES.ES_TYPE") : undefined, - refresh: config.get("ES.ES_REFRESH"), - id: ret.id, - body: ret, - }); + // await esClient.create({ + // index: config.get("ES.ES_INDEX"), + // type: config.get("ES.OPENSEARCH") == "false" ? config.get("ES.ES_TYPE") : undefined, + // refresh: config.get("ES.ES_REFRESH"), + // id: ret.id, + // body: ret, + // }); // If the challenge is self-service, add the creating user as the "client manager", *not* the manager // This is necessary for proper handling of the vanilla embed on the self-service work item dashboard - if (challenge.legacy.selfService) { - if (currentUser.handle) { - await helper.createResource(ret.id, ret.createdBy, config.CLIENT_MANAGER_ROLE_ID); - } - } else { - if (currentUser.handle) { - await helper.createResource(ret.id, ret.createdBy, config.MANAGER_ROLE_ID); - } - } + // if (challenge.legacy.selfService) { + // if (currentUser.handle) { + // await helper.createResource(ret.id, ret.createdBy, config.CLIENT_MANAGER_ROLE_ID); + // } + // } else { + // if (currentUser.handle) { + // await helper.createResource(ret.id, ret.createdBy, config.MANAGER_ROLE_ID); + // } + // } // post bus event await helper.postBusEvent(constants.Topics.ChallengeCreated, ret); @@ -1739,7 +1739,8 @@ async function update(currentUser, challengeId, data) { } if (data.winners && data.winners.length && data.winners.length > 0) { - await validateWinners(data.winners, challengeId); + console.log("Request to validate winners", data.winners, challengeId); + // await validateWinners(data.winners, challengeId); } // Only m2m tokens are allowed to modify the `task.*` information on a challenge @@ -1797,6 +1798,8 @@ async function update(currentUser, challengeId, data) { }); if (_.get(type, "isTask")) { + console.log("Type is task - how come", challengeId, type, track); + /* if (!_.isEmpty(_.get(data, "task.memberId"))) { const challengeResources = await helper.getChallengeResources(challengeId); const registrants = _.filter( @@ -1817,6 +1820,7 @@ async function update(currentUser, challengeId, data) { ); } } + */ } if (!_.isUndefined(data.terms)) { From 5e4b07f1faa3159790ac9fd13d7cde86bbe48212 Mon Sep 17 00:00:00 2001 From: Rakib Ansary Date: Fri, 24 Mar 2023 06:47:38 +0600 Subject: [PATCH 08/22] feat: remove attributes that match exactly with saved challenge Signed-off-by: Rakib Ansary --- package.json | 1 + src/controllers/ChallengeController.js | 69 +--- src/routes.js | 6 +- src/services/ChallengeService.js | 480 +++++++++---------------- yarn.lock | 97 ++++- 5 files changed, 272 insertions(+), 381 deletions(-) diff --git a/package.json b/package.json index 627ab6bd..f1f547ec 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "body-parser": "^1.15.1", "config": "^3.0.1", "cors": "^2.7.1", + "deep-equal": "^2.2.0", "dotenv": "^8.2.0", "dynamoose": "^1.11.1", "elasticsearch": "^16.7.3", diff --git a/src/controllers/ChallengeController.js b/src/controllers/ChallengeController.js index 8a3aed47..a956743e 100644 --- a/src/controllers/ChallengeController.js +++ b/src/controllers/ChallengeController.js @@ -48,15 +48,9 @@ async function searchChallenges(req, res) { */ async function createChallenge(req, res) { logger.debug( - `createChallenge User: ${JSON.stringify( - req.authUser - )} - Body: ${JSON.stringify(req.body)}` - ); - const result = await service.createChallenge( - req.authUser, - req.body, - req.userToken + `createChallenge User: ${JSON.stringify(req.authUser)} - Body: ${JSON.stringify(req.body)}` ); + const result = await service.createChallenge(req.authUser, req.body, req.userToken); res.status(HttpStatus.CREATED).send(result); } @@ -66,10 +60,7 @@ async function createChallenge(req, res) { * @param {Object} res the response */ async function sendNotifications(req, res) { - const result = await service.sendNotifications( - req.authUser, - req.params.challengeId - ); + const result = await service.sendNotifications(req.authUser, req.params.challengeId); res.status(HttpStatus.CREATED).send(result); } @@ -93,31 +84,7 @@ async function getChallenge(req, res) { * @param {Object} res the response */ async function getChallengeStatistics(req, res) { - const result = await service.getChallengeStatistics( - req.authUser, - req.params.challengeId - ); - res.send(result); -} - -/** - * Fully update challenge - * @param {Object} req the request - * @param {Object} res the response - */ -async function fullyUpdateChallenge(req, res) { - logger.debug( - `fullyUpdateChallenge User: ${JSON.stringify( - req.authUser - )} - ChallengeID: ${req.params.challengeId} - Body: ${JSON.stringify( - req.body - )}` - ); - const result = await service.fullyUpdateChallenge( - req.authUser, - req.params.challengeId, - req.body - ); + const result = await service.getChallengeStatistics(req.authUser, req.params.challengeId); res.send(result); } @@ -126,19 +93,13 @@ async function fullyUpdateChallenge(req, res) { * @param {Object} req the request * @param {Object} res the response */ -async function partiallyUpdateChallenge(req, res) { +async function updateChallenge(req, res) { logger.debug( - `partiallyUpdateChallenge User: ${JSON.stringify( - req.authUser - )} - ChallengeID: ${req.params.challengeId} - Body: ${JSON.stringify( - req.body - )}` - ); - const result = await service.partiallyUpdateChallenge( - req.authUser, - req.params.challengeId, - req.body + `updateChallenge User: ${JSON.stringify(req.authUser)} - ChallengeID: ${ + req.params.challengeId + } - Body: ${JSON.stringify(req.body)}` ); + const result = await service.updateChallenge(req.authUser, req.params.challengeId, req.body); res.send(result); } @@ -149,14 +110,9 @@ async function partiallyUpdateChallenge(req, res) { */ async function deleteChallenge(req, res) { logger.debug( - `deleteChallenge User: ${JSON.stringify(req.authUser)} - ChallengeID: ${ - req.params.challengeId - }` - ); - const result = await service.deleteChallenge( - req.authUser, - req.params.challengeId + `deleteChallenge User: ${JSON.stringify(req.authUser)} - ChallengeID: ${req.params.challengeId}` ); + const result = await service.deleteChallenge(req.authUser, req.params.challengeId); res.send(result); } @@ -164,8 +120,7 @@ module.exports = { searchChallenges, createChallenge, getChallenge, - fullyUpdateChallenge, - partiallyUpdateChallenge, + updateChallenge, deleteChallenge, getChallengeStatistics, sendNotifications, diff --git a/src/routes.js b/src/routes.js index 45085fbe..407b7a41 100644 --- a/src/routes.js +++ b/src/routes.js @@ -55,7 +55,7 @@ module.exports = { }, put: { controller: "ChallengeController", - method: "fullyUpdateChallenge", + method: "updateChallenge", auth: "jwt", access: [ constants.UserRoles.Admin, @@ -68,12 +68,12 @@ module.exports = { }, patch: { controller: "ChallengeController", - method: "partiallyUpdateChallenge", + method: "updateChallenge", auth: "jwt", access: [ constants.UserRoles.Admin, - constants.UserRoles.Copilot, constants.UserRoles.SelfServiceCustomer, + constants.UserRoles.Copilot, constants.UserRoles.Manager, constants.UserRoles.User, ], diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index 9aed9820..a292fb9f 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -40,6 +40,7 @@ const { validateChallengeUpdateRequest, enrichChallengeForResponse, } = require("../common/challenge-helper"); +const deepEqual = require("deep-equal"); const challengeDomain = new ChallengeDomain(GRPC_CHALLENGE_SERVER_HOST, GRPC_CHALLENGE_SERVER_PORT); @@ -1487,9 +1488,12 @@ async function validateWinners(winners, challengeId) { * @param {Boolean} isFull the flag indicate it is a fully update operation. * @returns {Object} the updated challenge */ -async function update(currentUser, challengeId, data) { +async function updateChallenge(currentUser, challengeId, data) { const challenge = await challengeDomain.lookup(getLookupCriteria("id", challengeId)); + // Remove fields from data that are not allowed to be updated and that match the existing challenge + data = sanitizeData(sanitizeChallenge(data), challenge); + validateChallengeUpdateRequest(currentUser, challenge, data); const projectId = _.get(challenge, "projectId"); @@ -1545,7 +1549,7 @@ async function update(currentUser, challengeId, data) { sendActivationEmail = data.status === constants.challengeStatuses.Active; } catch (e) { - await update( + await updateChallenge( currentUser, challengeId, { @@ -1740,6 +1744,7 @@ async function update(currentUser, challengeId, data) { if (data.winners && data.winners.length && data.winners.length > 0) { console.log("Request to validate winners", data.winners, challengeId); + // TODO: PUT THIS BACK BEFORE DEPLOYMENT // await validateWinners(data.winners, challengeId); } @@ -1798,8 +1803,6 @@ async function update(currentUser, challengeId, data) { }); if (_.get(type, "isTask")) { - console.log("Type is task - how come", challengeId, type, track); - /* if (!_.isEmpty(_.get(data, "task.memberId"))) { const challengeResources = await helper.getChallengeResources(challengeId); const registrants = _.filter( @@ -1820,7 +1823,6 @@ async function update(currentUser, challengeId, data) { ); } } - */ } if (!_.isUndefined(data.terms)) { @@ -1829,14 +1831,10 @@ async function update(currentUser, challengeId, data) { if (data.phases && data.phases.length > 0) { await getPhasesAndPopulate(data); - } - // Populate challenge.track and challenge.type based on the track/type IDs - if (track) { - data.track = track.name; - } - if (type) { - data.type = type.name; + if (deepEqual(data.phases, challenge.phases)) { + delete data.phases; + } } try { @@ -1938,310 +1936,7 @@ async function update(currentUser, challengeId, data) { return challenge; } -/** - * Send notifications - * @param {Object} currentUser the current use - * @param {String} challengeId the challenge id - */ -async function sendNotifications(currentUser, challengeId) { - const challenge = await getChallenge(currentUser, challengeId); - const creator = await helper.getMemberByHandle(challenge.createdBy); - if (challenge.status === constants.challengeStatuses.Completed) { - await helper.sendSelfServiceNotification( - constants.SelfServiceNotificationTypes.WORK_COMPLETED, - [{ email: creator.email }], - { - handle: creator.handle, - workItemName: challenge.name, - workItemUrl: `${config.SELF_SERVICE_APP_URL}/work-items/${challenge.id}?tab=solutions`, - } - ); - return { type: constants.SelfServiceNotificationTypes.WORK_COMPLETED }; - } -} - -sendNotifications.schema = { - currentUser: Joi.any(), - challengeId: Joi.id(), -}; - -/** - * Remove unwanted properties from the challenge object - * @param {Object} challenge the challenge object - */ -function sanitizeChallenge(challenge) { - const sanitized = _.pick(challenge, [ - "trackId", - "typeId", - "name", - "description", - "privateDescription", - "descriptionFormat", - "timelineTemplateId", - "tags", - "projectId", - "legacyId", - "startDate", - "status", - "task", - "groups", - "cancelReason", - ]); - if (!_.isUndefined(sanitized.name)) { - sanitized.name = xss(sanitized.name); - } - if (!_.isUndefined(sanitized.description)) { - sanitized.description = xss(sanitized.description); - } - if (challenge.legacy) { - sanitized.legacy = _.pick(challenge.legacy, [ - "track", - "subTrack", - "reviewType", - "confidentialityType", - "forumId", - "directProjectId", - "screeningScorecardId", - "reviewScorecardId", - "isTask", - "useSchedulingAPI", - "pureV5Task", - "pureV5", - "selfService", - "selfServiceCopilot", - ]); - } - if (challenge.billing) { - sanitized.billing = _.pick(challenge.billing, ["billingAccountId", "markup"]); - } - if (challenge.metadata) { - sanitized.metadata = _.map(challenge.metadata, (meta) => _.pick(meta, ["name", "value"])); - } - if (challenge.phases) { - sanitized.phases = _.map(challenge.phases, (phase) => - _.pick(phase, [ - "phaseId", - "duration", - "isOpen", - "actualEndDate", - "scheduledStartDate", - "constraints", - ]) - ); - } - if (challenge.prizeSets) { - sanitized.prizeSets = _.map(challenge.prizeSets, (prizeSet) => ({ - ..._.pick(prizeSet, ["type", "description"]), - prizes: _.map(prizeSet.prizes, (prize) => _.pick(prize, ["description", "type", "value"])), - })); - } - if (challenge.events) { - sanitized.events = _.map(challenge.events, (event) => _.pick(event, ["id", "name", "key"])); - } - if (challenge.winners) { - sanitized.winners = _.map(challenge.winners, (winner) => - _.pick(winner, ["userId", "handle", "placement", "type"]) - ); - } - if (challenge.discussions) { - sanitized.discussions = _.map(challenge.discussions, (discussion) => ({ - ..._.pick(discussion, ["id", "provider", "name", "type", "url", "options"]), - name: _.get(discussion, "name", "").substring(0, config.FORUM_TITLE_LENGTH_LIMIT), - })); - } - if (challenge.terms) { - sanitized.terms = _.map(challenge.terms, (term) => _.pick(term, ["id", "roleId"])); - } - if (challenge.attachments) { - sanitized.attachments = _.map(challenge.attachments, (attachment) => - _.pick(attachment, ["id", "name", "url", "fileSize", "description", "challengeId"]) - ); - } - return sanitized; -} - -/** - * Fully update challenge. - * @param {Object} currentUser the user who perform operation - * @param {String} challengeId the challenge id - * @param {Object} data the challenge data to be updated - * @returns {Object} the updated challenge - */ -async function fullyUpdateChallenge(currentUser, challengeId, data) { - return update(currentUser, challengeId, sanitizeChallenge(data), true); -} - -fullyUpdateChallenge.schema = { - currentUser: Joi.any(), - challengeId: Joi.id(), - data: Joi.object() - .keys({ - legacy: Joi.object() - .keys({ - reviewType: Joi.string() - .valid(_.values(constants.reviewTypes)) - .insensitive() - .default(constants.reviewTypes.Internal), - confidentialityType: Joi.string().default(config.DEFAULT_CONFIDENTIALITY_TYPE), - forumId: Joi.number().integer(), - directProjectId: Joi.number().integer(), - screeningScorecardId: Joi.number().integer(), - reviewScorecardId: Joi.number().integer(), - isTask: Joi.boolean(), - useSchedulingAPI: Joi.boolean(), - pureV5Task: Joi.boolean(), - pureV5: Joi.boolean(), - selfService: Joi.boolean(), - selfServiceCopilot: Joi.string().allow(null), - }) - .unknown(true), - cancelReason: Joi.string(), - billing: Joi.object() - .keys({ - billingAccountId: Joi.string(), - markup: Joi.number().min(0).max(100), - }) - .unknown(true), - task: Joi.object().keys({ - isTask: Joi.boolean().default(false), - isAssigned: Joi.boolean().default(false), - memberId: Joi.string().allow(null), - }), - trackId: Joi.optionalId(), - typeId: Joi.optionalId(), - name: Joi.string().required(), - description: Joi.string(), - privateDescription: Joi.string(), - descriptionFormat: Joi.string(), - metadata: Joi.array() - .items( - Joi.object() - .keys({ - name: Joi.string().required(), - value: Joi.required(), - }) - .unknown(true) - ) - .unique((a, b) => a.name === b.name), - timelineTemplateId: Joi.string(), // Joi.optionalId(), - phases: Joi.array().items( - Joi.object() - .keys({ - phaseId: Joi.id(), - duration: Joi.number().integer().min(0), - isOpen: Joi.boolean(), - actualEndDate: Joi.date().allow(null), - scheduledStartDate: Joi.date().allow(null), - constraints: Joi.array() - .items( - Joi.object() - .keys({ - name: Joi.string(), - value: Joi.number().integer().min(0), - }) - .optional() - ) - .optional(), - }) - .unknown(true) - ), - prizeSets: Joi.array().items( - Joi.object() - .keys({ - type: Joi.string().valid(_.values(constants.prizeSetTypes)).required(), - description: Joi.string(), - prizes: Joi.array() - .items( - Joi.object().keys({ - description: Joi.string(), - type: Joi.string().required(), - value: Joi.number().min(0).required(), - }) - ) - .min(1) - .required(), - }) - .unknown(true) - ), - events: Joi.array().items( - Joi.object() - .keys({ - id: Joi.number().required(), - name: Joi.string(), - key: Joi.string(), - }) - .unknown(true) - ), - discussions: Joi.array().items( - Joi.object().keys({ - id: Joi.optionalId(), - name: Joi.string().required(), - type: Joi.string().required().valid(_.values(constants.DiscussionTypes)), - provider: Joi.string().required(), - url: Joi.string(), - options: Joi.array().items(Joi.object()), - }) - ), - tags: Joi.array().items(Joi.string()), // tag names - projectId: Joi.number().integer().positive().required(), - legacyId: Joi.number().integer().positive(), - startDate: Joi.date(), - status: Joi.string().valid(_.values(constants.challengeStatuses)).required(), - attachments: Joi.array().items( - Joi.object().keys({ - id: Joi.id(), - challengeId: Joi.id(), - name: Joi.string().required(), - url: Joi.string().uri().required(), - fileSize: Joi.fileSize(), - description: Joi.string(), - }) - ), - groups: Joi.array().items(Joi.optionalId()), - // gitRepoURLs: Joi.array().items(Joi.string().uri()), - winners: Joi.array() - .items( - Joi.object() - .keys({ - userId: Joi.number().integer().positive().required(), - handle: Joi.string().required(), - placement: Joi.number().integer().positive().required(), - type: Joi.string() - .valid(_.values(constants.prizeSetTypes)) - .default(constants.prizeSetTypes.ChallengePrizes), - }) - .unknown(true) - ) - .min(1), - terms: Joi.array() - .items( - Joi.object() - .keys({ - id: Joi.id(), - roleId: Joi.id(), - }) - .unknown(true) - ) - .optional() - .allow([]), - overview: Joi.any().forbidden(), - }) - .unknown(true) - .required(), -}; - -/** - * Partially update challenge. - * @param {Object} currentUser the user who perform operation - * @param {String} challengeId the challenge id - * @param {Object} data the challenge data to be updated - * @returns {Object} the updated challenge - */ -async function partiallyUpdateChallenge(currentUser, challengeId, data) { - return update(currentUser, challengeId, sanitizeChallenge(data)); -} - -partiallyUpdateChallenge.schema = { +updateChallenge.schema = { currentUser: Joi.any(), challengeId: Joi.id(), data: Joi.object() @@ -2394,6 +2089,156 @@ partiallyUpdateChallenge.schema = { .required(), }; +/** + * Send notifications + * @param {Object} currentUser the current use + * @param {String} challengeId the challenge id + */ +async function sendNotifications(currentUser, challengeId) { + const challenge = await getChallenge(currentUser, challengeId); + const creator = await helper.getMemberByHandle(challenge.createdBy); + if (challenge.status === constants.challengeStatuses.Completed) { + await helper.sendSelfServiceNotification( + constants.SelfServiceNotificationTypes.WORK_COMPLETED, + [{ email: creator.email }], + { + handle: creator.handle, + workItemName: challenge.name, + workItemUrl: `${config.SELF_SERVICE_APP_URL}/work-items/${challenge.id}?tab=solutions`, + } + ); + return { type: constants.SelfServiceNotificationTypes.WORK_COMPLETED }; + } +} + +sendNotifications.schema = { + currentUser: Joi.any(), + challengeId: Joi.id(), +}; + +/** + * Remove unwanted properties from the challenge object + * @param {Object} challenge the challenge object + */ +function sanitizeChallenge(challenge) { + const sanitized = _.pick(challenge, [ + "trackId", + "typeId", + "name", + "description", + "privateDescription", + "descriptionFormat", + "timelineTemplateId", + "tags", + "projectId", + "legacyId", + "startDate", + "status", + "task", + "groups", + "cancelReason", + ]); + if (!_.isUndefined(sanitized.name)) { + sanitized.name = xss(sanitized.name); + } + if (!_.isUndefined(sanitized.description)) { + sanitized.description = xss(sanitized.description); + } + if (challenge.legacy) { + sanitized.legacy = _.pick(challenge.legacy, [ + "track", + "subTrack", + "reviewType", + "confidentialityType", + "forumId", + "directProjectId", + "screeningScorecardId", + "reviewScorecardId", + "isTask", + "useSchedulingAPI", + "pureV5Task", + "pureV5", + "selfService", + "selfServiceCopilot", + ]); + } + if (challenge.billing) { + sanitized.billing = _.pick(challenge.billing, ["billingAccountId", "markup"]); + } + if (challenge.metadata) { + sanitized.metadata = _.map(challenge.metadata, (meta) => _.pick(meta, ["name", "value"])); + } + if (challenge.phases) { + sanitized.phases = _.map(challenge.phases, (phase) => + _.pick(phase, [ + "phaseId", + "duration", + "isOpen", + "actualEndDate", + "scheduledStartDate", + "constraints", + ]) + ); + } + if (challenge.prizeSets) { + sanitized.prizeSets = _.map(challenge.prizeSets, (prizeSet) => ({ + ..._.pick(prizeSet, ["type", "description"]), + prizes: _.map(prizeSet.prizes, (prize) => _.pick(prize, ["description", "type", "value"])), + })); + } + if (challenge.events) { + sanitized.events = _.map(challenge.events, (event) => _.pick(event, ["id", "name", "key"])); + } + if (challenge.winners) { + sanitized.winners = _.map(challenge.winners, (winner) => + _.pick(winner, ["userId", "handle", "placement", "type"]) + ); + } + if (challenge.discussions) { + sanitized.discussions = _.map(challenge.discussions, (discussion) => ({ + ..._.pick(discussion, ["id", "provider", "name", "type", "url", "options"]), + name: _.get(discussion, "name", "").substring(0, config.FORUM_TITLE_LENGTH_LIMIT), + })); + } + if (challenge.terms) { + sanitized.terms = _.map(challenge.terms, (term) => _.pick(term, ["id", "roleId"])); + } + if (challenge.attachments) { + sanitized.attachments = _.map(challenge.attachments, (attachment) => + _.pick(attachment, ["id", "name", "url", "fileSize", "description", "challengeId"]) + ); + } + return sanitized; +} +/* + metadata = [ { }, { }, { }] -> in database + metadata = [ {} ] + + final state: [ {} ] +*/ + +function sanitizeData(data, challenge) { + for (const key in data) { + if (key === "phases") continue; + + if (challenge.hasOwnProperty(key)) { + if ( + (typeof data[key] === "object" || Array.isArray(data[key])) && + deepEqual(data[key], challenge[key]) + ) { + delete data[key]; + } else if ( + typeof data[key] !== "object" && + !Array.isArray(data[key]) && + data[key] === challenge[key] + ) { + delete data[key]; + } + } + } + return data; +} + /** * Delete challenge. * @param {Object} currentUser the user who perform operation @@ -2434,8 +2279,7 @@ module.exports = { searchChallenges, createChallenge, getChallenge, - fullyUpdateChallenge, - partiallyUpdateChallenge, + updateChallenge, deleteChallenge, getChallengeStatistics, sendNotifications, diff --git a/yarn.lock b/yarn.lock index ecc5a0a3..ba4797dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1106,6 +1106,29 @@ deep-equal@1.0.1: resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" integrity sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw== +deep-equal@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.0.tgz#5caeace9c781028b9ff459f33b779346637c43e6" + integrity sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw== + dependencies: + call-bind "^1.0.2" + es-get-iterator "^1.1.2" + get-intrinsic "^1.1.3" + is-arguments "^1.1.1" + is-array-buffer "^3.0.1" + is-date-object "^1.0.5" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + isarray "^2.0.5" + object-is "^1.1.5" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.4.3" + side-channel "^1.0.4" + which-boxed-primitive "^1.0.2" + which-collection "^1.0.1" + which-typed-array "^1.1.9" + default-require-extensions@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-2.0.0.tgz#f5f8fbb18a7d6d50b21f641f649ebb522cfe24f7" @@ -1267,6 +1290,21 @@ es-array-method-boxes-properly@^1.0.0: resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== +es-get-iterator@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" + integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + has-symbols "^1.0.3" + is-arguments "^1.1.1" + is-map "^2.0.2" + is-set "^2.0.2" + is-string "^1.0.7" + isarray "^2.0.5" + stop-iteration-iterator "^1.0.0" + es-set-tostringtag@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" @@ -1861,7 +1899,7 @@ inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@~2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -internal-slot@^1.0.5: +internal-slot@^1.0.4, internal-slot@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986" integrity sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ== @@ -1880,7 +1918,7 @@ ipaddr.js@1.9.1: resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== -is-arguments@^1.0.4: +is-arguments@^1.0.4, is-arguments@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== @@ -1946,7 +1984,7 @@ is-core-module@^2.9.0: dependencies: has "^1.0.3" -is-date-object@^1.0.1: +is-date-object@^1.0.1, is-date-object@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== @@ -1989,6 +2027,11 @@ is-ip@^2.0.0: dependencies: ip-regex "^2.0.0" +is-map@^2.0.1, is-map@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" + integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== + is-negative-zero@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" @@ -2019,6 +2062,11 @@ is-retry-allowed@^2.2.0: resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz#88f34cbd236e043e71b6932d09b0c65fb7b4d71d" integrity sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg== +is-set@^2.0.1, is-set@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec" + integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g== + is-shared-array-buffer@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" @@ -2066,6 +2114,11 @@ is-typedarray@~1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== +is-weakmap@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" + integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== + is-weakref@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" @@ -2073,11 +2126,24 @@ is-weakref@^1.0.2: dependencies: call-bind "^1.0.2" +is-weakset@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.2.tgz#4569d67a747a1ce5a994dfd4ef6dcea76e7c0a1d" + integrity sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + isemail@3.x.x: version "3.2.0" resolved "https://registry.yarnpkg.com/isemail/-/isemail-3.2.0.tgz#59310a021931a9fb06bbb51e155ce0b3f236832c" @@ -2710,6 +2776,14 @@ object-inspect@^1.12.3, object-inspect@^1.9.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== +object-is@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" + integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + object-keys@^1.0.11, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -3373,6 +3447,13 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== +stop-iteration-iterator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" + integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== + dependencies: + internal-slot "^1.0.4" + streamsearch@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" @@ -3770,6 +3851,16 @@ which-boxed-primitive@^1.0.2: is-string "^1.0.5" is-symbol "^1.0.3" +which-collection@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906" + integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A== + dependencies: + is-map "^2.0.1" + is-set "^2.0.1" + is-weakmap "^2.0.1" + is-weakset "^2.0.1" + which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" From e70465e7b019fa14d8a686cd20dd6b5d31d29f12 Mon Sep 17 00:00:00 2001 From: Rakib Ansary Date: Fri, 24 Mar 2023 13:06:18 +0600 Subject: [PATCH 09/22] refactor: challenge update --- package.json | 4 +- src/common/challenge-helper.js | 82 ++++++++++++++++++++++++- src/services/ChallengeService.js | 102 ++++++++++++++++--------------- yarn.lock | 76 +++++++++++------------ 4 files changed, 174 insertions(+), 90 deletions(-) diff --git a/package.json b/package.json index f1f547ec..5a10472f 100644 --- a/package.json +++ b/package.json @@ -42,8 +42,8 @@ "dependencies": { "@grpc/grpc-js": "^1.8.12", "@opensearch-project/opensearch": "^2.2.0", - "@topcoder-framework/domain-challenge": "^0.8.0", - "@topcoder-framework/lib-common": "^0.8.0", + "@topcoder-framework/domain-challenge": "^0.9.1", + "@topcoder-framework/lib-common": "^0.9.1", "aws-sdk": "^2.1145.0", "axios": "^0.19.0", "axios-retry": "^3.4.0", diff --git a/src/common/challenge-helper.js b/src/common/challenge-helper.js index 21e9690e..5009169f 100644 --- a/src/common/challenge-helper.js +++ b/src/common/challenge-helper.js @@ -101,7 +101,7 @@ class ChallengeHelper { } async validateChallengeUpdateRequest(currentUser, challenge, data) { - // await helper.ensureUserCanModifyChallenge(currentUser, challenge); + await helper.ensureUserCanModifyChallenge(currentUser, challenge); helper.ensureNoDuplicateOrNullElements(data.tags, "tags"); helper.ensureNoDuplicateOrNullElements(data.groups, "groups"); @@ -188,6 +188,86 @@ class ChallengeHelper { } } + sanitizeRepeatedFieldsInUpdateRequest(data) { + if (data.winners != null) { + data.winnerUpdate = { + winners: [ + { + handle: "ansary", + placement: 1, + userId: 123, + }, + ], // data.winners, + }; + delete data.winners; + } + + if (data.discussions != null) { + data.discussionUpdate = { + discussions: data.discussions, + }; + delete data.discussions; + } + + if (data.metadata != null) { + data.metadataUpdate = { + metadata: data.metadata, + }; + delete data.metadata; + } + + if (data.phases != null) { + data.phaseUpdate = { + phases: data.phases, + }; + delete data.phases; + } + + if (data.events != null) { + data.eventUpdate = { + events: data.events, + }; + delete data.events; + } + + if (data.terms != null) { + data.termUpdate = { + terms: data.terms, + }; + delete data.terms; + } + + if (data.prizeSets != null) { + data.prizeSetUpdate = { + prizeSets: data.prizeSets, + }; + delete data.prizeSets; + } + + if (data.tags != null) { + data.tagUpdate = { + tags: data.tags, + }; + delete data.tags; + } + + if (data.attachments != null) { + data.attachmentUpdate = { + attachments: data.attachments, + }; + delete data.attachments; + } + + if (data.groups != null) { + data.groupUpdate = { + groups: data.groups, + }; + delete data.groups; + } + + return data; + } + enrichChallengeForResponse(challenge, track, type) { if (challenge.phases && challenge.phases.length > 0) { const registrationPhase = _.find(challenge.phases, (p) => p.name === "Registration"); diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index a292fb9f..0ac1caeb 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -34,11 +34,12 @@ const { Metadata: GrpcMetadata } = require("@grpc/grpc-js"); const esClient = helper.getESClient(); -const { ChallengeDomain } = require("@topcoder-framework/domain-challenge"); +const { ChallengeDomain, UpdateChallengeInput } = require("@topcoder-framework/domain-challenge"); const { hasAdminRole } = require("../common/role-helper"); const { validateChallengeUpdateRequest, enrichChallengeForResponse, + sanitizeRepeatedFieldsInUpdateRequest, } = require("../common/challenge-helper"); const deepEqual = require("deep-equal"); @@ -1125,26 +1126,26 @@ async function createChallenge(currentUser, challenge, userToken) { enrichChallengeForResponse(ret, track, type); // Create in ES - // await esClient.create({ - // index: config.get("ES.ES_INDEX"), - // type: config.get("ES.OPENSEARCH") == "false" ? config.get("ES.ES_TYPE") : undefined, - // refresh: config.get("ES.ES_REFRESH"), - // id: ret.id, - // body: ret, - // }); + await esClient.create({ + index: config.get("ES.ES_INDEX"), + type: config.get("ES.OPENSEARCH") == "false" ? config.get("ES.ES_TYPE") : undefined, + refresh: config.get("ES.ES_REFRESH"), + id: ret.id, + body: ret, + }); // If the challenge is self-service, add the creating user as the "client manager", *not* the manager // This is necessary for proper handling of the vanilla embed on the self-service work item dashboard - // if (challenge.legacy.selfService) { - // if (currentUser.handle) { - // await helper.createResource(ret.id, ret.createdBy, config.CLIENT_MANAGER_ROLE_ID); - // } - // } else { - // if (currentUser.handle) { - // await helper.createResource(ret.id, ret.createdBy, config.MANAGER_ROLE_ID); - // } - // } + if (challenge.legacy.selfService) { + if (currentUser.handle) { + await helper.createResource(ret.id, ret.createdBy, config.CLIENT_MANAGER_ROLE_ID); + } + } else { + if (currentUser.handle) { + await helper.createResource(ret.id, ret.createdBy, config.MANAGER_ROLE_ID); + } + } // post bus event await helper.postBusEvent(constants.Topics.ChallengeCreated, ret); @@ -1744,8 +1745,7 @@ async function updateChallenge(currentUser, challengeId, data) { if (data.winners && data.winners.length && data.winners.length > 0) { console.log("Request to validate winners", data.winners, challengeId); - // TODO: PUT THIS BACK BEFORE DEPLOYMENT - // await validateWinners(data.winners, challengeId); + await validateWinners(data.winners, challengeId); } // Only m2m tokens are allowed to modify the `task.*` information on a challenge @@ -1842,59 +1842,63 @@ async function updateChallenge(currentUser, challengeId, data) { `ChallengeDomain.update id: ${challengeId} Details: ${JSON.stringify(data, null, 2)}` ); - const grpcMetadata = new GrpcMetadata(); + const updateInput = sanitizeRepeatedFieldsInUpdateRequest(data); - grpcMetadata.set("handle", currentUser.handle); - grpcMetadata.set("userId", currentUser.userId); + if (!_.isEmpty(updateInput)) { + const grpcMetadata = new GrpcMetadata(); - const { items } = await challengeDomain.update( - { - filterCriteria: getScanCriteria({ - id: challengeId, - }), - updateInput: { - ...data, - }, - }, - grpcMetadata - ); + grpcMetadata.set("handle", currentUser.handle); + grpcMetadata.set("userId", currentUser.userId); - console.log("Items", items); + await challengeDomain.update( + { + filterCriteria: getScanCriteria({ id: challengeId }), + updateInput, + }, + grpcMetadata + ); + } } catch (e) { throw e; } - challenge = await challengeDomain.lookup({ id: challengeId }); + const updatedChallenge = await challengeDomain.lookup(getLookupCriteria("id", challengeId)); // post bus event - logger.debug(`Post Bus Event: ${constants.Topics.ChallengeUpdated} ${JSON.stringify(challenge)}`); + logger.debug( + `Post Bus Event: ${constants.Topics.ChallengeUpdated} ${JSON.stringify(updatedChallenge)}` + ); - await helper.postBusEvent(constants.Topics.ChallengeUpdated, challenge, { - key: challenge.status === "Completed" ? `${challenge.id}:${challenge.status}` : undefined, + await helper.postBusEvent(constants.Topics.ChallengeUpdated, updatedChallenge, { + key: + updatedChallenge.status === "Completed" + ? `${updatedChallenge.id}:${updatedChallenge.status}` + : undefined, }); - enrichChallengeForResponse(challenge, track, type); + enrichChallengeForResponse(updatedChallenge, track, type); // Update ES + // TODO: Uncomment await esClient.update({ index: config.get("ES.ES_INDEX"), type: config.get("ES.OPENSEARCH") == "false" ? config.get("ES.ES_TYPE") : undefined, refresh: config.get("ES.ES_REFRESH"), id: challengeId, body: { - doc: challenge, + doc: updatedChallenge, }, }); - if (challenge.legacy.selfService) { - const creator = await helper.getMemberByHandle(challenge.createdBy); + if (updatedChallenge.legacy.selfService) { + const creator = await helper.getMemberByHandle(updatedChallenge.createdBy); if (sendSubmittedEmail) { await helper.sendSelfServiceNotification( constants.SelfServiceNotificationTypes.WORK_REQUEST_SUBMITTED, [{ email: creator.email }], { handle: creator.handle, - workItemName: challenge.name, + workItemName: updatedChallenge.name, } ); } @@ -1904,8 +1908,8 @@ async function updateChallenge(currentUser, challengeId, data) { [{ email: creator.email }], { handle: creator.handle, - workItemName: challenge.name, - workItemUrl: `${config.SELF_SERVICE_APP_URL}/work-items/${challenge.id}`, + workItemName: updatedChallenge.name, + workItemUrl: `${config.SELF_SERVICE_APP_URL}/work-items/${updatedChallenge.id}`, } ); } @@ -1915,8 +1919,8 @@ async function updateChallenge(currentUser, challengeId, data) { [{ email: creator.email }], { handle: creator.handle, - workItemName: challenge.name, - workItemUrl: `${config.SELF_SERVICE_APP_URL}/work-items/${challenge.id}?tab=solutions`, + workItemName: updatedChallenge.name, + workItemUrl: `${config.SELF_SERVICE_APP_URL}/work-items/${updatedChallenge.id}?tab=solutions`, } ); } @@ -1927,13 +1931,13 @@ async function updateChallenge(currentUser, challengeId, data) { [{ email: creator.email }], { handle: creator.handle, - workItemName: challenge.name, + workItemName: updatedChallenge.name, } ); } } - return challenge; + return updatedChallenge; } updateChallenge.schema = { diff --git a/yarn.lock b/yarn.lock index ba4797dd..95d0b2e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -126,17 +126,17 @@ kuler "^2.0.0" "@grpc/grpc-js@^1.8.0", "@grpc/grpc-js@^1.8.12": - version "1.8.12" - resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.8.12.tgz#bc0120859e8b153db764b473cc019ddf6bb2b414" - integrity sha512-MbUMvpVvakeKhdYux6gbSIPJaFMLNSY8jw4PqLI+FFztGrQRrYYAnHlR94+ncBQQewkpXQaW449m3tpH/B/ZnQ== + version "1.8.13" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.8.13.tgz#e775685962909b76f8d4b813833c3d123867165b" + integrity sha512-iY3jsdfbc0ARoCLFvbvUB8optgyb0r1XLPb142u+QtgBcKJYkCIFt3Fd/881KqjLYWjsBJF57N3b8Eop9NDfUA== dependencies: "@grpc/proto-loader" "^0.7.0" "@types/node" ">=12.12.47" "@grpc/proto-loader@^0.7.0": - version "0.7.5" - resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.5.tgz#ee9e7488fa585dc6b0f7fe88cd39723a3e64c906" - integrity sha512-mfcTuMbFowq1wh/Rn5KQl6qb95M21Prej3bewD9dUQMurYGVckGO/Pbe2Ocwto6sD05b/mxZLspvqwx60xO2Rg== + version "0.7.6" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.6.tgz#b71fdf92b184af184b668c4e9395a5ddc23d61de" + integrity sha512-QyAXR8Hyh7uMDmveWxDSUcJr9NAWaZ2I6IXgAYvQmfflwouTM+rArE2eEaCtLlRqO81j7pRLCt81IefUei6Zbw== dependencies: "@types/long" "^4.0.1" lodash.camelcase "^4.3.0" @@ -245,35 +245,35 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== -"@topcoder-framework/client-relational@^0.8.0": - version "0.8.0" - resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/client-relational/-/client-relational-0.8.0.tgz#66725c1b82498297aac911298a2cf1aeaed991c8" - integrity sha512-jSyyqDQuBC5PQ/QOh9amaqNmrnztnDp/zMC7MAhSAUa5mSJdoJcn7t3fTpi6wJ5BHT8hkOOAQbsFb4NkhYNhkw== +"@topcoder-framework/client-relational@^0.9.1": + version "0.9.1" + resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/client-relational/-/client-relational-0.9.1.tgz#6f87c8ddb8d2c19799ca51d220340ab0c0f67bd9" + integrity sha512-42sZAY60oSve8QmLL7eyrmZx7C5YAvbKZt77CiLTfUGBmURxwzlADbJKrCKBw+R0VSmtBxZ6oHdvBnXhSPjPHA== dependencies: "@grpc/grpc-js" "^1.8.0" - "@topcoder-framework/lib-common" "^0.8.0" - topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.31" + "@topcoder-framework/lib-common" "^0.9.1" + topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.33" tslib "^2.4.1" -"@topcoder-framework/domain-challenge@^0.8.0": - version "0.8.0" - resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/domain-challenge/-/domain-challenge-0.8.0.tgz#67074caf11edbf48e9cef29313b3ceb2b21ee757" - integrity sha512-IWgyGBlMOBl48CsmE37Z7dghvtfjcfGyo0PJVpRsYzdlnvHd6nY+tlq/bEKY2c9JasnlMeni/i4rUWeXG8JZyw== +"@topcoder-framework/domain-challenge@^0.9.1": + version "0.9.1" + resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/domain-challenge/-/domain-challenge-0.9.1.tgz#cfec41a1403c6f744a83f21d0ddc9d29a7b23f69" + integrity sha512-m5lcWumHWJUVvB+39ddKH+dn8MOFPHsDpxFdWepD4jw6Vo2GZhJINpp3ZQWLA2UQUAYSaQp8kwOa93wdvzE7eg== dependencies: "@grpc/grpc-js" "^1.8.0" - "@topcoder-framework/client-relational" "^0.8.0" - "@topcoder-framework/lib-common" "^0.8.0" - topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.31" + "@topcoder-framework/client-relational" "^0.9.1" + "@topcoder-framework/lib-common" "^0.9.1" + topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.33" tslib "^2.4.1" -"@topcoder-framework/lib-common@^0.8.0": - version "0.8.0" - resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/lib-common/-/lib-common-0.8.0.tgz#3f29daace074734d74c638e860204011d5e83a21" - integrity sha512-xuyU3kQGiNOPPzwZLXzrTbu6o9FC2SVei7FWBwzL4HraD6aj3fDqME7dappY32QWB/lFxqyTm00SeekmkbCr1w== +"@topcoder-framework/lib-common@^0.9.1": + version "0.9.1" + resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/lib-common/-/lib-common-0.9.1.tgz#59ee523d05a8da4cab7866bd9ba24dd714c713a1" + integrity sha512-4W5Oz3XN6n3dsEZwmSiDxVuQtkuAxPJc9PyivIatyLB5HSAva/OuQbBcCWhFl2ORRywVxEBJz2cWFme+a6vEPw== dependencies: "@grpc/grpc-js" "^1.8.0" rimraf "^3.0.2" - topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.31" + topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.33" tslib "^2.4.1" "@types/body-parser@*": @@ -346,9 +346,9 @@ integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== "@types/node@*", "@types/node@>=12.12.47", "@types/node@>=13.7.0": - version "18.15.3" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.3.tgz#f0b991c32cfc6a4e7f3399d6cb4b8cf9a0315014" - integrity sha512-p6ua9zBxz5otCmbpb5D3U4B5Nanw6Pk3PPyX05xnxbB/fRv71N7CPmORg7uAD5P70T0xmx1pzAx/FUfa5X+3cw== + version "18.15.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.6.tgz#af98ef4a36e7ac5f2d03040f3109fcce972bf6cb" + integrity sha512-YErOafCZpK4g+Rp3Q/PBgZNAsWKGunQTm9FA3/Pbcm0VCriTEzcrutQ/SxSc0rytAp0NoFWue669jmKhEtd0sA== "@types/node@11.11.0": version "11.11.0" @@ -566,9 +566,9 @@ aws-sdk@2.395.0: xml2js "0.4.19" aws-sdk@^2.1145.0: - version "2.1338.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1338.0.tgz#e9272a0563940ceebed5910d271c944f31dbae00" - integrity sha512-apxv53ABuvi87UQHAUqRrJOaGNMiPXAe6bizzJhOnsaNqasg2KjDDit7QSCi6HlLNG44n1ApIvMtR/k+NnxU4Q== + version "2.1342.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1342.0.tgz#2ddb60e7480b6f3a3b1ec5cfba4c6beed7cfc024" + integrity sha512-RknStRPY+ohgOhuuDYEkAWuBcU9841EjtelZn4J2VubhaS7ZFQ2lmiYqm4P5Tw8Kwq6GuUqISBB8RCp8cO2qfA== dependencies: buffer "4.9.2" events "1.1.1" @@ -2693,9 +2693,9 @@ node-environment-flags@1.0.5: semver "^5.7.0" nodemon@^2.0.20: - version "2.0.21" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.21.tgz#267edff25578da91075d6aa54346ef77ecb7b302" - integrity sha512-djN/n2549DUtY33S7o1djRCd7dEm0kBnj9c7S9XVXqRUbuggN1MZH/Nqa+5RFQr63Fbefq37nFXAE9VU86yL1A== + version "2.0.22" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.22.tgz#182c45c3a78da486f673d6c1702e00728daf5258" + integrity sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ== dependencies: chokidar "^3.5.2" debug "^3.2.7" @@ -2957,9 +2957,9 @@ precond@0.2: integrity sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ== prettier@^2.8.1: - version "2.8.4" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.4.tgz#34dd2595629bfbb79d344ac4a91ff948694463c3" - integrity sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw== + version "2.8.6" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.6.tgz#5c174b29befd507f14b83e3c19f83fdc0e974b71" + integrity sha512-mtuzdiBbHwPEgl7NxWlqOkithPyp4VN93V7VeHVWBF+ad3I5avc0RVDT4oImXQy9H/AqxA2NSQH8pSxHW6FYbQ== process-nextick-args@~2.0.0: version "2.0.1" @@ -3677,9 +3677,9 @@ topcoder-bus-api-wrapper@topcoder-platform/tc-bus-api-wrapper.git: superagent "^3.8.3" tc-core-library-js appirio-tech/tc-core-library-js.git#v2.6.4 -"topcoder-interface@github:topcoder-platform/plat-interface-definition#v0.0.31": +"topcoder-interface@github:topcoder-platform/plat-interface-definition#v0.0.33": version "1.0.0" - resolved "https://codeload.github.com/topcoder-platform/plat-interface-definition/tar.gz/8fde0bf4ab310b887383c7b37b6ae919cd407388" + resolved "https://codeload.github.com/topcoder-platform/plat-interface-definition/tar.gz/8d53fb22968e3801d3d77c7c9333b18775eae282" topo@3.x.x: version "3.0.3" From 8782f0e61698bcf1a181c60e46a8ba9bffb1c21c Mon Sep 17 00:00:00 2001 From: Rakib Ansary Date: Fri, 24 Mar 2023 13:48:32 +0600 Subject: [PATCH 10/22] fix: allow memberId to be a number Signed-off-by: Rakib Ansary --- src/services/ChallengeService.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index 0ac1caeb..c46f7d00 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -1826,7 +1826,7 @@ async function updateChallenge(currentUser, challengeId, data) { } if (!_.isUndefined(data.terms)) { - await helper.validateChallengeTerms(data.terms); + await helper.validateChallengeTerms(data.terms.map((t) => t.id)); } if (data.phases && data.phases.length > 0) { @@ -1968,7 +1968,7 @@ updateChallenge.schema = { task: Joi.object().keys({ isTask: Joi.boolean().default(false), isAssigned: Joi.boolean().default(false), - memberId: Joi.string().allow(null), + memberId: Joi.alternatives().try(Joi.string().allow(null), Joi.number().allow(null)), }), billing: Joi.object() .keys({ @@ -2070,7 +2070,7 @@ updateChallenge.schema = { description: Joi.string(), }) ), - groups: Joi.array().items(Joi.id()), // group names + groups: Joi.array().items(Joi.optionalId()).unique(), // gitRepoURLs: Joi.array().items(Joi.string().uri()), winners: Joi.array() .items( @@ -2086,7 +2086,12 @@ updateChallenge.schema = { .unknown(true) ) .min(1), - terms: Joi.array().items(Joi.id().optional()).optional().allow([]), + terms: Joi.array().items( + Joi.object().keys({ + id: Joi.id(), + roleId: Joi.id(), + }) + ), overview: Joi.any().forbidden(), }) .unknown(true) From e3d6dcdde131d285c8d5c1ad5581318a1b3ec196 Mon Sep 17 00:00:00 2001 From: Emre Date: Fri, 24 Mar 2023 10:57:18 +0300 Subject: [PATCH 11/22] fix: getM2MToken reference --- src/common/helper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/helper.js b/src/common/helper.js index 6d672297..e5228f09 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -945,7 +945,7 @@ async function listChallengesByMember(memberId) { * @returns {Promise} an array of resources. */ async function listResourcesByMemberAndChallenge(memberId, challengeId) { - const token = await getM2MToken(); + const token = await m2mHelper.getM2MToken(); let response = {}; try { response = await axios.get(config.RESOURCES_API_URL, { From 669c1b1b89a18ac69f77ee178b6ca29431968986 Mon Sep 17 00:00:00 2001 From: Rakib Ansary Date: Fri, 24 Mar 2023 14:38:37 +0600 Subject: [PATCH 12/22] fix: winner can not be set for non tasks --- src/services/ChallengeService.js | 66 ++++++++++++++++---------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index c46f7d00..eaa81966 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -1869,6 +1869,8 @@ async function updateChallenge(currentUser, challengeId, data) { `Post Bus Event: ${constants.Topics.ChallengeUpdated} ${JSON.stringify(updatedChallenge)}` ); + enrichChallengeForResponse(updatedChallenge, track, type); + await helper.postBusEvent(constants.Topics.ChallengeUpdated, updatedChallenge, { key: updatedChallenge.status === "Completed" @@ -1876,8 +1878,6 @@ async function updateChallenge(currentUser, challengeId, data) { : undefined, }); - enrichChallengeForResponse(updatedChallenge, track, type); - // Update ES // TODO: Uncomment await esClient.update({ @@ -1964,12 +1964,14 @@ updateChallenge.schema = { selfServiceCopilot: Joi.string().allow(null), }) .unknown(true), - cancelReason: Joi.string(), - task: Joi.object().keys({ - isTask: Joi.boolean().default(false), - isAssigned: Joi.boolean().default(false), - memberId: Joi.alternatives().try(Joi.string().allow(null), Joi.number().allow(null)), - }), + cancelReason: Joi.string().optional(), + task: Joi.object() + .keys({ + isTask: Joi.boolean().default(false), + isAssigned: Joi.boolean().default(false), + memberId: Joi.alternatives().try(Joi.string().allow(null), Joi.number().allow(null)), + }) + .optional(), billing: Joi.object() .keys({ billingAccountId: Joi.string(), @@ -1978,10 +1980,10 @@ updateChallenge.schema = { .unknown(true), trackId: Joi.optionalId(), typeId: Joi.optionalId(), - name: Joi.string(), - description: Joi.string(), - privateDescription: Joi.string(), - descriptionFormat: Joi.string(), + name: Joi.string().optional(), + description: Joi.string().optional(), + privateDescription: Joi.string().optional(), + descriptionFormat: Joi.string().optional(), metadata: Joi.array() .items( Joi.object() @@ -1992,7 +1994,7 @@ updateChallenge.schema = { .unknown(true) ) .unique((a, b) => a.name === b.name), - timelineTemplateId: Joi.string(), // changing this to update migrated challenges + timelineTemplateId: Joi.string().optional(), // changing this to update migrated challenges phases: Joi.array() .items( Joi.object() @@ -2015,7 +2017,8 @@ updateChallenge.schema = { }) .unknown(true) ) - .min(1), + .min(1) + .optional(), events: Joi.array().items( Joi.object() .keys({ @@ -2024,17 +2027,20 @@ updateChallenge.schema = { key: Joi.string(), }) .unknown(true) + .optional() ), - discussions: Joi.array().items( - Joi.object().keys({ - id: Joi.optionalId(), - name: Joi.string().required(), - type: Joi.string().required().valid(_.values(constants.DiscussionTypes)), - provider: Joi.string().required(), - url: Joi.string(), - options: Joi.array().items(Joi.object()), - }) - ), + discussions: Joi.array() + .items( + Joi.object().keys({ + id: Joi.optionalId(), + name: Joi.string().required(), + type: Joi.string().required().valid(_.values(constants.DiscussionTypes)), + provider: Joi.string().required(), + url: Joi.string(), + options: Joi.array().items(Joi.object()), + }) + ) + .optional(), startDate: Joi.date(), prizeSets: Joi.array() .items( @@ -2085,7 +2091,7 @@ updateChallenge.schema = { }) .unknown(true) ) - .min(1), + .optional(), terms: Joi.array().items( Joi.object().keys({ id: Joi.id(), @@ -2198,10 +2204,12 @@ function sanitizeChallenge(challenge) { if (challenge.events) { sanitized.events = _.map(challenge.events, (event) => _.pick(event, ["id", "name", "key"])); } - if (challenge.winners) { + if (challenge.legacy != null && challenge.legacy.pureV5Task === true && challenge.winners) { sanitized.winners = _.map(challenge.winners, (winner) => _.pick(winner, ["userId", "handle", "placement", "type"]) ); + } else { + delete challenge.winners; } if (challenge.discussions) { sanitized.discussions = _.map(challenge.discussions, (discussion) => ({ @@ -2219,12 +2227,6 @@ function sanitizeChallenge(challenge) { } return sanitized; } -/* - metadata = [ { }, { }, { }] -> in database - metadata = [ {} ] - - final state: [ {} ] -*/ function sanitizeData(data, challenge) { for (const key in data) { From 243b6e8e1efbe40b4e601ea94111048df4cfa8f2 Mon Sep 17 00:00:00 2001 From: Rakib Ansary Date: Fri, 24 Mar 2023 14:57:42 +0600 Subject: [PATCH 13/22] fix: remove hardcoded debug code --- src/common/challenge-helper.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/common/challenge-helper.js b/src/common/challenge-helper.js index 5009169f..3a85035b 100644 --- a/src/common/challenge-helper.js +++ b/src/common/challenge-helper.js @@ -191,13 +191,7 @@ class ChallengeHelper { sanitizeRepeatedFieldsInUpdateRequest(data) { if (data.winners != null) { data.winnerUpdate = { - winners: [ - { - handle: "ansary", - placement: 1, - userId: 123, - }, - ], // data.winners, + winners: data.winners, }; delete data.winners; } From 06578d3760c677fe8805eab6cf6a7f9b4cc92d35 Mon Sep 17 00:00:00 2001 From: Rakib Ansary Date: Fri, 24 Mar 2023 14:59:14 +0600 Subject: [PATCH 14/22] fix: unsetting winners Signed-off-by: Rakib Ansary --- src/services/ChallengeService.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index eaa81966..acb06448 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -2204,12 +2204,10 @@ function sanitizeChallenge(challenge) { if (challenge.events) { sanitized.events = _.map(challenge.events, (event) => _.pick(event, ["id", "name", "key"])); } - if (challenge.legacy != null && challenge.legacy.pureV5Task === true && challenge.winners) { + if (challenge.winners) { sanitized.winners = _.map(challenge.winners, (winner) => _.pick(winner, ["userId", "handle", "placement", "type"]) ); - } else { - delete challenge.winners; } if (challenge.discussions) { sanitized.discussions = _.map(challenge.discussions, (discussion) => ({ From 68a7dfd6e2218628cb3669e03b96bba018958bfc Mon Sep 17 00:00:00 2001 From: Emre Date: Fri, 24 Mar 2023 14:44:12 +0300 Subject: [PATCH 15/22] fix: phase update object --- src/services/ChallengeService.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index acb06448..0c1e1f16 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -1494,6 +1494,7 @@ async function updateChallenge(currentUser, challengeId, data) { // Remove fields from data that are not allowed to be updated and that match the existing challenge data = sanitizeData(sanitizeChallenge(data), challenge); + console.debug('Sanitized Data:', data); validateChallengeUpdateRequest(currentUser, challenge, data); @@ -1738,7 +1739,6 @@ async function updateChallenge(currentUser, challengeId, data) { } data.phases = newPhases; - challenge.phases = newPhases; data.startDate = newStartDate; data.endDate = helper.calculateChallengeEndDate(challenge, data); } @@ -2188,8 +2188,6 @@ function sanitizeChallenge(challenge) { _.pick(phase, [ "phaseId", "duration", - "isOpen", - "actualEndDate", "scheduledStartDate", "constraints", ]) From 80a46899dee8a121561bfc2167809e118c60ed37 Mon Sep 17 00:00:00 2001 From: Emre Date: Fri, 24 Mar 2023 14:51:08 +0300 Subject: [PATCH 16/22] fix: allow private description to be empty --- src/services/ChallengeService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index 0c1e1f16..43aed2ab 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -1982,7 +1982,7 @@ updateChallenge.schema = { typeId: Joi.optionalId(), name: Joi.string().optional(), description: Joi.string().optional(), - privateDescription: Joi.string().optional(), + privateDescription: Joi.string().allow('').optional(), descriptionFormat: Joi.string().optional(), metadata: Joi.array() .items( From f0f4dd7d899b546a55e9c36f0e2f9620f8156084 Mon Sep 17 00:00:00 2001 From: Emre Date: Fri, 24 Mar 2023 15:53:08 +0300 Subject: [PATCH 17/22] fix: phases --- src/common/phase-helper.js | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/common/phase-helper.js b/src/common/phase-helper.js index 0e678c51..1ffb99a7 100644 --- a/src/common/phase-helper.js +++ b/src/common/phase-helper.js @@ -185,13 +185,13 @@ class ChallengePhaseHelper { }; if (_.isUndefined(phase.predecessor)) { if (_.isUndefined(_.get(phaseFromInput, "scheduledStartDate"))) { - phase.scheduledStartDate = moment(startDate).toDate(); + phase.scheduledStartDate = moment(startDate).toDate().toISOString(); } else { - phase.scheduledStartDate = moment(_.get(phaseFromInput, "scheduledStartDate")).toDate(); + phase.scheduledStartDate = moment(_.get(phaseFromInput, "scheduledStartDate")).toDate().toISOString(); } phase.scheduledEndDate = moment(phase.scheduledStartDate) .add(phase.duration, "seconds") - .toDate(); + .toDate().toISOString(); } return phase; }); @@ -209,7 +209,7 @@ class ChallengePhaseHelper { } phase.scheduledEndDate = moment(phase.scheduledStartDate) .add(phase.duration, "seconds") - .toDate(); + .toDate().toISOString(); } return finalPhases; } @@ -236,36 +236,36 @@ class ChallengePhaseHelper { if (!_.isUndefined(phase.actualEndDate)) { return updatedPhase; } - if (phase.name === "Iterative Review Phase") { + if (updatedPhase.name === "Iterative Review Phase") { return updatedPhase; } - const newPhase = _.find(newPhases, (p) => p.phaseId === phase.phaseId); + const newPhase = _.find(newPhases, (p) => p.phaseId === updatedPhase.phaseId); if (_.isUndefined(newPhase) && !isBeingActivated) { return updatedPhase; } - phase.duration = _.defaultTo(_.get(newPhase, "duration"), phase.duration); - if (_.isUndefined(phase.predecessor)) { + updatedPhase.duration = _.defaultTo(_.get(newPhase, "duration"), updatedPhase.duration); + if (_.isUndefined(updatedPhase.predecessor)) { if ( isBeingActivated && moment( - _.defaultTo(_.get(newPhase, "scheduledStartDate"), phase.scheduledStartDate) + _.defaultTo(_.get(newPhase, "scheduledStartDate"), updatedPhase.scheduledStartDate) ).isSameOrBefore(moment()) ) { - phase.isOpen = true; - phase.scheduledStartDate = moment().toDate(); - phase.actualStartDate = phase.scheduledStartDate; + updatedPhase.isOpen = true; + updatedPhase.scheduledStartDate = moment().toDate().toISOString(); + updatedPhase.actualStartDate = updatedPhase.scheduledStartDate; } else if ( - phase.isOpen === false && + updatedPhase.isOpen === false && !_.isUndefined(_.get(newPhase, "scheduledStartDate")) ) { - phase.scheduledStartDate = moment(newPhase.scheduledStartDate).toDate(); + updatedPhase.scheduledStartDate = moment(newPhase.scheduledStartDate).toDate().toISOString(); } - phase.scheduledEndDate = moment(phase.scheduledStartDate) - .add(phase.duration, "seconds") - .toDate(); + updatedPhase.scheduledEndDate = moment(updatedPhase.scheduledStartDate) + .add(updatedPhase.duration, "seconds") + .toDate().toISOString(); } if (!_.isUndefined(newPhase) && !_.isUndefined(newPhase.constraints)) { - phase.constraints = newPhase.constraints; + updatedPhase.constraints = newPhase.constraints; } return updatedPhase; }); @@ -282,7 +282,7 @@ class ChallengePhaseHelper { phase.scheduledStartDate = precedecessorPhase.scheduledEndDate; phase.scheduledEndDate = moment(phase.scheduledStartDate) .add(phase.duration, "seconds") - .toDate(); + .toDate().toISOString(); } return updatedPhases; } From 310444476cfaae032c326a6ab4778f0094bbfc25 Mon Sep 17 00:00:00 2001 From: Rakib Ansary Date: Fri, 24 Mar 2023 20:34:32 +0600 Subject: [PATCH 18/22] chore: handle phase update Signed-off-by: Rakib Ansary --- package.json | 4 +- src/common/challenge-helper.js | 4 +- src/services/ChallengeService.js | 176 ++++++++++++++++--------------- yarn.lock | 40 +++---- 4 files changed, 114 insertions(+), 110 deletions(-) diff --git a/package.json b/package.json index 5a10472f..3131760c 100644 --- a/package.json +++ b/package.json @@ -42,8 +42,8 @@ "dependencies": { "@grpc/grpc-js": "^1.8.12", "@opensearch-project/opensearch": "^2.2.0", - "@topcoder-framework/domain-challenge": "^0.9.1", - "@topcoder-framework/lib-common": "^0.9.1", + "@topcoder-framework/domain-challenge": "^0.10.3", + "@topcoder-framework/lib-common": "^0.10.3", "aws-sdk": "^2.1145.0", "axios": "^0.19.0", "axios-retry": "^3.4.0", diff --git a/src/common/challenge-helper.js b/src/common/challenge-helper.js index 3a85035b..3b1e87de 100644 --- a/src/common/challenge-helper.js +++ b/src/common/challenge-helper.js @@ -101,7 +101,9 @@ class ChallengeHelper { } async validateChallengeUpdateRequest(currentUser, challenge, data) { - await helper.ensureUserCanModifyChallenge(currentUser, challenge); + if (process.env.LOCAL != "true") { + await helper.ensureUserCanModifyChallenge(currentUser, challenge); + } helper.ensureNoDuplicateOrNullElements(data.tags, "tags"); helper.ensureNoDuplicateOrNullElements(data.groups, "groups"); diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index 43aed2ab..c581dcce 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -1033,9 +1033,9 @@ async function createChallenge(currentUser, challenge, userToken) { } if (!challenge.startDate) { - challenge.startDate = new Date(); + challenge.startDate = new Date().toISOString(); } else { - challenge.startDate = new Date(challenge.startDate); + challenge.startDate = new Date(challenge.startDate).toISOString(); } const { track, type } = await challengeHelper.validateAndGetChallengeTypeAndTrack(challenge); @@ -1104,8 +1104,8 @@ async function createChallenge(currentUser, challenge, userToken) { if (challenge.metadata == null) challenge.metadata = []; if (challenge.groups == null) challenge.groups = []; if (challenge.tags == null) challenge.tags = []; - if (challenge.startDate != null) challenge.startDate = challenge.startDate.getTime(); - if (challenge.endDate != null) challenge.endDate = challenge.endDate.getTime(); + if (challenge.startDate != null) challenge.startDate = challenge.startDate; + if (challenge.endDate != null) challenge.endDate = challenge.endDate; if (challenge.discussions == null) challenge.discussions = []; challenge.metadata = challenge.metadata.map((m) => ({ @@ -1125,25 +1125,28 @@ async function createChallenge(currentUser, challenge, userToken) { enrichChallengeForResponse(ret, track, type); - // Create in ES - await esClient.create({ - index: config.get("ES.ES_INDEX"), - type: config.get("ES.OPENSEARCH") == "false" ? config.get("ES.ES_TYPE") : undefined, - refresh: config.get("ES.ES_REFRESH"), - id: ret.id, - body: ret, - }); + const isLocal = process.env.LOCAL == "true"; + if (!isLocal) { + // Create in ES + await esClient.create({ + index: config.get("ES.ES_INDEX"), + type: config.get("ES.OPENSEARCH") == "false" ? config.get("ES.ES_TYPE") : undefined, + refresh: config.get("ES.ES_REFRESH"), + id: ret.id, + body: ret, + }); - // If the challenge is self-service, add the creating user as the "client manager", *not* the manager - // This is necessary for proper handling of the vanilla embed on the self-service work item dashboard + // If the challenge is self-service, add the creating user as the "client manager", *not* the manager + // This is necessary for proper handling of the vanilla embed on the self-service work item dashboard - if (challenge.legacy.selfService) { - if (currentUser.handle) { - await helper.createResource(ret.id, ret.createdBy, config.CLIENT_MANAGER_ROLE_ID); - } - } else { - if (currentUser.handle) { - await helper.createResource(ret.id, ret.createdBy, config.MANAGER_ROLE_ID); + if (challenge.legacy.selfService) { + if (currentUser.handle) { + await helper.createResource(ret.id, ret.createdBy, config.CLIENT_MANAGER_ROLE_ID); + } + } else { + if (currentUser.handle) { + await helper.createResource(ret.id, ret.createdBy, config.MANAGER_ROLE_ID); + } } } @@ -1251,7 +1254,7 @@ createChallenge.schema = { tags: Joi.array().items(Joi.string()), // tag names projectId: Joi.number().integer().positive(), legacyId: Joi.number().integer().positive(), - startDate: Joi.date(), + startDate: Joi.date().iso(), status: Joi.string().valid(_.values(constants.challengeStatuses)), groups: Joi.array().items(Joi.optionalId()).unique(), // gitRepoURLs: Joi.array().items(Joi.string().uri()), @@ -1494,7 +1497,7 @@ async function updateChallenge(currentUser, challengeId, data) { // Remove fields from data that are not allowed to be updated and that match the existing challenge data = sanitizeData(sanitizeChallenge(data), challenge); - console.debug('Sanitized Data:', data); + console.debug("Sanitized Data:", data); validateChallengeUpdateRequest(currentUser, challenge, data); @@ -1739,7 +1742,7 @@ async function updateChallenge(currentUser, challengeId, data) { } data.phases = newPhases; - data.startDate = newStartDate; + data.startDate = new Date(newStartDate).toISOString(); data.endDate = helper.calculateChallengeEndDate(challenge, data); } @@ -1850,6 +1853,7 @@ async function updateChallenge(currentUser, challengeId, data) { grpcMetadata.set("handle", currentUser.handle); grpcMetadata.set("userId", currentUser.userId); + console.log("Calling update", updateInput); await challengeDomain.update( { filterCriteria: getScanCriteria({ id: challengeId }), @@ -1878,62 +1882,64 @@ async function updateChallenge(currentUser, challengeId, data) { : undefined, }); - // Update ES - // TODO: Uncomment - await esClient.update({ - index: config.get("ES.ES_INDEX"), - type: config.get("ES.OPENSEARCH") == "false" ? config.get("ES.ES_TYPE") : undefined, - refresh: config.get("ES.ES_REFRESH"), - id: challengeId, - body: { - doc: updatedChallenge, - }, - }); + const isLocal = process.env.LOCAL == "true"; + if (!isLocal) { + // Update ES + await esClient.update({ + index: config.get("ES.ES_INDEX"), + type: config.get("ES.OPENSEARCH") == "false" ? config.get("ES.ES_TYPE") : undefined, + refresh: config.get("ES.ES_REFRESH"), + id: challengeId, + body: { + doc: updatedChallenge, + }, + }); - if (updatedChallenge.legacy.selfService) { - const creator = await helper.getMemberByHandle(updatedChallenge.createdBy); - if (sendSubmittedEmail) { - await helper.sendSelfServiceNotification( - constants.SelfServiceNotificationTypes.WORK_REQUEST_SUBMITTED, - [{ email: creator.email }], - { - handle: creator.handle, - workItemName: updatedChallenge.name, - } - ); - } - if (sendActivationEmail) { - await helper.sendSelfServiceNotification( - constants.SelfServiceNotificationTypes.WORK_REQUEST_STARTED, - [{ email: creator.email }], - { - handle: creator.handle, - workItemName: updatedChallenge.name, - workItemUrl: `${config.SELF_SERVICE_APP_URL}/work-items/${updatedChallenge.id}`, - } - ); - } - if (sendCompletedEmail) { - await helper.sendSelfServiceNotification( - constants.SelfServiceNotificationTypes.WORK_COMPLETED, - [{ email: creator.email }], - { - handle: creator.handle, - workItemName: updatedChallenge.name, - workItemUrl: `${config.SELF_SERVICE_APP_URL}/work-items/${updatedChallenge.id}?tab=solutions`, - } - ); - } - if (sendRejectedEmail || cancelReason) { - logger.debug("Should send redirected email"); - await helper.sendSelfServiceNotification( - constants.SelfServiceNotificationTypes.WORK_REQUEST_REDIRECTED, - [{ email: creator.email }], - { - handle: creator.handle, - workItemName: updatedChallenge.name, - } - ); + if (updatedChallenge.legacy.selfService) { + const creator = await helper.getMemberByHandle(updatedChallenge.createdBy); + if (sendSubmittedEmail) { + await helper.sendSelfServiceNotification( + constants.SelfServiceNotificationTypes.WORK_REQUEST_SUBMITTED, + [{ email: creator.email }], + { + handle: creator.handle, + workItemName: updatedChallenge.name, + } + ); + } + if (sendActivationEmail) { + await helper.sendSelfServiceNotification( + constants.SelfServiceNotificationTypes.WORK_REQUEST_STARTED, + [{ email: creator.email }], + { + handle: creator.handle, + workItemName: updatedChallenge.name, + workItemUrl: `${config.SELF_SERVICE_APP_URL}/work-items/${updatedChallenge.id}`, + } + ); + } + if (sendCompletedEmail) { + await helper.sendSelfServiceNotification( + constants.SelfServiceNotificationTypes.WORK_COMPLETED, + [{ email: creator.email }], + { + handle: creator.handle, + workItemName: updatedChallenge.name, + workItemUrl: `${config.SELF_SERVICE_APP_URL}/work-items/${updatedChallenge.id}?tab=solutions`, + } + ); + } + if (sendRejectedEmail || cancelReason) { + logger.debug("Should send redirected email"); + await helper.sendSelfServiceNotification( + constants.SelfServiceNotificationTypes.WORK_REQUEST_REDIRECTED, + [{ email: creator.email }], + { + handle: creator.handle, + workItemName: updatedChallenge.name, + } + ); + } } } @@ -1982,7 +1988,7 @@ updateChallenge.schema = { typeId: Joi.optionalId(), name: Joi.string().optional(), description: Joi.string().optional(), - privateDescription: Joi.string().allow('').optional(), + privateDescription: Joi.string().allow("").optional(), descriptionFormat: Joi.string().optional(), metadata: Joi.array() .items( @@ -2041,7 +2047,7 @@ updateChallenge.schema = { }) ) .optional(), - startDate: Joi.date(), + startDate: Joi.date().iso(), prizeSets: Joi.array() .items( Joi.object() @@ -2185,12 +2191,7 @@ function sanitizeChallenge(challenge) { } if (challenge.phases) { sanitized.phases = _.map(challenge.phases, (phase) => - _.pick(phase, [ - "phaseId", - "duration", - "scheduledStartDate", - "constraints", - ]) + _.pick(phase, ["phaseId", "duration", "scheduledStartDate", "constraints"]) ); } if (challenge.prizeSets) { @@ -2221,6 +2222,7 @@ function sanitizeChallenge(challenge) { _.pick(attachment, ["id", "name", "url", "fileSize", "description", "challengeId"]) ); } + return sanitized; } diff --git a/yarn.lock b/yarn.lock index 95d0b2e8..f6cdee9d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -245,35 +245,35 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== -"@topcoder-framework/client-relational@^0.9.1": - version "0.9.1" - resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/client-relational/-/client-relational-0.9.1.tgz#6f87c8ddb8d2c19799ca51d220340ab0c0f67bd9" - integrity sha512-42sZAY60oSve8QmLL7eyrmZx7C5YAvbKZt77CiLTfUGBmURxwzlADbJKrCKBw+R0VSmtBxZ6oHdvBnXhSPjPHA== +"@topcoder-framework/client-relational@^0.10.3": + version "0.10.3" + resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/client-relational/-/client-relational-0.10.3.tgz#80ceef15ea3092a5087627d1de6d6d62ee530cd8" + integrity sha512-WH5YZenKVJOxracuhcG14jiWTvs094LvePXew9vwYrXCaKS+ao9krrLghz11vJ+wcd3bcGF9PEIbnGfmHvnirg== dependencies: "@grpc/grpc-js" "^1.8.0" - "@topcoder-framework/lib-common" "^0.9.1" - topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.33" + "@topcoder-framework/lib-common" "^0.10.3" + topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.37" tslib "^2.4.1" -"@topcoder-framework/domain-challenge@^0.9.1": - version "0.9.1" - resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/domain-challenge/-/domain-challenge-0.9.1.tgz#cfec41a1403c6f744a83f21d0ddc9d29a7b23f69" - integrity sha512-m5lcWumHWJUVvB+39ddKH+dn8MOFPHsDpxFdWepD4jw6Vo2GZhJINpp3ZQWLA2UQUAYSaQp8kwOa93wdvzE7eg== +"@topcoder-framework/domain-challenge@^0.10.3": + version "0.10.3" + resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/domain-challenge/-/domain-challenge-0.10.3.tgz#fbaa82388c33b3e7131f9ba3a2e6eb7e02a8078d" + integrity sha512-IoouiTPgVKKbZn9kqcVHygD3XqXwMvw7ktWFx72gEl9iNS36YH6lgMXb9WQa3OJHdfNqj9vh5Vx7LDdEN85+3w== dependencies: "@grpc/grpc-js" "^1.8.0" - "@topcoder-framework/client-relational" "^0.9.1" - "@topcoder-framework/lib-common" "^0.9.1" - topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.33" + "@topcoder-framework/client-relational" "^0.10.3" + "@topcoder-framework/lib-common" "^0.10.3" + topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.37" tslib "^2.4.1" -"@topcoder-framework/lib-common@^0.9.1": - version "0.9.1" - resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/lib-common/-/lib-common-0.9.1.tgz#59ee523d05a8da4cab7866bd9ba24dd714c713a1" - integrity sha512-4W5Oz3XN6n3dsEZwmSiDxVuQtkuAxPJc9PyivIatyLB5HSAva/OuQbBcCWhFl2ORRywVxEBJz2cWFme+a6vEPw== +"@topcoder-framework/lib-common@^0.10.3": + version "0.10.3" + resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/lib-common/-/lib-common-0.10.3.tgz#c0ede7b39a53bba6d9092c5aca7d16a98abe31c5" + integrity sha512-gvIJ47NFIEuBfqDTDH2GA7hPilpMbUgj4At7zbSpIAiTRhiMt3qKaDcSniSbR3RJ/8AP8BYoAFkMZlRJwcaA6g== dependencies: "@grpc/grpc-js" "^1.8.0" rimraf "^3.0.2" - topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.33" + topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.37" tslib "^2.4.1" "@types/body-parser@*": @@ -3677,9 +3677,9 @@ topcoder-bus-api-wrapper@topcoder-platform/tc-bus-api-wrapper.git: superagent "^3.8.3" tc-core-library-js appirio-tech/tc-core-library-js.git#v2.6.4 -"topcoder-interface@github:topcoder-platform/plat-interface-definition#v0.0.33": +"topcoder-interface@github:topcoder-platform/plat-interface-definition#v0.0.37": version "1.0.0" - resolved "https://codeload.github.com/topcoder-platform/plat-interface-definition/tar.gz/8d53fb22968e3801d3d77c7c9333b18775eae282" + resolved "https://codeload.github.com/topcoder-platform/plat-interface-definition/tar.gz/0b08d4b07a016028c1d8e97d95fb1a2a3974acc1" topo@3.x.x: version "3.0.3" From b60daa88d90d696e6cee2b1b9492c8ebaa097137 Mon Sep 17 00:00:00 2001 From: Thomas Kranitsas Date: Sat, 25 Mar 2023 11:30:20 +0200 Subject: [PATCH 19/22] validation updates --- src/common/challenge-helper.js | 5 +++++ src/services/ChallengeService.js | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/src/common/challenge-helper.js b/src/common/challenge-helper.js index 3b1e87de..b472f06a 100644 --- a/src/common/challenge-helper.js +++ b/src/common/challenge-helper.js @@ -117,6 +117,11 @@ class ChallengeHelper { await ensureAcessibilityToModifiedGroups(currentUser, data, challenge); } + // Ensure descriptionFormat is either 'markdown' or 'html' + if (data.descriptionFormat && !_.includes(["markdown", "html"], data.descriptionFormat)) { + throw new errors.BadRequestError("The property 'descriptionFormat' must be either 'markdown' or 'html'"); + } + // Ensure unchangeable fields are not changed if ( _.get(challenge, "legacy.track") && diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index c581dcce..d161fdc7 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -1515,6 +1515,13 @@ async function updateChallenge(currentUser, challengeId, data) { _.set(data, "billing.markup", markup || 0); } + // Make sure the user cannot change the direct project ID + if (data.legacy && data.legacy.directProjectId) { + const { directProjectId } = await projectHelper.getProject(projectId, currentUser); + + _.set(challenge, "legacy.directProjectId", directProjectId); + } + /* BEGIN self-service stuffs */ // TODO: At some point in the future this should be moved to a Self-Service Challenge Helper From 28d164684d82fb8dfb5a94951f4a8d1c8caca3a9 Mon Sep 17 00:00:00 2001 From: Thomas Kranitsas Date: Sat, 25 Mar 2023 11:37:06 +0200 Subject: [PATCH 20/22] unset legacy.directProjectId as we do not allowe this to change --- src/services/ChallengeService.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index d161fdc7..6d0f4bbb 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -1517,9 +1517,7 @@ async function updateChallenge(currentUser, challengeId, data) { // Make sure the user cannot change the direct project ID if (data.legacy && data.legacy.directProjectId) { - const { directProjectId } = await projectHelper.getProject(projectId, currentUser); - - _.set(challenge, "legacy.directProjectId", directProjectId); + _.unset(data, "legacy.directProjectId", directProjectId); } /* BEGIN self-service stuffs */ From 33247ca4eeb0657624e8f39d796bc7adb4320f36 Mon Sep 17 00:00:00 2001 From: Rakib Ansary Date: Sat, 25 Mar 2023 17:29:08 +0600 Subject: [PATCH 21/22] fix: bump up versions Signed-off-by: Rakib Ansary --- package.json | 4 ++-- yarn.lock | 40 ++++++++++++++++++++-------------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index 3131760c..cd2d0cbe 100644 --- a/package.json +++ b/package.json @@ -42,8 +42,8 @@ "dependencies": { "@grpc/grpc-js": "^1.8.12", "@opensearch-project/opensearch": "^2.2.0", - "@topcoder-framework/domain-challenge": "^0.10.3", - "@topcoder-framework/lib-common": "^0.10.3", + "@topcoder-framework/domain-challenge": "^0.10.8", + "@topcoder-framework/lib-common": "^0.10.8", "aws-sdk": "^2.1145.0", "axios": "^0.19.0", "axios-retry": "^3.4.0", diff --git a/yarn.lock b/yarn.lock index f6cdee9d..480efe6a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -245,35 +245,35 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== -"@topcoder-framework/client-relational@^0.10.3": - version "0.10.3" - resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/client-relational/-/client-relational-0.10.3.tgz#80ceef15ea3092a5087627d1de6d6d62ee530cd8" - integrity sha512-WH5YZenKVJOxracuhcG14jiWTvs094LvePXew9vwYrXCaKS+ao9krrLghz11vJ+wcd3bcGF9PEIbnGfmHvnirg== +"@topcoder-framework/client-relational@^0.10.8": + version "0.10.8" + resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/client-relational/-/client-relational-0.10.8.tgz#8a4b0c3eab4cb4e1e83c8ee533abc9b541a20fbc" + integrity sha512-vdEQhi7vKNIZWmWc3TnKEqLqXuyOMkJszgI5rXl54fkt/3MI1FPUHwHC6CexpieLdtJ+MMeGLcUxDDWcOux+3g== dependencies: "@grpc/grpc-js" "^1.8.0" - "@topcoder-framework/lib-common" "^0.10.3" - topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.37" + "@topcoder-framework/lib-common" "^0.10.8" + topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.41" tslib "^2.4.1" -"@topcoder-framework/domain-challenge@^0.10.3": - version "0.10.3" - resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/domain-challenge/-/domain-challenge-0.10.3.tgz#fbaa82388c33b3e7131f9ba3a2e6eb7e02a8078d" - integrity sha512-IoouiTPgVKKbZn9kqcVHygD3XqXwMvw7ktWFx72gEl9iNS36YH6lgMXb9WQa3OJHdfNqj9vh5Vx7LDdEN85+3w== +"@topcoder-framework/domain-challenge@^0.10.8": + version "0.10.8" + resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/domain-challenge/-/domain-challenge-0.10.8.tgz#08915436d07f57d83dbca2fafc75f4c5789f4c27" + integrity sha512-IHdqnR0QCpKQHpgD4BRHTdTxu1GPBnYYJiuKi+8lG+N/Xq9wVmerq2qxye7M6wWmqDRbLj3pF8kETcU4DQG73w== dependencies: "@grpc/grpc-js" "^1.8.0" - "@topcoder-framework/client-relational" "^0.10.3" - "@topcoder-framework/lib-common" "^0.10.3" - topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.37" + "@topcoder-framework/client-relational" "^0.10.8" + "@topcoder-framework/lib-common" "^0.10.8" + topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.41" tslib "^2.4.1" -"@topcoder-framework/lib-common@^0.10.3": - version "0.10.3" - resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/lib-common/-/lib-common-0.10.3.tgz#c0ede7b39a53bba6d9092c5aca7d16a98abe31c5" - integrity sha512-gvIJ47NFIEuBfqDTDH2GA7hPilpMbUgj4At7zbSpIAiTRhiMt3qKaDcSniSbR3RJ/8AP8BYoAFkMZlRJwcaA6g== +"@topcoder-framework/lib-common@^0.10.8": + version "0.10.8" + resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/lib-common/-/lib-common-0.10.8.tgz#c25077288df668650ceb5015ee8fe1e34ea45faa" + integrity sha512-+AmSAfpW8mXuWE66/pUREYXrRA93Oddl+ZAT4/Qn5UstrgZTTRI6/ge0tEyn/yshRO9y8i+2X0Kt6fn80HWJTQ== dependencies: "@grpc/grpc-js" "^1.8.0" rimraf "^3.0.2" - topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.37" + topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.41" tslib "^2.4.1" "@types/body-parser@*": @@ -3677,9 +3677,9 @@ topcoder-bus-api-wrapper@topcoder-platform/tc-bus-api-wrapper.git: superagent "^3.8.3" tc-core-library-js appirio-tech/tc-core-library-js.git#v2.6.4 -"topcoder-interface@github:topcoder-platform/plat-interface-definition#v0.0.37": +"topcoder-interface@github:topcoder-platform/plat-interface-definition#v0.0.41": version "1.0.0" - resolved "https://codeload.github.com/topcoder-platform/plat-interface-definition/tar.gz/0b08d4b07a016028c1d8e97d95fb1a2a3974acc1" + resolved "https://codeload.github.com/topcoder-platform/plat-interface-definition/tar.gz/75ed841dc94988a0757f91fc1c00a95be2a37d12" topo@3.x.x: version "3.0.3" From 8ce4e0266cfa863b9d87a2878f5d9d6b0537e03d Mon Sep 17 00:00:00 2001 From: Rakib Ansary Date: Sun, 26 Mar 2023 13:59:11 +0600 Subject: [PATCH 22/22] fix: prize amount in cents --- package.json | 4 ++-- src/common/challenge-helper.js | 24 +++++++++++++++++++ src/services/ChallengeService.js | 13 +++++++---- yarn.lock | 40 ++++++++++++++++---------------- 4 files changed, 54 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index cd2d0cbe..0f56204a 100644 --- a/package.json +++ b/package.json @@ -42,8 +42,8 @@ "dependencies": { "@grpc/grpc-js": "^1.8.12", "@opensearch-project/opensearch": "^2.2.0", - "@topcoder-framework/domain-challenge": "^0.10.8", - "@topcoder-framework/lib-common": "^0.10.8", + "@topcoder-framework/domain-challenge": "^0.10.13", + "@topcoder-framework/lib-common": "^0.10.13", "aws-sdk": "^2.1145.0", "axios": "^0.19.0", "axios-retry": "^3.4.0", diff --git a/src/common/challenge-helper.js b/src/common/challenge-helper.js index 3b1e87de..8a402073 100644 --- a/src/common/challenge-helper.js +++ b/src/common/challenge-helper.js @@ -316,6 +316,30 @@ class ChallengeHelper { return m; }); } + + convertPrizeSetValuesToCents(prizeSets) { + prizeSets.forEach((prizeSet) => { + prizeSet.prizes.forEach((prize) => { + prize.amountInCents = prize.value * 100; + delete prize.value; + }); + }); + } + + convertPrizeSetValuesToDollars(prizeSets, overview) { + prizeSets.forEach((prizeSet) => { + prizeSet.prizes.forEach((prize) => { + if (prize.amountInCents != null) { + prize.value = prize.amountInCents / 100; + delete prize.amountInCents; + } + }); + }); + if (overview && overview.totalPrizesInCents) { + overview.totalPrizes = overview.totalPrizesInCents / 100; + delete overview.totalPrizesInCents; + } + } } module.exports = new ChallengeHelper(); diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index c581dcce..2e99445c 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -40,6 +40,8 @@ const { validateChallengeUpdateRequest, enrichChallengeForResponse, sanitizeRepeatedFieldsInUpdateRequest, + convertPrizeSetValuesToCents, + convertPrizeSetValuesToDollars, } = require("../common/challenge-helper"); const deepEqual = require("deep-equal"); @@ -1118,7 +1120,9 @@ async function createChallenge(currentUser, challenge, userToken) { grpcMetadata.set("handle", currentUser.handle); grpcMetadata.set("userId", currentUser.userId); + convertPrizeSetValuesToCents(challenge.prizeSets); const ret = await challengeDomain.create(challenge, grpcMetadata); + convertPrizeSetValuesToDollars(ret.prizeSets, ret.overview); ret.numOfSubmissions = 0; ret.numOfRegistrants = 0; @@ -1841,10 +1845,6 @@ async function updateChallenge(currentUser, challengeId, data) { } try { - logger.debug( - `ChallengeDomain.update id: ${challengeId} Details: ${JSON.stringify(data, null, 2)}` - ); - const updateInput = sanitizeRepeatedFieldsInUpdateRequest(data); if (!_.isEmpty(updateInput)) { @@ -1853,7 +1853,9 @@ async function updateChallenge(currentUser, challengeId, data) { grpcMetadata.set("handle", currentUser.handle); grpcMetadata.set("userId", currentUser.userId); - console.log("Calling update", updateInput); + if (updateInput.prizeSetUpdate != null) { + convertPrizeSetValuesToCents(updateInput.prizeSetUpdate.prizeSets); + } await challengeDomain.update( { filterCriteria: getScanCriteria({ id: challengeId }), @@ -1867,6 +1869,7 @@ async function updateChallenge(currentUser, challengeId, data) { } const updatedChallenge = await challengeDomain.lookup(getLookupCriteria("id", challengeId)); + convertPrizeSetValuesToDollars(updatedChallenge.prizeSets, updatedChallenge.overview); // post bus event logger.debug( diff --git a/yarn.lock b/yarn.lock index 480efe6a..938144b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -245,35 +245,35 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== -"@topcoder-framework/client-relational@^0.10.8": - version "0.10.8" - resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/client-relational/-/client-relational-0.10.8.tgz#8a4b0c3eab4cb4e1e83c8ee533abc9b541a20fbc" - integrity sha512-vdEQhi7vKNIZWmWc3TnKEqLqXuyOMkJszgI5rXl54fkt/3MI1FPUHwHC6CexpieLdtJ+MMeGLcUxDDWcOux+3g== +"@topcoder-framework/client-relational@^0.10.13": + version "0.10.13" + resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/client-relational/-/client-relational-0.10.13.tgz#84293cd265328d5f770c28ffd690fbb434ac936b" + integrity sha512-p4ygOE0K2xrz/wmTSS5/3DX2lEH/bmiWsW+sL8RVstAhilWSQmdyJb49sI/QzbFqhHGS/aQnkKPt8gaNtIaVWQ== dependencies: "@grpc/grpc-js" "^1.8.0" - "@topcoder-framework/lib-common" "^0.10.8" - topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.41" + "@topcoder-framework/lib-common" "^0.10.13" + topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.46" tslib "^2.4.1" -"@topcoder-framework/domain-challenge@^0.10.8": - version "0.10.8" - resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/domain-challenge/-/domain-challenge-0.10.8.tgz#08915436d07f57d83dbca2fafc75f4c5789f4c27" - integrity sha512-IHdqnR0QCpKQHpgD4BRHTdTxu1GPBnYYJiuKi+8lG+N/Xq9wVmerq2qxye7M6wWmqDRbLj3pF8kETcU4DQG73w== +"@topcoder-framework/domain-challenge@^0.10.13": + version "0.10.13" + resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/domain-challenge/-/domain-challenge-0.10.13.tgz#dede4cd01054e56eb4e4486eeb99cfd9ab4d75f1" + integrity sha512-srkncIcHaD1aGYD6DSHGzZDORjPZkTN9qNgZSNNYXx3Q6pNc4z3dUQqv79bEv472af4zkXmemMcmHqPTRilVtQ== dependencies: "@grpc/grpc-js" "^1.8.0" - "@topcoder-framework/client-relational" "^0.10.8" - "@topcoder-framework/lib-common" "^0.10.8" - topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.41" + "@topcoder-framework/client-relational" "^0.10.13" + "@topcoder-framework/lib-common" "^0.10.13" + topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.46" tslib "^2.4.1" -"@topcoder-framework/lib-common@^0.10.8": - version "0.10.8" - resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/lib-common/-/lib-common-0.10.8.tgz#c25077288df668650ceb5015ee8fe1e34ea45faa" - integrity sha512-+AmSAfpW8mXuWE66/pUREYXrRA93Oddl+ZAT4/Qn5UstrgZTTRI6/ge0tEyn/yshRO9y8i+2X0Kt6fn80HWJTQ== +"@topcoder-framework/lib-common@^0.10.13": + version "0.10.13" + resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/lib-common/-/lib-common-0.10.13.tgz#69a0c70d601cc37821ece1b13d300dbe8e6ddc10" + integrity sha512-LXaoLQma+7cs7ly6McXmhO3YWNF27MzqiR3fgtlefVU1XbfVfWhSfDLitTUSw08PMgv+VC6nTfyo0t4202ZVcg== dependencies: "@grpc/grpc-js" "^1.8.0" rimraf "^3.0.2" - topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.41" + topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.46" tslib "^2.4.1" "@types/body-parser@*": @@ -3677,9 +3677,9 @@ topcoder-bus-api-wrapper@topcoder-platform/tc-bus-api-wrapper.git: superagent "^3.8.3" tc-core-library-js appirio-tech/tc-core-library-js.git#v2.6.4 -"topcoder-interface@github:topcoder-platform/plat-interface-definition#v0.0.41": +"topcoder-interface@github:topcoder-platform/plat-interface-definition#v0.0.46": version "1.0.0" - resolved "https://codeload.github.com/topcoder-platform/plat-interface-definition/tar.gz/75ed841dc94988a0757f91fc1c00a95be2a37d12" + resolved "https://codeload.github.com/topcoder-platform/plat-interface-definition/tar.gz/8ed5b7686125a17209c85c33f69c92476625e3c1" topo@3.x.x: version "3.0.3"