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 diff --git a/package.json b/package.json index fd85d29a..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.7.3", - "@topcoder-framework/lib-common": "^0.7.3", + "@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", @@ -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/common/challenge-helper.js b/src/common/challenge-helper.js index 59e5b232..b6a3db22 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 { /** @@ -45,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 { @@ -98,6 +99,252 @@ class ChallengeHelper { // check groups authorization await helper.ensureAccessibleByGroupsAccess(currentUser, challenge); } + + async validateChallengeUpdateRequest(currentUser, challenge, data) { + if (process.env.LOCAL != "true") { + await helper.ensureUserCanModifyChallenge(currentUser, challenge); + } + + helper.ensureNoDuplicateOrNullElements(data.tags, "tags"); + helper.ensureNoDuplicateOrNullElements(data.groups, "groups"); + + if (data.projectId) { + await ChallengeHelper.ensureProjectExist(data.projectId, currentUser); + } + + // check groups access to be updated group values + if (data.groups) { + 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") && + _.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` + ); + } + } + + sanitizeRepeatedFieldsInUpdateRequest(data) { + if (data.winners != null) { + data.winnerUpdate = { + winners: 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"); + const submissionPhase = _.find(challenge.phases, (p) => p.name === "Submission"); + + challenge.currentPhase = challenge.phases + .slice() + .reverse() + .find((phase) => phase.isOpen); + + challenge.currentPhaseNames = _.map( + _.filter(challenge.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; + }); + } + + 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/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 be969a9d..e5228f09 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"); } @@ -942,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, { @@ -1123,7 +1126,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 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/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; } 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/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 334ff226..7d7064bd 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -34,60 +34,19 @@ 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, + convertPrizeSetValuesToCents, + convertPrizeSetValuesToDollars, +} = require("../common/challenge-helper"); +const deepEqual = require("deep-equal"); 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 * @@ -177,33 +136,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 +1001,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 ); } @@ -1104,9 +1035,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); @@ -1175,8 +1106,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) => ({ @@ -1189,72 +1120,37 @@ 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; - 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; - }); - - // 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, - }); + enrichChallengeForResponse(ret, track, type); + + 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); + } } } @@ -1263,7 +1159,6 @@ async function createChallenge(currentUser, challenge, userToken) { return ret; } - createChallenge.schema = { currentUser: Joi.any(), challenge: Joi.object() @@ -1363,7 +1258,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()), @@ -1480,7 +1375,6 @@ async function getChallenge(currentUser, id, checkIfExists) { return challenge; } - getChallenge.schema = { currentUser: Joi.any(), id: Joi.id(), @@ -1602,107 +1496,106 @@ 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) { - const cancelReason = _.cloneDeep(data.cancelReason); - delete data.cancelReason; +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); + console.debug("Sanitized Data:", data); + + validateChallengeUpdateRequest(currentUser, challenge, data); + + const projectId = _.get(challenge, "projectId"); 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); + const { billingAccountId, markup } = await projectHelper.getProjectBillingInformation(projectId); + + if (billingAccountId && _.isUndefined(_.get(challenge, "billing.billingAccountId"))) { + _.set(data, "billing.billingAccountId", billingAccountId); + _.set(data, "billing.markup", markup || 0); } - helper.ensureNoDuplicateOrNullElements(data.tags, "tags"); - helper.ensureNoDuplicateOrNullElements(data.groups, "groups"); - // helper.ensureNoDuplicateOrNullElements(data.gitRepoURLs, 'gitRepoURLs') + // Make sure the user cannot change the direct project ID + if (data.legacy && data.legacy.directProjectId) { + _.unset(data, "legacy.directProjectId", directProjectId); + } - const challenge = await challengeDomain.lookup(getLookupCriteria("id", challengeId)); + /* BEGIN self-service stuffs */ - let dynamicDescription = _.cloneDeep(data.description || challenge.description); + // TODO: At some point in the future this should be moved to a Self-Service Challenge Helper - 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 && - 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; + 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); } - } 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!" - ); + data.description = dynamicDescription; } - } - 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 = []; + // 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 updateChallenge( + 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 (!_.find(data.metadata, (e) => e.name === "postMortemRequired")) { - data.metadata.push({ - name: "postMortemRequired", - value: "false", - }); + + 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; if (data.status) { if (data.status === constants.challengeStatuses.Active) { @@ -1728,6 +1621,7 @@ async function update(currentUser, challengeId, data, isFull) { isChallengeBeingActivated = true; } } + if ( data.status === constants.challengeStatuses.CancelledRequirementsInfeasible || data.status === constants.challengeStatuses.CancelledPaymentFailed @@ -1739,6 +1633,7 @@ async function update(currentUser, challengeId, data, isFull) { } sendRejectedEmail = true; } + if (data.status === constants.challengeStatuses.Completed) { if ( !_.get(challenge, "legacy.pureV5Task") && @@ -1751,81 +1646,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 +1680,6 @@ 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; @@ -1940,13 +1714,13 @@ async function update(currentUser, challengeId, data, isFull) { _.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 } }); } } @@ -1977,27 +1751,12 @@ async function update(currentUser, challengeId, data, isFull) { } data.phases = newPhases; - challenge.phases = newPhases; - data.startDate = newStartDate; + data.startDate = new Date(newStartDate).toISOString(); 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) { + console.log("Request to validate winners", data.winners, challengeId); await validateWinners(data.winners, challengeId); } @@ -2049,175 +1808,12 @@ async function update(currentUser, challengeId, data, isFull) { 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 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); - } - } - } + const { track, type } = await challengeHelper.validateAndGetChallengeTypeAndTrack({ + typeId: challenge.typeId, + trackId: challenge.trackId, + timelineTemplateId: challenge.timelineTemplateId, }); - 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"])); - if (_.get(type, "isTask")) { if (!_.isEmpty(_.get(data, "task.memberId"))) { const challengeResources = await helper.getChallengeResources(challengeId); @@ -2241,453 +1837,124 @@ async function update(currentUser, challengeId, data, isFull) { } } - logger.debug(`Challenge.update id: ${challengeId} Details: ${JSON.stringify(updateDetails)}`); - - delete data.attachments; - delete data.terms; - _.assign(challenge, data); - if (!_.isUndefined(newAttachments)) { - challenge.attachments = newAttachments; - data.attachments = newAttachments; + if (!_.isUndefined(data.terms)) { + await helper.validateChallengeTerms(data.terms.map((t) => t.id)); } - if (!_.isUndefined(newTermsOfUse)) { - challenge.terms = newTermsOfUse; - data.terms = newTermsOfUse; - } + if (data.phases && data.phases.length > 0) { + await getPhasesAndPopulate(data); - if (challenge.phases && challenge.phases.length > 0) { - await getPhasesAndPopulate(challenge); + if (deepEqual(data.phases, challenge.phases)) { + delete data.phases; + } } - // Populate challenge.track and challenge.type based on the track/type IDs + try { + const updateInput = sanitizeRepeatedFieldsInUpdateRequest(data); - if (track) { - challenge.track = track.name; - } - if (type) { - challenge.type = type.name; - } + if (!_.isEmpty(updateInput)) { + const grpcMetadata = new GrpcMetadata(); - try { - logger.debug( - `ChallengeDomain.update id: ${challengeId} Details: ${JSON.stringify(challenge)}` - ); - const { items } = await challengeDomain.update({ - filterCriteria: getScanCriteria({ - id: challengeId, - }), - updateInput: { - ...challenge, - }, - }); - if (items.length > 0) { - if (!challenge.legacyId) { - challenge.legacyId = items[0].legacyId; + grpcMetadata.set("handle", currentUser.handle); + grpcMetadata.set("userId", currentUser.userId); + + if (updateInput.prizeSetUpdate != null) { + convertPrizeSetValuesToCents(updateInput.prizeSetUpdate.prizeSets); } + await challengeDomain.update( + { + filterCriteria: getScanCriteria({ id: challengeId }), + updateInput, + }, + grpcMetadata + ); } } catch (e) { throw e; } - // 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 (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; - challenge.registrationEndDate = - registrationPhase.actualEndDate || registrationPhase.scheduledEndDate; - } - if (submissionPhase) { - challenge.submissionStartDate = - submissionPhase.actualStartDate || submissionPhase.scheduledStartDate; - challenge.submissionEndDate = - submissionPhase.actualEndDate || submissionPhase.scheduledEndDate; - } - } - // 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: challenge, - }, - }); - if (challenge.legacy.selfService) { - const creator = await helper.getMemberByHandle(challenge.createdBy); - if (sendSubmittedEmail) { - await helper.sendSelfServiceNotification( - constants.SelfServiceNotificationTypes.WORK_REQUEST_SUBMITTED, - [{ email: creator.email }], - { - handle: creator.handle, - workItemName: challenge.name, - } - ); - } - if (sendActivationEmail) { - await helper.sendSelfServiceNotification( - constants.SelfServiceNotificationTypes.WORK_REQUEST_STARTED, - [{ email: creator.email }], - { - handle: creator.handle, - workItemName: challenge.name, - workItemUrl: `${config.SELF_SERVICE_APP_URL}/work-items/${challenge.id}`, - } - ); - } - if (sendCompletedEmail) { - 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`, - } - ); - } - if (sendRejectedEmail || cancelReason) { - logger.debug("Should send redirected email"); - await helper.sendSelfServiceNotification( - constants.SelfServiceNotificationTypes.WORK_REQUEST_REDIRECTED, - [{ email: creator.email }], - { - handle: creator.handle, - workItemName: challenge.name, - } - ); - } - } - return challenge; -} + const updatedChallenge = await challengeDomain.lookup(getLookupCriteria("id", challengeId)); + convertPrizeSetValuesToDollars(updatedChallenge.prizeSets, updatedChallenge.overview); -/** - * 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 }; - } -} + // post bus event + logger.debug( + `Post Bus Event: ${constants.Topics.ChallengeUpdated} ${JSON.stringify(updatedChallenge)}` + ); -sendNotifications.schema = { - currentUser: Joi.any(), - challengeId: Joi.id(), -}; + enrichChallengeForResponse(updatedChallenge, track, type); -/** - * 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; -} + await helper.postBusEvent(constants.Topics.ChallengeUpdated, updatedChallenge, { + key: + updatedChallenge.status === "Completed" + ? `${updatedChallenge.id}:${updatedChallenge.status}` + : undefined, + }); -/** - * 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); -} + 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, + }, + }); -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(), -}; + 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, + } + ); + } + } + } -/** - * 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)); + return updatedChallenge; } -partiallyUpdateChallenge.schema = { +updateChallenge.schema = { currentUser: Joi.any(), challengeId: Joi.id(), data: Joi.object() @@ -2711,12 +1978,14 @@ partiallyUpdateChallenge.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.string().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(), @@ -2725,10 +1994,10 @@ partiallyUpdateChallenge.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().allow("").optional(), + descriptionFormat: Joi.string().optional(), metadata: Joi.array() .items( Joi.object() @@ -2739,7 +2008,7 @@ partiallyUpdateChallenge.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() @@ -2762,7 +2031,8 @@ partiallyUpdateChallenge.schema = { }) .unknown(true) ) - .min(1), + .min(1) + .optional(), events: Joi.array().items( Joi.object() .keys({ @@ -2771,18 +2041,21 @@ partiallyUpdateChallenge.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()), - }) - ), - startDate: Joi.date(), + 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().iso(), prizeSets: Joi.array() .items( Joi.object() @@ -2817,7 +2090,7 @@ partiallyUpdateChallenge.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( @@ -2832,14 +2105,157 @@ partiallyUpdateChallenge.schema = { }) .unknown(true) ) - .min(1), - terms: Joi.array().items(Joi.id().optional()).optional().allow([]), + .optional(), + terms: Joi.array().items( + Joi.object().keys({ + id: Joi.id(), + roleId: Joi.id(), + }) + ), overview: Joi.any().forbidden(), }) .unknown(true) .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", "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; +} + +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 @@ -2880,8 +2296,7 @@ module.exports = { searchChallenges, createChallenge, getChallenge, - fullyUpdateChallenge, - partiallyUpdateChallenge, + updateChallenge, deleteChallenge, getChallengeStatistics, sendNotifications, diff --git a/yarn.lock b/yarn.lock index bea6ab1e..938144b1 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.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.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.7.3" - topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.30" + "@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.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.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.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.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.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.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.30" + topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.46" 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" @@ -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" @@ -2627,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" @@ -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" @@ -2883,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" @@ -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" @@ -3596,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.30": +"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/704a8d8ed31bb5f7edfd328aeaedaa3d36d56e33" + resolved "https://codeload.github.com/topcoder-platform/plat-interface-definition/tar.gz/8ed5b7686125a17209c85c33f69c92476625e3c1" topo@3.x.x: version "3.0.3" @@ -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"