diff --git a/.env.sample b/.env.sample index d2b21c9..a468605 100644 --- a/.env.sample +++ b/.env.sample @@ -10,6 +10,10 @@ OBSERVATION_DATASOURCE_NAME = "OBSERVATION_DATASOURCE_NAME" // Obs OBSERVATION_EVIDENCE_DATASOURCE_NAME = "EVIDENCE_DATASOURCE_NAME" // Observation evidences data source name SURVEY_DATASOURCE_NAME = "SURVEY_DATASOURCE_NAME" // Survey data source name SURVEY_EVIDENCE_DATASOURCE_NAME = "SURVEY_EVIDENCE_DATASOURCE_NAME" // Survey evidence data source name +PROJECT_RESOURCE_DATASOURCE_NAME = "PROJECT_RESOURCE_DATASOURCE_NAME" // Project resource data source name +OBSERVATION_RESOURCE_DATASOURCE_NAME = "OBSERVATION_RESOURCE_DATASOURCE_NAME" // Observation resource data source name +SURVEY_RESOURCE_DATASOURCE_NAME = "SURVEY_RESOURCE_DATASOURCE_NAME" // Survey resource data source name +PROGRAM_RESOURCE_DATASOURCE_NAME = "PROGRAM_RESOURCE_DATASOURCE_NAME" // Program resource data source name CONTENT_REPORT_THRESHOLD = 5 // Restrict number of records to be shown for container reports ENTITY_SCORE_REPORT_THRESHOLD = 5 // Restrict number of submission for entity score report OBSERVATION_SCORE_REPORT_THRESHOLD = 2 // Restrict number of submission per entity in observation report diff --git a/.gitignore b/.gitignore index ea5939c..c29d7ea 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,5 @@ typings/ config/config.json tmp/* package-lock.json +keycloak-public-keys/ +.vscode/ diff --git a/common/constants.js b/common/constants.js new file mode 100644 index 0000000..6ae2a49 --- /dev/null +++ b/common/constants.js @@ -0,0 +1,34 @@ +/** + * name : constants.js + * author : Ankit Shahu + * created-date : 10-April-2023 + * Description : This is to keep all the hard code value in form of variable. + */ + + +/** + * Description : projection passed by frontend + */ +exports.ResourceTypeProjection = { + DISTRICT:"district", + BLOCK:"block", + ORGANISATION:"organisation" + +} + +/** + * Description : resource type passed by frontend + */ +exports.ResourceType = { + PROGRAM:"program", + SOLUTION:"solution" +} + +/** + * Description : solution type passed by frontend + */ +exports.SolutionType = { + PROJECT : "improvementProject", + OBSERVATION: "observation", + SURVEY :"survey" +} \ No newline at end of file diff --git a/common/druid_queries.json b/common/druid_queries.json index 42be5b3..078698c 100644 --- a/common/druid_queries.json +++ b/common/druid_queries.json @@ -73,5 +73,17 @@ "observations_by_entity" : {"queryType":"groupBy","dataSource":"ml_observation_dev","granularity":"all","dimensions":["entityId","entityName","observationName","observationSubmissionId","createdAt"],"filter":{"type":"or","fields":[]},"aggregations":[],"postAggregations":[],"intervals":["1901-01-01T00:00:00+00:00/2101-01-01T00:00:00+00:00"]}, - "question_response_query" : {"queryType":"scan","dataSource":"sl-observation","resultFormat": "list","columns":[ "solutionName", "criteriaName", "domainName", "questionName", "programName", "questionResponseLabel", "observationSubmissionId", "questionResponseType", "questionId", "questionSequenceByEcm","solution_type"],"filter": {"type":"and","fields": [ { "type": "or", "fields": [ { "type": "selector", "dimension": "questionResponseType", "value": "radio" }, { "type": "selector", "dimension": "questionResponseType", "value": "multiselect" }, { "type": "selector", "dimension": "questionResponseType", "value": "slider" } ] } ] },"intervals":"1901-01-01T00:00:00+00:00/2101-01-01T00:00:00+00:00"} + "question_response_query" : {"queryType":"scan","dataSource":"sl-observation","resultFormat": "list","columns":[ "solutionName", "criteriaName", "domainName", "questionName", "programName", "questionResponseLabel", "observationSubmissionId", "questionResponseType", "questionId", "questionSequenceByEcm","solution_type"],"filter": {"type":"and","fields": [ { "type": "or", "fields": [ { "type": "selector", "dimension": "questionResponseType", "value": "radio" }, { "type": "selector", "dimension": "questionResponseType", "value": "multiselect" }, { "type": "selector", "dimension": "questionResponseType", "value": "slider" } ] } ] },"intervals":"1901-01-01T00:00:00+00:00/2101-01-01T00:00:00+00:00"}, + + "solution_distric_level_query": {"queryType": "groupBy","dataSource": "","dimensions": ["district_name", "district_externalId"],"granularity": "all","intervals": "1901-01-01T00:00:00+00:00/2101-01-01T00:00:00+00:00","filter": {"type": "and","fields": []}}, + + "solution_organisations_level_query": {"queryType": "groupBy","dataSource": "","dimensions": ["organisation_id", "organisation_name"],"granularity": "all","intervals": "1901-01-01T00:00:00+00:00/2101-01-01T00:00:00+00:00","filter": {"type": "and","fields": []}}, + + "solution_block_level_query": {"queryType": "groupBy","dataSource": "","dimensions": ["block_externalId", "block_name"],"granularity": "all","intervals": "1901-01-01T00:00:00+00:00/2101-01-01T00:00:00+00:00","filter": {"type": "and","fields": []}}, + + "program_distric_level_query": {"queryType": "groupBy","dataSource": "","dimensions": ["district_name", "district_id"],"granularity": "all","intervals": "1901-01-01T00:00:00+00:00/2101-01-01T00:00:00+00:00","filter": {"type": "and","fields": []}}, + + "program_organisations_level_query": {"queryType": "groupBy","dataSource": "","dimensions": ["organisation_id", "organisation_name"],"granularity": "all","intervals": "1901-01-01T00:00:00+00:00/2101-01-01T00:00:00+00:00","filter": {"type": "and","fields": []}}, + + "program_block_level_query": {"queryType": "groupBy","dataSource": "","dimensions": ["block_id", "block_name"],"granularity": "all","intervals": "1901-01-01T00:00:00+00:00/2101-01-01T00:00:00+00:00","filter": {"type": "and","fields": []}} } \ No newline at end of file diff --git a/common/endpoints.js b/common/endpoints.js index 437bdab..76c1d2e 100644 --- a/common/endpoints.js +++ b/common/endpoints.js @@ -16,5 +16,6 @@ module.exports = { AZURE_GET_DOWNLOADABLE_URL : "/v1/cloud-services/azure/getDownloadableUrl", AWS_GET_DOWNLOADABLE_URL : "/v1/cloud-services/aws/getDownloadableUrl", GET_PRESIGNED_URL : "/v1/cloud-services/files/preSignedUrls", - GET_DOWNLOADABLE_URL: "/v1/cloud-services/files/getDownloadableUrl" + GET_DOWNLOADABLE_URL: "/v1/cloud-services/files/getDownloadableUrl", + GET_USER_EXTENSION : "/v1/user-extension/read" } \ No newline at end of file diff --git a/common/utils.js b/common/utils.js index a0c281c..e38bbe5 100644 --- a/common/utils.js +++ b/common/utils.js @@ -1,5 +1,5 @@ const druidQueries = require('./druid_queries.json'); - +const { ResourceType, SolutionType } = require("./constants"); /** * Return druid query for the given query name * @function @@ -80,10 +80,49 @@ function getDruidIntervalDate( data ) { return isoSring; } +/** + * Returns Druid Data source Name + * @function + * @name getDataSourceName + * @returns {String} returns druid data source name. +*/ +function getDataSourceName (query, body){ + let dataSource + if(query.resourceType === ResourceType.PROGRAM){ + dataSource = process.env.PROGRAM_RESOURCE_DATASOURCE_NAME + }else { + switch (body.solutionType) { + case SolutionType.PROJECT : dataSource = process.env.PROJECT_RESOURCE_DATASOURCE_NAME + break; + case SolutionType.OBSERVATION : dataSource = process.env.OBSERVATION_RESOURCE_DATASOURCE_NAME + break; + case SolutionType.SURVEY : dataSource = process.env.SURVEY_RESOURCE_DATASOURCE_NAME + break; + } + } + return dataSource +} + +/** + * Returns Druid Query Filter + * @function + * @name getResourceFilter + * @returns {String} returns druid query filter +*/ +function getResourceFilter (query) { + const resourceFilter = { + type: "selector", + dimension: query.resourceType == ResourceType.SOLUTION ? "solution_id" : "program_id", + value: query.resourceId + } + return resourceFilter +} module.exports = { getDruidQuery: getDruidQuery, getDruidConnection: getDruidConnection, getGotenbergConnection: getGotenbergConnection, - getDruidIntervalDate: getDruidIntervalDate + getDruidIntervalDate: getDruidIntervalDate, + getDataSourceName: getDataSourceName, + getResourceFilter:getResourceFilter } diff --git a/controllers/v1/resource.js b/controllers/v1/resource.js new file mode 100644 index 0000000..e1cbb83 --- /dev/null +++ b/controllers/v1/resource.js @@ -0,0 +1,111 @@ +/** + * name : resource.js + * author : Ankit Shahu + * created-date : 10-April-2023 + * Description : Resource level data for program and solution. + */ + + +const resourceHelper = require("../../helper/resource") +const {ResourceType, ResourceTypeProjection} = require("../../common/constants") + + /** + * @api {post} /mlreports/api/v1/resource/filtervalues?resourceType=program&resourceId=6013eab15faeea0e88a26ef5 + * List of data based on collection + * @apiVersion 1.0.0 + * @apiGroup public + * @apiSampleRequest /mlreports/api/v1/resource/filtervalues + * @param {json} Request-Body: + { + "projection": "block", + "query":{ // optional required for block + "districtLocationId": "b5c35cfc-6c1e-4266-94ef-a425c43c7f4e" + } + "solutionType": "observation"/"survey"/"improvementProject" //Required only for resourceType=solution, + "programId": "6013eab15faeea0e88a26efd" + } + * @apiParamExample {json} Response: + * { + "message": "Program resource fetched successfully ", + "status": 200, + "result": { + "districts": [ + { + "id": "98ae45d7-9257-4c14-a16a-9760a442ff28", + "name": "PRAKASAM" + }, + { + "id": "0c0391ba-610b-4796-8645-338d047b1e28", + "name": "TIRUVALLUR" + } + ], + "organisations": [ + { + "id": "0126796199493140480", + "name": "Staging Custodian Organization" + } + ] + } + * } + * @apiUse successBody + * @apiUse errorBody + */ + + /** + * List of data based on collection + * @method + * @name filtervalues + * @returns {JSON} list of data. + */ +exports.filtervalues = async function (req, res) { + + //userExtension will validate whether user have access to the program as designer or manager. + const userExtension = await resourceHelper.userExtensions(req,res); + if(userExtension){ + if(req.query.resourceType == ResourceType.PROGRAM || req.query.resourceType == ResourceType.SOLUTION){ + if(!req.query.resourceId){ + res.status(400).send({ + message: "Resource id not passed" + }) + } + if(req.body.projection == ResourceTypeProjection.DISTRICT){ + //Gets list of district where program or solution started + const getDistict = await resourceHelper.getDistricts( req ,res) + //Gets list of Organisation where program or solution started + const getOrganisation = await resourceHelper.getOrganisations(req,res) + if(getDistict || getOrganisation){ + const districtOrganisationResponse = { + message: req.query.resourceType == ResourceType.SOLUTION? "Solution details fetched successfully":"Program details fetched successfully", + status: "success", + result:{ + districts: getDistict, + organisations: getOrganisation + } + } + res.status(200).send(districtOrganisationResponse) + } + } else if(req.body.projection == ResourceTypeProjection.BLOCK){ + //Gets list of block where program or solution started + const getBlock = await resourceHelper.getBlocks( req,res ) + if(getBlock){ + const blockResponse = { + message: req.query.resourceType == ResourceType.SOLUTION? "Solution details fetched successfully":"Program details fetched successfully", + status: "success", + result:{ + block: getBlock, + } + } + res.status(200).send(blockResponse) + } + } + }else{ + res.status(400).send({ + message: "No data found" + }) + } + }else { + res.status(401).send({ + message: "You are not authorised to access this resource" + }) + } +} diff --git a/envVariables.js b/envVariables.js index 9afb095..f4abda1 100644 --- a/envVariables.js +++ b/envVariables.js @@ -38,6 +38,22 @@ let enviromentVariables = { "message" : "Required", "optional" : false }, + "PROJECT_RESOURCE_DATASOURCE_NAME" : { + "message" : "Required", + "optional" : false + }, + "OBSERVATION_RESOURCE_DATASOURCE_NAME" : { + "message" : "Required", + "optional" : false + }, + "SURVEY_RESOURCE_DATASOURCE_NAME" : { + "message" : "Required", + "optional" : false + }, + "PROGRAM_RESOURCE_DATASOURCE_NAME" : { + "message" : "Required", + "optional" : false + }, "ENTITY_SCORE_REPORT_THRESHOLD" : { "message" : "Required", "optional" : false diff --git a/helper/kendra_service.js b/helper/kendra_service.js index 67d056a..75ec327 100644 --- a/helper/kendra_service.js +++ b/helper/kendra_service.js @@ -66,8 +66,36 @@ async function getPreSignedUrl(file) { }) } +async function getUserExtension(token){ + return new Promise(async function (resolve, reject) { + + let url = urlPrefix + endpoints.GET_USER_EXTENSION; + let options = { + method: "POST", + json: true, + headers: { + "x-authenticated-user-token": token, + "internal-access-token": process.env.INTERNAL_ACCESS_TOKEN, + "Content-Type": "application/json", + }, + body: {}, + uri: url + } + + rp(options) + .then(result => { + return resolve(result); + }) + .catch(err => { + return resolve(err); + }) + }) + +} + module.exports = { getDownloadableUrl: getDownloadableUrl, - getPreSignedUrl: getPreSignedUrl + getPreSignedUrl: getPreSignedUrl, + getUserExtension:getUserExtension } \ No newline at end of file diff --git a/helper/resource.js b/helper/resource.js new file mode 100644 index 0000000..80adef5 --- /dev/null +++ b/helper/resource.js @@ -0,0 +1,130 @@ +/** + * name : resource.js + * author : Ankit Shahu + * created-date : 10-April-2023 + * Description : Resource level data for program and solution. + */ + + + const { ResourceType } = require("../common/constants"); +const rp = require('request-promise'); +const kendra_service = require("./kendra_service"); +const utils = require("../common/utils"); + +//user-extension validation: this will check if user is program manager or program designer of particular porgram. +exports.userExtensions = async function (req,res){ + return new Promise(async function (resolve, reject) { + try { + let userExtensions = await kendra_service.getUserExtension(req.userDetails.token) + if(userExtensions.status === 200){ + for(let platformCode = 0; platformCode < userExtensions.result.platformRoles.length; platformCode++){ + if((userExtensions.result.platformRoles[platformCode].code === "PROGRAM_DESIGNER" || userExtensions.result.platformRoles[platformCode].code === "PROGRAM_MANAGER") && (userExtensions.result.platformRoles[platformCode].programs.includes(req.body.programId))){ + resolve(true) + break; + } + } + resolve(false); + }else{ + resolve(false); + } + }catch(error){ + res.status(400).send({ + result: false, + message: err.message + }); + } + }) +} + +//this function will call druid and return all the districts to particular program or solution +exports.getDistricts = async function(req,res){ + return new Promise(async function (resolve, reject) { + try { + let bodyParam = req.query.resourceType === ResourceType.SOLUTION ? gen.utils.getDruidQuery("solution_distric_level_query") : gen.utils.getDruidQuery("program_distric_level_query"); + //Gets data source name based on resource type and solution type + bodyParam.dataSource = await utils.getDataSourceName(req.query, req.body) + //Gets Resource filter based on Resource type + const resourceFilter = await utils.getResourceFilter(req.query) + bodyParam.filter.fields.push(resourceFilter) + let options = gen.utils.getDruidConnection(); + options.method = "POST"; + options.body = bodyParam; + let data = await rp(options); + if(data){ + const typeOfId = req.query.resourceType == ResourceType.SOLUTION ? "district_externalId" : "district_id" + const result = data.map(district => ({ id: district.event[typeOfId] , name: district.event.district_name })); + resolve(result) + } + }catch(err){ + res.status(400).send({ + result: false, + message: err.message + }); + } + }) +} + +//this function will call druid and return all the organisations to particular program or solution +exports.getOrganisations = async function(req,res){ + return new Promise(async function (resolve, reject) { + try { + let bodyParam = req.query.resourceType === ResourceType.SOLUTION ? gen.utils.getDruidQuery("solution_organisations_level_query") : gen.utils.getDruidQuery("program_organisations_level_query"); + //Gets data source name based on resource type and solution type + bodyParam.dataSource = await utils.getDataSourceName(req.query, req.body) + //Gets Resource filter based on Resource type + const resourceFilter = await utils.getResourceFilter(req.query) + bodyParam.filter.fields.push(resourceFilter) + let options = gen.utils.getDruidConnection(); + options.method = "POST"; + options.body = bodyParam; + let data = await rp(options); + if(data){ + const result = data.map(organisation => ({ id: organisation.event.organisation_id, name: organisation.event.organisation_name })); + resolve(result) + } + }catch(err){ + res.status(400).send({ + result: false, + message: err.message + }); + } + }) + +} + +//this function will call druid and return all the block to particular program or solution +exports.getBlocks = async function(req,res){ + return new Promise(async function (resolve, reject) { + try { + let bodyParam = req.query.resourceType === ResourceType.SOLUTION ? gen.utils.getDruidQuery("solution_block_level_query") : gen.utils.getDruidQuery("program_block_level_query"); + //Gets data source name based on resource type and solution type + bodyParam.dataSource = await utils.getDataSourceName(req.query, req.body) + //Gets Resource filter based on Resource type + const resourceFilter = await utils.getResourceFilter(req.query) + bodyParam.filter.fields.push(resourceFilter) + + const districtFilter = { + type: "selector", + dimension: req.query.resourceType == ResourceType.SOLUTION ? "district_externalId" : "district_id", + value: req.body.query.districtLocationId + } + bodyParam.filter.fields.push(districtFilter) + let options = gen.utils.getDruidConnection(); + options.method = "POST"; + options.body = bodyParam; + let data = await rp(options); + if(data){ + const typeOfId = req.query.resourceType == ResourceType.SOLUTION ? "block_externalId" : "block_id" + const result = data.map(block => ({ id: block.event[typeOfId] , name: block.event.block_name })); + resolve(result) + } + }catch(err){ + res.status(400).send({ + result: false, + message: err.message + }); + } + + }) + +}