diff --git a/.circleci/config.yml b/.circleci/config.yml index cc1a3e94..b5bbd3f0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -91,6 +91,7 @@ workflows: only: - dev - hotfix/budget-update + - CORE-140 - "build-qa": context: org-global diff --git a/config/default.js b/config/default.js index 3c4f0045..c0a71032 100644 --- a/config/default.js +++ b/config/default.js @@ -128,4 +128,6 @@ module.exports = { INTERNAL_CACHE_TTL: process.env.INTERNAL_CACHE_TTL || 1800, GRPC_CHALLENGE_SERVER_HOST: process.env.GRPC_DOMAIN_CHALLENGE_SERVER_HOST || "localhost", GRPC_CHALLENGE_SERVER_PORT: process.env.GRPC_DOMAIN_CHALLENGE_SERVER_PORT || 8888, + GRPC_ACL_SERVER_HOST: process.env.GRPC_ACL_SERVER_HOST || "localhost", + GRPC_ACL_SERVER_PORT: process.env.GRPC_ACL_SERVER_PORT || 8889, }; diff --git a/package.json b/package.json index 2226eb2f..c79e4272 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@grpc/grpc-js": "^1.8.12", "@opensearch-project/opensearch": "^2.2.0", "@topcoder-framework/domain-challenge": "^0.24.0", + "@topcoder-framework/domain-acl": "^0.24.0", "@topcoder-framework/lib-common": "^0.24.0", "aws-sdk": "^2.1145.0", "axios": "^0.19.0", diff --git a/src/common/srm-helper.js b/src/common/srm-helper.js new file mode 100644 index 00000000..a87791e0 --- /dev/null +++ b/src/common/srm-helper.js @@ -0,0 +1,251 @@ +const _ = require("lodash"); +const moment = require("moment"); + +const SRMScheduleKeyMappings = _.reduce( + [ + "roundId", + "name", + "shortName", + "contestName", + "roundType", + "status", + "registrationStartTime", + "registrationEndTime", + "codingStartTime", + "codingEndTime", + "intermissionStartTime", + "intermissionEndTime", + "challengeStartTime", + "challengeEndTime", + "systestStartTime", + "systestEndTime", + ], + (acc, field) => ({ ...acc, [_.toLower(field)]: field }), + {} +); + +const PracticeProblemsKeyMappings = _.reduce( + [ + "problemId", + "componentId", + "roomId", + "roundId", + "divisionId", + "problemName", + "problemType", + "difficulty", + "status", + "points", + "myPoints", + ], + (acc, field) => ({ ...acc, [_.toLower(field)]: field }), + {} +); + +/** + * Get schedule query + * @param {Object} filter the query filter + * @param {Array} filter.statuses the statues + * @param {Date} filter.registrationStartTimeAfter the start of the registration time + * @param {Date=} filter.registrationStartTimeBefore the end of the registration time + * @param {String} filter.sortBy the sort field + * @param {String} filter.sortOrder the sort order + * @param {Number} filter.page the sort order + * @param {Number} filter.perPage the sort order + */ +function getSRMScheduleQuery(filter) { + const offset = (filter.page - 1) * filter.perPage; + let sortBy = filter.sortBy; + if (filter.sortBy === "registrationStartTime") { + sortBy = "reg.start_time"; + } else if (filter.sortBy === "codingStartTime") { + sortBy = "coding.start_time"; + } else if (filter.sortBy === "challengeStartTime") { + sortBy = "challenge.start_time"; + } + const statuses = _.join( + _.map(filter.statuses, (s) => `'${_.toUpper(s)}'`), + "," + ); + const registrationTimeFilter = `reg.start_time >= '${moment( + filter.registrationStartTimeAfter + ).format("yyyy-MM-DD HH:mm:ss")}'${ + filter.registrationStartTimeBefore + ? ` AND reg.start_time <= '${moment(filter.registrationStartTimeBefore).format( + "yyyy-MM-DD HH:mm:ss" + )}'` + : "" + }`; + + const query = `SELECT + SKIP ${offset} + FIRST ${filter.perPage} + r.round_id AS roundId + , r.name AS name + , r.short_name AS shortName + , c.name AS contestName + , rt.round_type_desc AS roundType + , r.status AS status + , reg.start_time AS registrationStartTime + , reg.end_time AS registrationEndTime + , coding.start_time AS codingStartTime + , coding.end_time AS codingEndTime + , intermission.start_time AS intermissionStartTime + , intermission.end_time AS intermissionEndTime + , challenge.start_time AS challengeStartTime + , challenge.end_time AS challengeEndTime + , systest.start_time AS systestStartTime + , systest.end_time AS systestEndTime + FROM + informixoltp:contest AS c + INNER JOIN informixoltp:round AS r ON r.contest_id = c.contest_id + INNER JOIN informixoltp:round_type_lu AS rt ON rt.round_type_id = r.round_type_id + LEFT JOIN informixoltp:round_segment AS reg ON reg.round_id = r.round_id AND reg.segment_id = 1 + LEFT JOIN informixoltp:round_segment AS coding ON coding.round_id = r.round_id AND coding.segment_id = 2 + LEFT JOIN informixoltp:round_segment AS intermission ON intermission.round_id = r.round_id AND intermission.segment_id = 3 + LEFT JOIN informixoltp:round_segment AS challenge ON challenge.round_id = r.round_id AND challenge.segment_id = 4 + LEFT JOIN informixoltp:round_segment AS systest ON systest.round_id = r.round_id AND systest.segment_id = 5 + WHERE + r.round_type_id in (1,2,10) AND + UPPER(r.status) in (${statuses}) AND + ${registrationTimeFilter} + ORDER BY ${sortBy} ${filter.sortOrder}`; + return query; +} + +/** + * Get schedule query + * @param {Object} criteria the query criteria + * @param {String} criteria.userId the user id + * @param {String} criteria.sortBy the sort field + * @param {String} criteria.sortOrder the sort order + * @param {Number} criteria.page the sort order + * @param {Number} criteria.perPage the sort order + * @param {String=} criteria.difficulty the sort order + * @param {String=} criteria.status the sort order + * @param {Number=} criteria.pointsLowerBound the sort order + * @param {Number=} criteria.pointsUpperBound the statues + * @param {String=} criteria.problemName the start of the registration time + */ +function getPracticeProblemsQuery(criteria) { + const offset = (criteria.page - 1) * criteria.perPage; + let sortBy = criteria.sortBy; + if (criteria.sortBy === "problemId") { + sortBy = "p.problem_id"; + } else if (criteria.sortBy === "problemName") { + sortBy = "p.name"; + } else if (criteria.sortBy === "problemType") { + sortBy = "ptl.problem_type_desc"; + } else if (criteria.sortBy === "points") { + sortBy = "rc.points"; + } else if (criteria.sortBy === "difficulty") { + sortBy = "p.proposed_difficulty_id"; + } else if (criteria.sortBy === "status") { + sortBy = "pcs.status_id"; + } else if (criteria.sortBy === "myPoints") { + sortBy = "NVL(pcs.points, 0)"; + } + const filters = []; + if (criteria.difficulty) { + if (criteria.difficulty === "easy") { + filters.push(`p.proposed_difficulty_id=1`); + } else if (criteria.difficulty === "medium") { + filters.push(`p.proposed_difficulty_id=2`); + } else if (criteria.difficulty === "hard") { + filters.push(`p.proposed_difficulty_id=3`); + } + } + if (criteria.status) { + if (criteria.status === "new") { + filters.push("NVL(pcs.status_id, 0) < 120"); + } else if (criteria.status === "viewed") { + filters.push("pcs.status_id >= 120 AND pcs.status_id != 150"); + } else if (criteria.status === "solved") { + filters.push("pcs.status_id = 150"); + } + } + if (criteria.pointsLowerBound) { + filters.push(`rc.points >= ${criteria.pointsLowerBound}`); + } + if (criteria.pointsUpperBound) { + filters.push(`rc.points <= ${criteria.pointsUpperBound}`); + } + if (criteria.problemName) { + filters.push( + `lower(p.name) like '%${_.toLower(_.replace(criteria.problemName, /[^a-z0-9]/gi, ""))}%'` + ); + } + + const queryCount = `SELECT count(*) AS count`; + + const querySelect = `SELECT + SKIP ${offset} + FIRST ${criteria.perPage} + p.problem_id AS problemId + , c.component_id AS componentId + , ro.room_id AS roomId + , rc.round_id AS roundId + , rc.division_id AS divisionId + , p.name AS problemName + , ptl.problem_type_desc AS problemType + , CASE WHEN (p.problem_type_id = 1 AND p.proposed_difficulty_id = 1) THEN 'Easy'::nvarchar(50) + WHEN (p.problem_type_id = 1 AND p.proposed_difficulty_id = 2) THEN 'Medium'::nvarchar(50) + WHEN (p.problem_type_id = 1 AND p.proposed_difficulty_id = 3) THEN 'Hard'::nvarchar(50) + END AS difficulty + , rc.points AS points + , CASE WHEN NVL(pcs.status_id, 0) < 120 THEN 'New'::nvarchar(50) + WHEN pcs.status_id = 150 THEN 'Solved'::nvarchar(50) + WHEN pcs.status_id >= 120 AND pcs.status_id != 150 THEN 'Viewed'::nvarchar(50) + END AS status + , NVL(pcs.points, 0) AS myPoints`; + + const queryFrom = `FROM informixoltp:problem p + INNER JOIN informixoltp:problem_type_lu ptl ON ptl.problem_type_id = p.problem_type_id + INNER JOIN informixoltp:component c ON c.problem_id = p.problem_id + INNER JOIN informixoltp:round_component rc ON rc.component_id = c.component_id + INNER JOIN informixoltp:round r ON r.round_id = rc.round_id AND r.status = 'A' AND r.round_type_id = 3 + INNER JOIN informixoltp:room ro ON ro.round_id = rc.round_id AND ro.room_type_id = 3 + LEFT JOIN informixoltp:component_state pcs ON pcs.round_id = rc.round_id AND pcs.component_id = c.component_id AND pcs.coder_id = ${criteria.userId}`; + + const queryWhere = filters.length ? `WHERE ${_.join(filters, " AND ")}` : ""; + + const queryOrder = `ORDER BY ${sortBy} ${criteria.sortOrder}`; + + const query = `${querySelect} ${queryFrom} ${queryWhere} ${queryOrder}`; + const countQuery = `${queryCount} ${queryFrom} ${queryWhere}`; + return { query, countQuery }; +} + +function convertSRMScheduleQueryOutput(queryOutput) { + return transformDatabaseResponse(queryOutput, SRMScheduleKeyMappings); +} + +function convertPracticeProblemsQueryOutput(queryOutput) { + return transformDatabaseResponse(queryOutput, PracticeProblemsKeyMappings); +} + +function transformDatabaseResponse(databaseResponse, keyMappings) { + const transformedData = []; + + if (databaseResponse && databaseResponse.rows && Array.isArray(databaseResponse.rows)) { + databaseResponse.rows.forEach((row) => { + const record = {}; + if (row.fields && Array.isArray(row.fields)) { + row.fields.forEach((field) => { + const lowercaseKey = field.key.toLowerCase(); + const mappedKey = keyMappings[lowercaseKey] || lowercaseKey; + record[mappedKey] = field.value; + }); + } + transformedData.push(record); + }); + } + return transformedData; +} + +module.exports = { + getSRMScheduleQuery, + convertSRMScheduleQueryOutput, + getPracticeProblemsQuery, + convertPracticeProblemsQueryOutput, +}; diff --git a/src/controllers/ChallengeController.js b/src/controllers/ChallengeController.js index 18c61937..38768dc1 100644 --- a/src/controllers/ChallengeController.js +++ b/src/controllers/ChallengeController.js @@ -126,6 +126,27 @@ async function advancePhase(req, res) { res.send(await service.advancePhase(req, req.authUser, req.params.challengeId, req.body)); } +/** + * Get SRM Schedule + * @param {Object} req the request + * @param {Object} res the response + */ +async function getSRMSchedule(req, res) { + const result = await service.getSRMSchedule(req, req.query); + res.send(result); +} + +/** + * Get Practice Problems + * @param {Object} req the request + * @param {Object} res the response + */ +async function getPracticeProblems(req, res) { + const result = await service.getPracticeProblems(req, req.authUser, req.query); + helper.setResHeaders(req, res, result); + res.send(result.result); +} + module.exports = { searchChallenges, createChallenge, @@ -135,4 +156,6 @@ module.exports = { getChallengeStatistics, sendNotifications, advancePhase, + getSRMSchedule, + getPracticeProblems, }; diff --git a/src/routes.js b/src/routes.js index 74e571c8..8d46e69d 100644 --- a/src/routes.js +++ b/src/routes.js @@ -40,6 +40,19 @@ module.exports = { method: "createRequest", }, }, + "/challenges/srms/schedule": { + get: { + controller: "ChallengeController", + method: "getSRMSchedule", + }, + }, + "/challenges/srms/practice/problems": { + get: { + controller: "ChallengeController", + method: "getPracticeProblems", + auth: "jwt", + }, + }, "/challenges/health": { get: { controller: "HealthController", diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index f0f7b110..c0c28d12 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -33,6 +33,7 @@ const esClient = helper.getESClient(); const PhaseAdvancer = require("../phase-management/PhaseAdvancer"); const { ChallengeDomain } = require("@topcoder-framework/domain-challenge"); +const { QueryDomain } = require("@topcoder-framework/domain-acl"); const { hasAdminRole } = require("../common/role-helper"); const { @@ -44,6 +45,12 @@ const { } = require("../common/challenge-helper"); const deepEqual = require("deep-equal"); const { getM2MToken } = require("../common/m2m-helper"); +const { + getSRMScheduleQuery, + getPracticeProblemsQuery, + convertSRMScheduleQueryOutput, + convertPracticeProblemsQueryOutput, +} = require("../common/srm-helper"); const challengeDomain = new ChallengeDomain( GRPC_CHALLENGE_SERVER_HOST, @@ -65,6 +72,9 @@ const challengeDomain = new ChallengeDomain( }), } ); + +const aclQueryDomain = new QueryDomain(config.GRPC_ACL_SERVER_HOST, config.GRPC_ACL_SERVER_PORT); + const phaseAdvancer = new PhaseAdvancer(challengeDomain); /** @@ -2454,6 +2464,64 @@ async function indexChallengeAndPostToKafka(updatedChallenge, track, type) { }); } +/** + * Get SRM Schedule + * @param {Object} criteria the criteria + */ +async function getSRMSchedule(criteria = {}) { + const sql = getSRMScheduleQuery(criteria); + const result = await aclQueryDomain.rawQuery({ sql }); + return convertSRMScheduleQueryOutput(result); +} + +getSRMSchedule.schema = { + criteria: Joi.object().keys({ + registrationStartTimeAfter: Joi.date().default(new Date()), + registrationStartTimeBefore: Joi.date(), + statuses: Joi.array() + .items(Joi.string().valid(["A", "F", "P"])) + .default(["A", "F", "P"]), + sortBy: Joi.string() + .valid(["registrationStartTime", "codingStartTime", "challengeStartTime"]) + .default("registrationStartTime"), + sortOrder: Joi.string().valid(["asc", "desc"]).default("asc"), + page: Joi.page(), + perPage: Joi.perPage(), + }), +}; + +/** + * Get SRM Schedule + * @param {Object} currentUser the user who perform operation + * @param {Object} criteria the criteria + */ +async function getPracticeProblems(currentUser, criteria = {}) { + criteria.userId = currentUser.userId; + const { query, countQuery } = getPracticeProblemsQuery(criteria); + const resultOutput = await aclQueryDomain.rawQuery({ sql: query }); + const countOutput = await aclQueryDomain.rawQuery({ sql: countQuery }); + const result = convertPracticeProblemsQueryOutput(resultOutput); + const total = countOutput.rows[0].fields[0].value; + return { total, page: criteria.page, perPage: criteria.perPage, result }; +} + +getPracticeProblems.schema = { + currentUser: Joi.any(), + criteria: Joi.object().keys({ + sortBy: Joi.string() + .valid(["problemName", "problemType", "points", "difficulty", "status", "myPoints"]) + .default("problemId"), + sortOrder: Joi.string().valid(["asc", "desc"]).default("desc"), + page: Joi.page(), + perPage: Joi.perPage(), + difficulty: Joi.string().valid(["easy", "medium", "hard"]), + status: Joi.string().valid(["new", "viewed", "solved"]), + pointsLowerBound: Joi.number().integer(), + pointsUpperBound: Joi.number().integer(), + problemName: Joi.string(), + }), +}; + module.exports = { searchChallenges, createChallenge, @@ -2463,6 +2531,8 @@ module.exports = { getChallengeStatistics, sendNotifications, advancePhase, + getSRMSchedule, + getPracticeProblems, }; logger.buildService(module.exports); diff --git a/yarn.lock b/yarn.lock index e8d35dd2..3bccd284 100644 --- a/yarn.lock +++ b/yarn.lock @@ -134,6 +134,14 @@ "@grpc/proto-loader" "^0.7.8" "@types/node" ">=12.12.47" +"@grpc/grpc-js@^1.8.7": + version "1.9.11" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.9.11.tgz#7b21195c910a49c0bb5d0df21d28a30c4e174851" + integrity sha512-QDhMfbTROOXUhLHMroow8f3EHiCKUOh6UwxMP5S3EuXMnWMNSVIhatGZRwkpg9OUTYdZPsDUVH3cOAkWhGFUJw== + dependencies: + "@grpc/proto-loader" "^0.7.8" + "@types/node" ">=12.12.47" + "@grpc/proto-loader@^0.7.8": version "0.7.10" resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.10.tgz#6bf26742b1b54d0a473067743da5d3189d06d720" @@ -260,6 +268,27 @@ topcoder-proto-registry "0.1.0" tslib "^2.4.1" +"@topcoder-framework/client-relational@^0.24.1": + version "0.24.1" + resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com/npm/topcoder-framework/@topcoder-framework/client-relational/-/client-relational-0.24.1.tgz#a01c4bb5a6117c38d5b61e1a79ded58abeb5b7c5" + integrity sha512-yZJS2N6l1YT/wadWRgMhMzrtFNfPgCBMVLUPO2ORHq182xXSIBkpXLRvX99z2XVPRGpjCVkfumh+jANQKX8AXw== + dependencies: + "@grpc/grpc-js" "^1.8.0" + "@topcoder-framework/lib-common" "^0.24.1" + topcoder-proto-registry "0.2.0" + tslib "^2.4.1" + +"@topcoder-framework/domain-acl@^0.24.0": + version "0.24.1" + resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com/npm/topcoder-framework/@topcoder-framework/domain-acl/-/domain-acl-0.24.1.tgz#61435da8d28756a319bc31bfd4f5b4458614f64f" + integrity sha512-7+uvCxkfURuXsBIwQBvn9aBobBheZCKlKQWzH4Dk5sCBxyhbjQ0B+TkFnfn2+65NzblwuvYEAEnJdLyAYu4RPA== + dependencies: + "@grpc/grpc-js" "^1.8.7" + "@topcoder-framework/client-relational" "^0.24.1" + "@topcoder-framework/lib-common" "^0.24.1" + topcoder-proto-registry "0.2.0" + tslib "^2.4.1" + "@topcoder-framework/domain-challenge@^0.24.0": version "0.24.0" resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com/npm/topcoder-framework/@topcoder-framework/domain-challenge/-/domain-challenge-0.24.0.tgz#023e57b95cc5213650eebffb860939b61d9b8068" @@ -281,6 +310,16 @@ topcoder-proto-registry "0.1.0" tslib "^2.4.1" +"@topcoder-framework/lib-common@^0.24.1": + version "0.24.1" + resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com/npm/topcoder-framework/@topcoder-framework/lib-common/-/lib-common-0.24.1.tgz#fc69af0f3deb263d347bfb8ac014065c5a7ceeec" + integrity sha512-Av/v5YybzyrJlhxANFxy+uJR938OWzd4vkcBZvAWmY4wX9D8UOiBA1nF2EMZ5+9xhY+PD3O/yuqnfqUs/4qT+g== + dependencies: + "@grpc/grpc-js" "^1.8.0" + rimraf "^3.0.2" + topcoder-proto-registry "0.2.0" + tslib "^2.4.1" + "@types/body-parser@*": version "1.19.4" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.4.tgz#78ad68f1f79eb851aa3634db0c7f57f6f601b462" @@ -4012,6 +4051,11 @@ topcoder-proto-registry@0.1.0: resolved "https://registry.yarnpkg.com/topcoder-proto-registry/-/topcoder-proto-registry-0.1.0.tgz#7bdcb7df7c8bbf9d54beba1c69a6210d0f4ca097" integrity sha512-2RYGdDfCaX02pNcJu7ofb26O0SPe4MA6yfvpzXx6DjiuGtZu5QSZHkeaxqAlzRc9/F5zfWmGJwin4TOppo2xrA== +topcoder-proto-registry@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/topcoder-proto-registry/-/topcoder-proto-registry-0.2.0.tgz#703d636d2581b7b3903fe299f6c3d572c5f728c0" + integrity sha512-qmoAY0jb25A4S4bunUagj+wP++d1Db0iZqMc0SaMFjzW33dXjay7TpJDBbNZuVk4He7kUhYXrn2CDikbPM3TFw== + topo@3.x.x: version "3.0.3" resolved "https://registry.yarnpkg.com/topo/-/topo-3.0.3.tgz#d5a67fb2e69307ebeeb08402ec2a2a6f5f7ad95c"