diff --git a/.circleci/config.yml b/.circleci/config.yml index 6c3dbae7..945f8788 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -51,6 +51,7 @@ jobs: DEPLOY_ENV: "DEV" LOGICAL_ENV: "dev" APPNAME: "challenge-api" + CODEARTIFACT_ENV: "PROD" steps: *builddeploy_steps "build-qa": @@ -80,7 +81,8 @@ workflows: filters: branches: only: - - dev + - refactor/domain-challenge-dev + - refactor/challenge-update - "build-qa": context: org-global diff --git a/app-routes.js b/app-routes.js index fdb7db14..3632e13e 100644 --- a/app-routes.js +++ b/app-routes.js @@ -50,6 +50,7 @@ module.exports = (app) => { next(new errors.ForbiddenError("You are not allowed to perform this action!")); } else { req.authUser.handle = config.M2M_AUDIT_HANDLE; + req.authUser.userId = config.M2M_AUDIT_USERID; req.userToken = req.headers.authorization.split(" ")[1]; next(); } diff --git a/config/default.js b/config/default.js index c4867fdb..3c4f0045 100644 --- a/config/default.js +++ b/config/default.js @@ -42,7 +42,9 @@ module.exports = { // above AWS_REGION is used if we use AWS ES HOST: process.env.ES_HOST || "localhost:9200", API_VERSION: process.env.ES_API_VERSION || "6.8", + OPENSEARCH: process.env.OPENSEARCH || "false", ES_INDEX: process.env.ES_INDEX || "challenge", + ES_TYPE: process.env.ES_TYPE || "_doc", ES_REFRESH: process.env.ES_REFRESH || "true", TEMP_REINDEXING: process.env.TEMP_REINDEXING || true, // if true, it won't delete the existing index when reindexing data }, @@ -95,6 +97,7 @@ module.exports = { DEFAULT_CONFIDENTIALITY_TYPE: process.env.DEFAULT_CONFIDENTIALITY_TYPE || "public", M2M_AUDIT_HANDLE: process.env.M2M_AUDIT_HANDLE || "tcwebservice", + M2M_AUDIT_USERID: process.env.M2M_AUDIT_USERID || 22838965, FORUM_TITLE_LENGTH_LIMIT: process.env.FORUM_TITLE_LENGTH_LIMIT || 90, diff --git a/package.json b/package.json index 831428c8..0f56204a 100644 --- a/package.json +++ b/package.json @@ -35,14 +35,15 @@ "chai-http": "^4.2.1", "mocha": "^6.1.4", "mocha-prepare": "^0.1.0", + "nodemon": "^2.0.20", "nyc": "^14.0.0", - "prettier": "^2.8.1", - "nodemon": "^2.0.20" + "prettier": "^2.8.1" }, "dependencies": { + "@grpc/grpc-js": "^1.8.12", "@opensearch-project/opensearch": "^2.2.0", - "@topcoder-framework/domain-challenge": "^0.7.0", - "@topcoder-framework/lib-common": "^0.7.0", + "@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", @@ -50,12 +51,15 @@ "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", "express": "^4.15.4", "express-fileupload": "^1.1.6", "express-interceptor": "^1.2.0", "get-parameter-names": "^0.3.0", + "http-aws-es": "^6.0.0", "http-status-codes": "^1.3.0", "joi": "^14.0.0", "jsonwebtoken": "^8.3.0", diff --git a/src/common/challenge-helper.js b/src/common/challenge-helper.js index 52a5c346..b6a3db22 100644 --- a/src/common/challenge-helper.js +++ b/src/common/challenge-helper.js @@ -5,9 +5,12 @@ const HttpStatus = require("http-status-codes"); const _ = require("lodash"); const errors = require("./errors"); const config = require("config"); +const helper = require("./helper"); +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 { /** @@ -43,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 { @@ -75,6 +78,273 @@ class ChallengeHelper { } } } + + async validateCreateChallengeRequest(currentUser, challenge) { + // projectId is required for non self-service challenges + if (challenge.legacy.selfService == null && challenge.projectId == null) { + throw new errors.BadRequestError("projectId is required for non self-service challenges."); + } + + if (challenge.status === constants.challengeStatuses.Active) { + throw new errors.BadRequestError( + "You cannot create an Active challenge. Please create a Draft challenge and then change the status to Active." + ); + } + + helper.ensureNoDuplicateOrNullElements(challenge.tags, "tags"); + helper.ensureNoDuplicateOrNullElements(challenge.groups, "groups"); + // helper.ensureNoDuplicateOrNullElements(challenge.terms, 'terms') + // helper.ensureNoDuplicateOrNullElements(challenge.events, 'events') + + // 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 00cb3467..e5228f09 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -19,6 +19,7 @@ const xss = require("xss"); const logger = require("./logger"); const { Client: ESClient } = require("@opensearch-project/opensearch"); +const elasticsearch = require("elasticsearch"); const projectHelper = require("./project-helper"); const m2mHelper = require("./m2m-helper"); @@ -449,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"); } @@ -838,12 +842,31 @@ function getESClient() { } const esHost = config.get("ES.HOST"); - esClient = new ESClient({ - node: esHost, - ssl: { - rejectUnauthorized: false, - }, - }); + if (config.get("ES.OPENSEARCH") == "false") { + if (/.*amazonaws.*/.test(esHost)) { + esClient = elasticsearch.Client({ + apiVersion: config.get("ES.API_VERSION"), + hosts: esHost, + connectionClass: require("http-aws-es"), // eslint-disable-line global-require + amazonES: { + region: config.get("AMAZON.AWS_REGION"), + credentials: new AWS.EnvironmentCredentials("AWS"), + }, + }); + } else { + esClient = new elasticsearch.Client({ + apiVersion: config.get("ES.API_VERSION"), + hosts: esHost, + }); + } + } else { + esClient = new ESClient({ + node: esHost, + ssl: { + rejectUnauthorized: false, + }, + }); + } return esClient; } @@ -922,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, { @@ -1103,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 b3eb6923..1ffb99a7 100644 --- a/src/common/phase-helper.js +++ b/src/common/phase-helper.js @@ -16,11 +16,6 @@ const errors = require("./errors"); const phaseService = require("../services/PhaseService"); const timelineTemplateService = require("../services/TimelineTemplateService"); -// const timelineTemplateDomain = new TimelineTemplateDomain( -// GRPC_CHALLENGE_SERVER_HOST, -// GRPC_CHALLENGE_SERVER_PORT -// ); - const phaseDomain = new PhaseDomain(GRPC_CHALLENGE_SERVER_HOST, GRPC_CHALLENGE_SERVER_PORT); class ChallengePhaseHelper { @@ -163,8 +158,133 @@ class ChallengePhaseHelper { p.constraints = []; } } + } + + async populatePhasesForChallengeCreation(phases, startDate, timelineTemplateId) { + if (_.isUndefined(timelineTemplateId)) { + throw new errors.BadRequestError(`Invalid timeline template ID: ${timelineTemplateId}`); + } + const { timelineTempate } = await this.getTemplateAndTemplateMap(timelineTemplateId); + const { phaseDefinitionMap } = await this.getPhaseDefinitionsAndMap(); + const finalPhases = _.map(timelineTempate, (phaseFromTemplate) => { + const phaseDefinition = phaseDefinitionMap.get(phaseFromTemplate.phaseId); + const phaseFromInput = _.find(phases, (p) => p.phaseId === phaseFromTemplate.phaseId); + const phase = { + id: uuid(), + phaseId: phaseFromTemplate.phaseId, + name: phaseDefinition.name, + description: phaseDefinition.description, + duration: _.defaultTo(_.get(phaseFromInput, "duration"), phaseFromTemplate.defaultDuration), + isOpen: false, + predecessor: phaseFromTemplate.predecessor, + constraints: _.defaultTo(_.get(phaseFromInput, "constraints"), []), + scheduledStartDate: undefined, + scheduledEndDate: undefined, + actualStartDate: undefined, + actualEndDate: undefined, + }; + if (_.isUndefined(phase.predecessor)) { + if (_.isUndefined(_.get(phaseFromInput, "scheduledStartDate"))) { + phase.scheduledStartDate = moment(startDate).toDate().toISOString(); + } else { + phase.scheduledStartDate = moment(_.get(phaseFromInput, "scheduledStartDate")).toDate().toISOString(); + } + phase.scheduledEndDate = moment(phase.scheduledStartDate) + .add(phase.duration, "seconds") + .toDate().toISOString(); + } + return phase; + }); + for (let phase of finalPhases) { + if (_.isUndefined(phase.predecessor)) { + continue; + } + const precedecessorPhase = _.find(finalPhases, { + phaseId: phase.predecessor, + }); + if (phase.name === "Iterative Review Phase") { + phase.scheduledStartDate = precedecessorPhase.scheduledStartDate; + } else { + phase.scheduledStartDate = precedecessorPhase.scheduledEndDate; + } + phase.scheduledEndDate = moment(phase.scheduledStartDate) + .add(phase.duration, "seconds") + .toDate().toISOString(); + } + return finalPhases; + } + + async populatePhasesForChallengeUpdate( + challengePhases, + newPhases, + timelineTemplateId, + isBeingActivated + ) { + const { timelineTempate, timelineTemplateMap } = await this.getTemplateAndTemplateMap( + timelineTemplateId + ); + const { phaseDefinitionMap } = await this.getPhaseDefinitionsAndMap(); - console.log("Phases", JSON.stringify(phases, null, 2)); + const updatedPhases = _.map(challengePhases, (phase) => { + const phaseFromTemplate = timelineTemplateMap.get(phase.phaseId); + const phaseDefinition = phaseDefinitionMap.get(phase.phaseId); + const updatedPhase = { + ...phase, + predecessor: phaseFromTemplate.predecessor, + description: phaseDefinition.description, + }; + if (!_.isUndefined(phase.actualEndDate)) { + return updatedPhase; + } + if (updatedPhase.name === "Iterative Review Phase") { + return updatedPhase; + } + const newPhase = _.find(newPhases, (p) => p.phaseId === updatedPhase.phaseId); + if (_.isUndefined(newPhase) && !isBeingActivated) { + return updatedPhase; + } + updatedPhase.duration = _.defaultTo(_.get(newPhase, "duration"), updatedPhase.duration); + if (_.isUndefined(updatedPhase.predecessor)) { + if ( + isBeingActivated && + moment( + _.defaultTo(_.get(newPhase, "scheduledStartDate"), updatedPhase.scheduledStartDate) + ).isSameOrBefore(moment()) + ) { + updatedPhase.isOpen = true; + updatedPhase.scheduledStartDate = moment().toDate().toISOString(); + updatedPhase.actualStartDate = updatedPhase.scheduledStartDate; + } else if ( + updatedPhase.isOpen === false && + !_.isUndefined(_.get(newPhase, "scheduledStartDate")) + ) { + updatedPhase.scheduledStartDate = moment(newPhase.scheduledStartDate).toDate().toISOString(); + } + updatedPhase.scheduledEndDate = moment(updatedPhase.scheduledStartDate) + .add(updatedPhase.duration, "seconds") + .toDate().toISOString(); + } + if (!_.isUndefined(newPhase) && !_.isUndefined(newPhase.constraints)) { + updatedPhase.constraints = newPhase.constraints; + } + return updatedPhase; + }); + for (let phase of updatedPhases) { + if (_.isUndefined(phase.predecessor)) { + continue; + } + if (phase.name === "Iterative Review Phase") { + continue; + } + const precedecessorPhase = _.find(updatedPhases, { + phaseId: phase.predecessor, + }); + phase.scheduledStartDate = precedecessorPhase.scheduledEndDate; + phase.scheduledEndDate = moment(phase.scheduledStartDate) + .add(phase.duration, "seconds") + .toDate().toISOString(); + } + return updatedPhases; } async validatePhases(phases) { 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/scripts/sync-es.js b/src/scripts/sync-es.js index b808d155..b5ba3388 100644 --- a/src/scripts/sync-es.js +++ b/src/scripts/sync-es.js @@ -15,6 +15,7 @@ async function indexChallenge(challenge) { try { await esClient.update({ index: config.get("ES.ES_INDEX"), + type: config.get("ES.OPENSEARCH") == "false" ? config.get("ES.ES_TYPE") : undefined, id: challenge.id, body: { doc: challenge, doc_as_upsert: true }, }); diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index 041f7fb5..7d7064bd 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -30,62 +30,23 @@ const phaseHelper = require("../common/phase-helper"); const projectHelper = require("../common/project-helper"); const challengeHelper = require("../common/challenge-helper"); +const { Metadata: GrpcMetadata } = require("@grpc/grpc-js"); + const esClient = helper.getESClient(); -const { ChallengeDomain, Challenge } = 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 * @@ -175,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 @@ -224,7 +158,7 @@ async function searchByLegacyId(currentUser, legacyId, page, perPage) { }, }, }; - + 1493; logger.debug(`es Query ${JSON.stringify(esQuery)}`); let docs; try { @@ -916,7 +850,10 @@ async function searchChallenges(currentUser, criteria) { // Search with constructed query let docs; try { - docs = (await esClient.search(esQuery)).body; + docs = + config.get("ES.OPENSEARCH") == "false" + ? await esClient.search(esQuery) + : (await esClient.search(esQuery)).body; } catch (e) { // Catch error when the ES is fresh and has no data logger.error(`Query Error from ES ${JSON.stringify(e, null, 4)}`); @@ -977,7 +914,6 @@ async function searchChallenges(currentUser, criteria) { return { total, page, perPage, result }; } - searchChallenges.schema = { currentUser: Joi.any(), criteria: Joi.object() @@ -1048,27 +984,6 @@ searchChallenges.schema = { .unknown(true), }; -async function validateCreateChallengeRequest(currentUser, challenge) { - // projectId is required for non self-service challenges - if (challenge.legacy.selfService == null && challenge.projectId == null) { - throw new errors.BadRequestError("projectId is required for non self-service challenges."); - } - - if (challenge.status === constants.challengeStatuses.Active) { - throw new errors.BadRequestError( - "You cannot create an Active challenge. Please create a Draft challenge and then change the status to Active." - ); - } - - helper.ensureNoDuplicateOrNullElements(challenge.tags, "tags"); - helper.ensureNoDuplicateOrNullElements(challenge.groups, "groups"); - // helper.ensureNoDuplicateOrNullElements(challenge.terms, 'terms') - // helper.ensureNoDuplicateOrNullElements(challenge.events, 'events') - - // check groups authorization - await helper.ensureAccessibleByGroupsAccess(currentUser, challenge); -} - /** * Create challenge. * @param {Object} currentUser the user who perform operation @@ -1077,7 +992,7 @@ async function validateCreateChallengeRequest(currentUser, challenge) { * @returns {Object} the created challenge */ async function createChallenge(currentUser, challenge, userToken) { - await validateCreateChallengeRequest(currentUser, challenge); + await challengeHelper.validateCreateChallengeRequest(currentUser, challenge); if (challenge.legacy.selfService) { // if self-service, create a new project (what about if projectId is provided in the payload? confirm with business!) @@ -1086,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 ); } @@ -1121,8 +1035,10 @@ async function createChallenge(currentUser, challenge, userToken) { } if (!challenge.startDate) { - challenge.startDate = new Date(); - } else challenge.startDate = new Date(challenge.startDate); + challenge.startDate = new Date().toISOString(); + } else { + challenge.startDate = new Date(challenge.startDate).toISOString(); + } const { track, type } = await challengeHelper.validateAndGetChallengeTypeAndTrack(challenge); @@ -1162,18 +1078,11 @@ async function createChallenge(currentUser, challenge, userToken) { throw new errors.BadRequestError(`trackId and typeId are required to create a challenge`); } } - - if (challenge.timelineTemplateId) { - if (!challenge.phases) { - challenge.phases = []; - } - - await phaseHelper.populatePhases( - challenge.phases, - challenge.startDate, - challenge.timelineTemplateId - ); - } + challenge.phases = await phaseHelper.populatePhasesForChallengeCreation( + challenge.phases, + challenge.startDate, + challenge.timelineTemplateId + ); // populate challenge terms // const projectTerms = await helper.getProjectDefaultTerms(challenge.projectId) @@ -1197,86 +1106,59 @@ 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(); - - const ret = await challengeDomain.create(challenge); + if (challenge.startDate != null) challenge.startDate = challenge.startDate; + if (challenge.endDate != null) challenge.endDate = challenge.endDate; + if (challenge.discussions == null) challenge.discussions = []; - console.log("Created Challenge", JSON.stringify(ret, null, 2)); - - ret.numOfSubmissions = 0; - ret.numOfRegistrants = 0; + challenge.metadata = challenge.metadata.map((m) => ({ + name: m.name, + value: typeof m.value === "string" ? m.value : JSON.stringify(m.value), + })); - 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; - } - } + const grpcMetadata = new GrpcMetadata(); - if (track) { - ret.track = track.name; - } + grpcMetadata.set("handle", currentUser.handle); + grpcMetadata.set("userId", currentUser.userId); - if (type) { - ret.type = type.name; - } + convertPrizeSetValuesToCents(challenge.prizeSets); + const ret = await challengeDomain.create(challenge, grpcMetadata); + convertPrizeSetValuesToDollars(ret.prizeSets, ret.overview); - 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; - }); + ret.numOfSubmissions = 0; + ret.numOfRegistrants = 0; - // Create in ES - await esClient.create({ - index: config.get("ES.ES_INDEX"), - 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 - /** Disable Creating Resources locally (because challenge is not being indexed in ES and will result in challenge NOT FOUND error) - if (challenge.legacy.selfService) { - if (currentUser.handle) { - await helper.createResource(ret.id, ret.createdBy, config.CLIENT_MANAGER_ROLE_ID); - } - } else { - // if created by a user, add user as a manager, but only if *not* a self-service challenge - if (currentUser.handle) { - // logger.debug(`Adding user as manager ${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 { - // logger.debug(`Not adding manager ${currentUser.sub} ${JSON.stringify(currentUser)}`) + if (currentUser.handle) { + await helper.createResource(ret.id, ret.createdBy, config.MANAGER_ROLE_ID); + } } } - */ // post bus event await helper.postBusEvent(constants.Topics.ChallengeCreated, ret); return ret; } - createChallenge.schema = { currentUser: Joi.any(), challenge: Joi.object() @@ -1376,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()), @@ -1419,12 +1301,20 @@ async function getChallenge(currentUser, id, checkIfExists) { // _id: id // })) try { - challenge = ( - await esClient.getSource({ + if (config.get("ES.OPENSEARCH") == "true") { + challenge = ( + await esClient.getSource({ + index: config.get("ES.ES_INDEX"), + id, + }) + ).body; + } else { + challenge = await esClient.getSource({ index: config.get("ES.ES_INDEX"), + type: config.get("ES.ES_TYPE"), id, - }) - ).body; + }); + } } catch (e) { if (e.statusCode === HttpStatus.NOT_FOUND) { throw new errors.NotFoundError(`Challenge of id ${id} is not found.`); @@ -1485,7 +1375,6 @@ async function getChallenge(currentUser, id, checkIfExists) { return challenge; } - getChallenge.schema = { currentUser: Joi.any(), id: Joi.id(), @@ -1535,7 +1424,6 @@ async function getChallengeStatistics(currentUser, id) { } return _.map(_.keys(map), (userId) => map[userId]); } - getChallengeStatistics.schema = { currentUser: Joi.any(), id: Joi.id(), @@ -1608,103 +1496,107 @@ 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); +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; - delete data.cancelReason; - 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 { billingAccountId, markup } = await projectHelper.getProjectBillingInformation(projectId); - const challenge = await challengeDomain.lookup(getLookupCriteria("id", challengeId)); - let dynamicDescription = _.cloneDeep(data.description || challenge.description); - 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 (billingAccountId && _.isUndefined(_.get(challenge, "billing.billingAccountId"))) { + _.set(data, "billing.billingAccountId", billingAccountId); + _.set(data, "billing.markup", markup || 0); } - if ( - challenge.legacy.selfService && - data.status === constants.challengeStatuses.Draft && - challenge.status !== constants.challengeStatuses.Draft - ) { - sendSubmittedEmail = true; + + // Make sure the user cannot change the direct project ID + if (data.legacy && data.legacy.directProjectId) { + _.unset(data, "legacy.directProjectId", directProjectId); } - // 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; + + /* BEGIN self-service stuffs */ + + // 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); } - } 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) { if ( @@ -1725,7 +1617,11 @@ async function update(currentUser, challengeId, data, isFull) { "Cannot Activate this project, it has no active billing account." ); } + if (challenge.status === constants.challengeStatuses.Draft) { + isChallengeBeingActivated = true; + } } + if ( data.status === constants.challengeStatuses.CancelledRequirementsInfeasible || data.status === constants.challengeStatuses.CancelledPaymentFailed @@ -1737,6 +1633,7 @@ async function update(currentUser, challengeId, data, isFull) { } sendRejectedEmail = true; } + if (data.status === constants.challengeStatuses.Completed) { if ( !_.get(challenge, "legacy.pureV5Task") && @@ -1749,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) { @@ -1858,55 +1680,10 @@ 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; + const timelineTemplateChanged = false; if (!_.get(data, "legacy.pureV5") && !_.get(challenge, "legacy.pureV5")) { if ( finalStatus !== constants.challengeStatuses.New && @@ -1919,6 +1696,7 @@ async function update(currentUser, challengeId, data, isFull) { } else if (finalTimelineTemplateId !== challenge.timelineTemplateId) { // make sure there are no previous phases if the timeline template has changed challenge.phases = []; + timelineTemplateChanged = true; } if (data.prizeSets && data.prizeSets.length > 0) { @@ -1936,17 +1714,17 @@ 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 } }); } } - if (data.phases || data.startDate) { + if (data.phases || data.startDate || timelineTemplateChanged) { if ( challenge.status === constants.challengeStatuses.Completed || challenge.status.indexOf(constants.challengeStatuses.Cancelled) > -1 @@ -1955,55 +1733,30 @@ async function update(currentUser, challengeId, data, isFull) { `Challenge phase/start date can not be modified for Completed or Cancelled challenges.` ); } - - if (data.phases && data.phases.length > 0) { - for (let i = 0; i < challenge.phases.length; i += 1) { - const updatedPhaseInfo = _.find( - data.phases, - (p) => p.phaseId === challenge.phases[i].phaseId - ); - if (updatedPhaseInfo) { - _.extend(challenge.phases[i], updatedPhaseInfo); - } - } - if (challenge.phases.length === 0 && data.phases && data.phases.length > 0) { - challenge.phases = data.phases; - } - } - - const newPhases = _.cloneDeep(challenge.phases) || []; const newStartDate = data.startDate || challenge.startDate; + let newPhases; + if (timelineTemplateChanged) { + newPhases = await phaseHelper.populatePhasesForChallengeCreation( + data.phases, + newStartDate, + finalTimelineTemplateId + ); + } else if (data.startDate || (data.phases && data.phases.length > 0)) { + newPhases = await phaseHelper.populatePhasesForChallengeUpdate( + challenge.phases, + data.phases, + challenge.timelineTemplateId, + isChallengeBeingActivated + ); + } - await PhaseService.validatePhases(newPhases); - - // populate phases - await phaseHelper.populatePhases( - newPhases, - newStartDate, - data.timelineTemplateId || challenge.timelineTemplateId - ); 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); } @@ -2055,642 +1808,153 @@ 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") { + 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"))) { + const challengeResources = await helper.getChallengeResources(challengeId); + const registrants = _.filter( + challengeResources, + (r) => r.roleId === config.SUBMITTER_ROLE_ID + ); if ( - _.isUndefined(challenge[key]) || - challenge[key].length !== value.length || - _.intersection(challenge[key], value).length !== value.length + !_.find( + registrants, + (r) => _.toString(r.memberId) === _.toString(_.get(data, "task.memberId")) + ) ) { - op = "$PUT"; + throw new errors.BadRequestError( + `Member ${_.get( + data, + "task.memberId" + )} is not a submitter resource of challenge ${challengeId}` + ); } - // } 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"])); - - if (_.get(type, "isTask")) { - if (!_.isEmpty(_.get(data, "task.memberId"))) { - const challengeResources = await helper.getChallengeResources(challengeId); - const registrants = _.filter( - challengeResources, - (r) => r.roleId === config.SUBMITTER_ROLE_ID - ); - if ( - !_.find( - registrants, - (r) => _.toString(r.memberId) === _.toString(_.get(data, "task.memberId")) - ) - ) { - throw new errors.BadRequestError( - `Member ${_.get( - data, - "task.memberId" - )} is not a submitter resource of challenge ${challengeId}` - ); - } - } - } - - 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(newTermsOfUse)) { - challenge.terms = newTermsOfUse; - data.terms = newTermsOfUse; - } - - if (challenge.phases && challenge.phases.length > 0) { - await getPhasesAndPopulate(challenge); - } - - // Populate challenge.track and challenge.type based on the track/type IDs - - if (track) { - challenge.track = track.name; - } - if (type) { - challenge.type = type.name; - } - - 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; - } - } - } 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"), - 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; -} - -/** - * 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)); + if (!_.isUndefined(data.terms)) { + await helper.validateChallengeTerms(data.terms.map((t) => t.id)); + } + + if (data.phases && data.phases.length > 0) { + await getPhasesAndPopulate(data); + + if (deepEqual(data.phases, challenge.phases)) { + delete data.phases; + } + } + + try { + const updateInput = sanitizeRepeatedFieldsInUpdateRequest(data); + + if (!_.isEmpty(updateInput)) { + const grpcMetadata = new GrpcMetadata(); + + 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; + } + + const updatedChallenge = await challengeDomain.lookup(getLookupCriteria("id", challengeId)); + convertPrizeSetValuesToDollars(updatedChallenge.prizeSets, updatedChallenge.overview); + + // post bus event + logger.debug( + `Post Bus Event: ${constants.Topics.ChallengeUpdated} ${JSON.stringify(updatedChallenge)}` + ); + + enrichChallengeForResponse(updatedChallenge, track, type); + + await helper.postBusEvent(constants.Topics.ChallengeUpdated, updatedChallenge, { + key: + updatedChallenge.status === "Completed" + ? `${updatedChallenge.id}:${updatedChallenge.status}` + : undefined, + }); + + 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, + } + ); + } + } + } + + return updatedChallenge; } -partiallyUpdateChallenge.schema = { +updateChallenge.schema = { currentUser: Joi.any(), challengeId: Joi.id(), data: Joi.object() @@ -2714,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(), @@ -2728,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() @@ -2742,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() @@ -2765,7 +2031,8 @@ partiallyUpdateChallenge.schema = { }) .unknown(true) ) - .min(1), + .min(1) + .optional(), events: Joi.array().items( Joi.object() .keys({ @@ -2774,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() @@ -2820,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( @@ -2835,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 @@ -2883,10 +2296,10 @@ module.exports = { searchChallenges, createChallenge, getChallenge, - fullyUpdateChallenge, - partiallyUpdateChallenge, + updateChallenge, deleteChallenge, getChallengeStatistics, sendNotifications, }; + logger.buildService(module.exports); diff --git a/src/services/ChallengeTimelineTemplateService.js b/src/services/ChallengeTimelineTemplateService.js index 3f20a688..c0c1fd24 100644 --- a/src/services/ChallengeTimelineTemplateService.js +++ b/src/services/ChallengeTimelineTemplateService.js @@ -56,6 +56,8 @@ searchChallengeTimelineTemplates.schema = { trackId: Joi.optionalId(), timelineTemplateId: Joi.optionalId(), isDefault: Joi.boolean(), + page: Joi.page(), + perPage: Joi.perPage(), }), }; diff --git a/yarn.lock b/yarn.lock index 302108a1..938144b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -125,18 +125,18 @@ enabled "2.0.x" kuler "^2.0.0" -"@grpc/grpc-js@^1.8.0": - 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== +"@grpc/grpc-js@^1.8.0", "@grpc/grpc-js@^1.8.12": + 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.0": - version "0.7.0" - resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/client-relational/-/client-relational-0.7.0.tgz#bd219fb466ce2d436ca393b1f1bb4bdd0f05be80" - integrity sha512-AXkKyzmKfQap+eib9FehQZbZ7oAYGW+41gMXNFpxmqrZ0/TMgh8znnaw6uPmwyalVPh1bZdvxIGadCQxgi3jWw== +"@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.0" - topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.29" + "@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.0": - version "0.7.0" - resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/domain-challenge/-/domain-challenge-0.7.0.tgz#318acc9fb7cfdd1c837d69eee43f94ae17e30840" - integrity sha512-ekg2oplRLc0UXxzHzm3Eb6YX4iWqJkcg0Nye6g+k93vkGiWZaccW4cujBKZq/39JVx6+Sc1uiAlLbi6gbRY7Jg== +"@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.0" - "@topcoder-framework/lib-common" "^0.7.0" - topcoder-interface "github:topcoder-platform/plat-interface-definition#v0.0.29" + "@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.0": - version "0.7.0" - resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com:443/npm/topcoder-framework/@topcoder-framework/lib-common/-/lib-common-0.7.0.tgz#557900413fe2e0b67d233f04c63db2e81eac5dbc" - integrity sha512-3qjcRYGHqRiBWPbOM2C/BwpZEswIqCbc+scskIHmtY/FYn52lTT1w7Cm/KOcgBpE3S/mmWq0YwtZKNNzbRwglA== +"@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.29" + 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" @@ -406,6 +406,13 @@ agent-base@6: dependencies: debug "4" +agentkeepalive@^3.4.1: + version "3.5.2" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-3.5.2.tgz#a113924dd3fa24a0bc3b78108c450c2abee00f67" + integrity sha512-e0L/HNe6qkQ7H19kTlRRqUibEAwDK5AFk6y3PtMsuut2VAH6+Q4xZml1tNDJD7kSAyqmbG/K08K5WEJYtUrSlQ== + dependencies: + humanize-ms "^1.2.1" + ajv@^6.12.3: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -421,6 +428,11 @@ ansi-colors@3.2.3: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.3.tgz#57d35b8686e851e2cc04c403f1c00203976a1813" integrity sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw== +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA== + ansi-regex@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.1.tgz#123d6479e92ad45ad897d4054e3c7ca7db4944e1" @@ -436,6 +448,11 @@ ansi-regex@^5.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + integrity sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA== + ansi-styles@^3.2.0, ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -549,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" @@ -792,6 +809,17 @@ chai@^4.2.0: pathval "^1.1.1" type-detect "^4.0.5" +chalk@^1.0.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + integrity sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A== + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + chalk@^2.0.0, chalk@^2.0.1: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -1078,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" @@ -1158,6 +1209,15 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== +elasticsearch@^16.7.3: + version "16.7.3" + resolved "https://registry.yarnpkg.com/elasticsearch/-/elasticsearch-16.7.3.tgz#bf0e1cc129ab2e0f06911953a1b1f3c740715fab" + integrity sha512-e9kUNhwnIlu47fGAr4W6yZJbkpsgQJB0TqNK8rCANe1J4P65B1sGnbCFTgcKY3/dRgCWnuP1AJ4obvzW604xEQ== + dependencies: + agentkeepalive "^3.4.1" + chalk "^1.0.0" + lodash "^4.17.10" + emoji-regex@^7.0.1: version "7.0.3" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" @@ -1230,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" @@ -1263,7 +1338,7 @@ escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== -escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5: +escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== @@ -1635,6 +1710,13 @@ har-validator@~5.1.3: ajv "^6.12.3" har-schema "^2.0.0" +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + integrity sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg== + dependencies: + ansi-regex "^2.0.0" + has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" @@ -1718,6 +1800,11 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== +http-aws-es@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/http-aws-es/-/http-aws-es-6.0.0.tgz#1528978d2bee718b8732dcdced0856efa747aeff" + integrity sha512-g+qp7J110/m4aHrR3iit4akAlnW0UljZ6oTq/rCcbsI8KP9x+95vqUtx49M2XQ2JMpwJio3B6gDYx+E8WDxqiA== + http-errors@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" @@ -1760,6 +1847,13 @@ https-proxy-agent@^5.0.0: agent-base "6" debug "4" +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== + dependencies: + ms "^2.0.0" + iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -1805,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== @@ -1824,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== @@ -1890,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== @@ -1933,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" @@ -1963,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" @@ -2010,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" @@ -2017,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" @@ -2317,7 +2439,7 @@ lodash@4.17.15: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== -lodash@^4.17.15, lodash@^4.17.19: +lodash@^4.17.10, lodash@^4.17.15, lodash@^4.17.19: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -2521,7 +2643,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.1.1, ms@^2.1.2, ms@^2.1.3: +ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.2, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -2571,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" @@ -2654,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" @@ -2827,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" @@ -3317,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" @@ -3389,6 +3526,13 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" +strip-ansi@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg== + dependencies: + ansi-regex "^2.0.0" + strip-ansi@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" @@ -3443,6 +3587,11 @@ supports-color@6.0.0: dependencies: has-flag "^3.0.0" +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + integrity sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g== + supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -3528,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.29": +"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/6ad366c0dc28a8452bd71ed87d718ac559bee62b" + resolved "https://codeload.github.com/topcoder-platform/plat-interface-definition/tar.gz/8ed5b7686125a17209c85c33f69c92476625e3c1" topo@3.x.x: version "3.0.3" @@ -3702,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"