diff --git a/README.md b/README.md index b6c3931..86f4379 100644 --- a/README.md +++ b/README.md @@ -35,10 +35,14 @@ The following parameters can be set in config files or in env variables: - ES: config object for Elasticsearch - ES.HOST: Elasticsearch host - ES.API_VERSION: Elasticsearch API version -- ES.ES_INDEX: Elasticsearch index name for member -- ES.ES_TYPE: Elasticsearch index type for member +- ES.MEMBER_PROFILE_ES_INDEX: Elasticsearch index name for member profile +- ES.MEMBER_PROFILE_ES_TYPE: Elasticsearch index type for member profile - ES.MEMBER_TRAIT_ES_INDEX: Elasticsearch index name for member trait - ES.MEMBER_TRAIT_ES_TYPE: Elasticsearch index type for member trait +- ES.MEMBER_STATS_ES_INDEX: Elasticsearch index name for member stats +- ES.MEMBER_STATS_ES_TYPE: Elasticsearch index type for member stats +- ES.MEMBER_SKILLS_ES_INDEX: Elasticsearch index name for member skills +- ES.MEMBER_SKILLS_ES_TYPE: Elasticsearch index type for member skills - FILE_UPLOAD_SIZE_LIMIT: the file upload size limit in bytes - PHOTO_URL_TEMPLATE: photo URL template, its will be replaced with S3 object key - VERIFY_TOKEN_EXPIRATION: verify token expiration in minutes diff --git a/app-bootstrap.js b/app-bootstrap.js index 784fbd9..8bbc5be 100644 --- a/app-bootstrap.js +++ b/app-bootstrap.js @@ -5,4 +5,5 @@ global.Promise = require('bluebird') const Joi = require('joi') Joi.page = () => Joi.number().integer().min(1).default(1) -Joi.perPage = () => Joi.number().integer().min(1).max(100).default(20) +Joi.perPage = () => Joi.number().integer().min(1).max(100).default(10) +Joi.sort = () => Joi.string().default("asc") \ No newline at end of file diff --git a/config/default.js b/config/default.js index 82a06e7..fcf4a18 100644 --- a/config/default.js +++ b/config/default.js @@ -45,13 +45,15 @@ module.exports = { HOST: process.env.ES_HOST || 'localhost:9200', API_VERSION: process.env.ES_API_VERSION || '6.8', // member index - ES_INDEX: process.env.ES_INDEX || 'members-2020-01', + MEMBER_PROFILE_ES_INDEX: process.env.MEMBER_PROFILE_ES_INDEX || 'members-2020-01', // member type, ES 6.x accepts only 1 Type per index and it's mandatory to define it - ES_TYPE: process.env.ES_TYPE || 'profiles', + MEMBER_PROFILE_ES_TYPE: process.env.MEMBER_PROFILE_ES_TYPE || 'profiles', MEMBER_TRAIT_ES_INDEX: process.env.MEMBER_TRAIT_ES_INDEX || 'members-2020-01', MEMBER_TRAIT_ES_TYPE: process.env.MEMBER_TRAIT_ES_TYPE || 'profiletraits', MEMBER_STATS_ES_INDEX: process.env.MEMBER_STATS_ES_INDEX || 'memberstats-2020-01', - MEMBER_STATS_ES_TYPE: process.env.MEMBER_STATS_ES_TYPE || 'stats' + MEMBER_STATS_ES_TYPE: process.env.MEMBER_STATS_ES_TYPE || 'stats', + MEMBER_SKILLS_ES_INDEX: process.env.MEMBER_SKILLS_ES_INDEX || 'memberskills-2020-01', + MEMBER_SKILLS_ES_TYPE: process.env.MEMBER_SKILLS_ES_TYPE || 'skills' }, // health check timeout in milliseconds diff --git a/src/common/helper.js b/src/common/helper.js index b1289ab..e7690a8 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -13,6 +13,24 @@ const uuid = require('uuid/v4') const querystring = require('querystring') const request = require('request'); +// Color schema for Ratings +const RATING_COLORS = [{ + color: '#9D9FA0' /* Grey */, + limit: 900, +}, { + color: '#69C329' /* Green */, + limit: 1200, +}, { + color: '#616BD5' /* Blue */, + limit: 1500, +}, { + color: '#FCD617' /* Yellow */, + limit: 2200, +}, { + color: '#EF3A3A' /* Red */, + limit: Infinity, +}]; + // Bus API Client let busApiClient @@ -478,6 +496,8 @@ function cleanUpStatistics (stats, fields) { if (typeof stats[count].maxRating == "string") { stats[count].maxRating = JSON.parse(stats[count].maxRating) } + // set the rating color + stats[count].maxRating.ratingColor = this.getRatingColor(stats[count].maxRating.rating) } if (stats[count].hasOwnProperty("DATA_SCIENCE")) { if (typeof stats[count].DATA_SCIENCE == "string") { @@ -602,6 +622,12 @@ function findTagById (data, id) { return _.find(data, { 'id': id }); } +function getRatingColor(rating) { + let i = 0; const r = Number(rating); + while (RATING_COLORS[i].limit <= r) i += 1; + return RATING_COLORS[i].color || 'black'; +} + module.exports = { wrapExpress, autoWrapExpress, @@ -625,5 +651,6 @@ module.exports = { cleanupSkills, mergeSkills, getAllTags, - findTagById + findTagById, + getRatingColor } diff --git a/src/common/logger.js b/src/common/logger.js index 1011716..18289c4 100644 --- a/src/common/logger.js +++ b/src/common/logger.js @@ -85,16 +85,16 @@ logger.decorateWithLogging = (service) => { const params = method.params || getParams(method) service[name] = async function () { logger.debug(`ENTER ${name}`) - logger.debug('input arguments') - const args = Array.prototype.slice.call(arguments) - logger.debug(util.inspect(_sanitizeObject(_combineObject(params, args)))) + // logger.debug('input arguments') + // const args = Array.prototype.slice.call(arguments) + // logger.debug(util.inspect(_sanitizeObject(_combineObject(params, args)))) try { const result = await method.apply(this, arguments) logger.debug(`EXIT ${name}`) - logger.debug('output arguments') - if (result !== null && result !== undefined) { - logger.debug(util.inspect(_sanitizeObject(result))) - } + // logger.debug('output arguments') + // if (result !== null && result !== undefined) { + // logger.debug(util.inspect(_sanitizeObject(result))) + // } return result } catch (e) { logger.logFullError(e, name) diff --git a/src/init-es.js b/src/init-es.js index d07f281..ab15f35 100644 --- a/src/init-es.js +++ b/src/init-es.js @@ -14,9 +14,9 @@ const client = helper.getESClient() const initES = async () => { if (process.argv.length === 3 && process.argv[2] === 'force') { - logger.info(`Delete index ${config.ES.ES_INDEX} if any.`) + logger.info(`Delete index ${config.ES.MEMBER_PROFILE_ES_INDEX} if any.`) try { - await client.indices.delete({ index: config.ES.ES_INDEX }) + await client.indices.delete({ index: config.ES.MEMBER_PROFILE_ES_INDEX }) } catch (err) { // ignore } @@ -28,14 +28,14 @@ const initES = async () => { } } - let exists = await client.indices.exists({ index: config.ES.ES_INDEX }) + let exists = await client.indices.exists({ index: config.ES.MEMBER_PROFILE_ES_INDEX }) if (exists) { - logger.info(`The index ${config.ES.ES_INDEX} exists.`) + logger.info(`The index ${config.ES.MEMBER_PROFILE_ES_INDEX} exists.`) } else { - logger.info(`The index ${config.ES.ES_INDEX} will be created.`) + logger.info(`The index ${config.ES.MEMBER_PROFILE_ES_INDEX} will be created.`) const body = { mappings: {} } - body.mappings[config.get('ES.ES_TYPE')] = { + body.mappings[config.get('ES.MEMBER_PROFILE_ES_TYPE')] = { properties: { handleLower: { type: 'keyword' }, handle: { type: 'keyword' }, @@ -45,7 +45,7 @@ const initES = async () => { } await client.indices.create({ - index: config.ES.ES_INDEX, + index: config.ES.MEMBER_PROFILE_ES_INDEX, body }) } diff --git a/src/routes.js b/src/routes.js index 69bb5d4..af17898 100644 --- a/src/routes.js +++ b/src/routes.js @@ -14,6 +14,15 @@ module.exports = { method: 'checkHealth' } }, + '/members/search/members': { + get: { + controller: 'SearchController', + method: 'searchMembers', + auth: 'jwt', + allowNoToken: true, + scopes: [MEMBERS.READ, MEMBERS.ALL] + } + }, '/members/:handle': { get: { controller: 'MemberController', @@ -117,14 +126,6 @@ module.exports = { controller: 'MiscController', method: 'getMemberFinancial', auth: 'jwt', - scopes: [MEMBERS.READ, MEMBERS.ALL] - } - }, - '/members': { - get: { - controller: 'SearchController', - method: 'searchMembers', - auth: 'jwt', allowNoToken: true, scopes: [MEMBERS.READ, MEMBERS.ALL] } diff --git a/src/scripts/seed-data.js b/src/scripts/seed-data.js index 470c753..ff2ce5c 100644 --- a/src/scripts/seed-data.js +++ b/src/scripts/seed-data.js @@ -390,8 +390,8 @@ async function seedData () { await helper.create('Member', member) // create member in ES await esClient.create({ - index: config.ES.ES_INDEX, - type: config.ES.ES_TYPE, + index: config.ES.MEMBER_PROFILE_ES_INDEX, + type: config.ES.MEMBER_PROFILE_ES_TYPE, id: member.handleLower, body: member, refresh: 'true' // refresh ES so that it is visible for read operations instantly diff --git a/src/scripts/view-es-data.js b/src/scripts/view-es-data.js index 7a15b2e..68f85ca 100644 --- a/src/scripts/view-es-data.js +++ b/src/scripts/view-es-data.js @@ -18,7 +18,7 @@ const esClient = helper.getESClient() async function showESData () { const result = await esClient.search({ index: indexName, - type: config.get('ES.ES_TYPE') // type name is same for all indices + type: config.get('ES.MEMBER_PROFILE_ES_TYPE') // type name is same for all indices }) return result.hits.hits || [] } diff --git a/src/services/MemberService.js b/src/services/MemberService.js index 1f45723..f3ec7e8 100644 --- a/src/services/MemberService.js +++ b/src/services/MemberService.js @@ -8,6 +8,7 @@ const uuid = require('uuid/v4') const config = require('config') const helper = require('../common/helper') const logger = require('../common/logger') +const statisticsService = require('./StatisticsService') const errors = require('../common/errors') const constants = require('../../app-constants') const HttpStatus = require('http-status-codes') @@ -15,8 +16,8 @@ const HttpStatus = require('http-status-codes') const esClient = helper.getESClient() const MEMBER_FIELDS = ['userId', 'handle', 'handleLower', 'firstName', 'lastName', 'tracks', 'status', - 'addresses', 'description', 'email', 'homeCountryCode', 'competitionCountryCode', 'photoURL', 'createdAt', - 'createdBy','updatedAt','updatedBy'] + 'addresses', 'description', 'email', 'homeCountryCode', 'competitionCountryCode', 'photoURL', 'maxRating', + 'createdAt', 'createdBy','updatedAt','updatedBy'] const INTERNAL_MEMBER_FIELDS = ['newEmail', 'emailVerifyToken', 'emailVerifyTokenDate', 'newEmailVerifyToken', 'newEmailVerifyTokenDate', 'handleSuggest'] @@ -58,11 +59,11 @@ function omitMemberAttributes (currentUser, mb) { */ async function getMember (currentUser, handle, query) { // validate and parse query parameter - const selectFields = helper.parseCommaSeparatedString(query.fields, MEMBER_FIELDS) + const selectFields = helper.parseCommaSeparatedString(query.fields, MEMBER_FIELDS) || MEMBER_FIELDS // query member from Elasticsearch const esQuery = { - index: config.ES.ES_INDEX, - type: config.ES.ES_TYPE, + index: config.ES.MEMBER_PROFILE_ES_INDEX, + type: config.ES.MEMBER_PROFILE_ES_TYPE, size: constants.ES_SEARCH_MAX_SIZE, // use a large size to query all records body: { query: { @@ -80,6 +81,21 @@ async function getMember (currentUser, handle, query) { } else { members = _.map(members.hits.hits, '_source') } + // get the 'maxRating' from stats + if (_.includes(selectFields, 'maxRating')) { + for (let i = 0; i < members.length; i += 1) { + const memberStatsFields = { "fields": "userId,groupId,handleLower,maxRating" } + const memberStats = await statisticsService.getMemberStats(currentUser, members[i].handleLower, + memberStatsFields, false) + if(memberStats[0]) { + if (memberStats[0].hasOwnProperty("maxRating")) { + members[i].maxRating = memberStats[0].maxRating + } else { + members[i].maxRating = {} + } + } + } + } // clean member fields according to current user members = cleanMember(currentUser, members) // select fields diff --git a/src/services/MemberTraitService.js b/src/services/MemberTraitService.js index adedf7f..8735c91 100644 --- a/src/services/MemberTraitService.js +++ b/src/services/MemberTraitService.js @@ -27,7 +27,7 @@ async function getTraits (currentUser, handle, query) { // get member const member = await helper.getMemberByHandle(handle) // parse query parameters - const traitIds = helper.parseCommaSeparatedString(query.traitIds, TRAIT_IDS) + const traitIds = helper.parseCommaSeparatedString(query.traitIds, TRAIT_IDS) || TRAIT_IDS const fields = helper.parseCommaSeparatedString(query.fields, TRAIT_FIELDS) || TRAIT_FIELDS // query member traits from Elasticsearch @@ -203,8 +203,7 @@ updateTraits.schema = createTraits.schema */ async function removeTraits (currentUser, handle, query) { // parse trait ids - const traitIds = helper.parseCommaSeparatedString(query.traitIds, TRAIT_IDS) - + const traitIds = helper.parseCommaSeparatedString(query.traitIds, TRAIT_IDS) || TRAIT_IDS const member = await helper.getMemberByHandle(handle) // check authorization if (!helper.canManageMember(currentUser, member)) { diff --git a/src/services/MiscService.js b/src/services/MiscService.js index 42326cb..6d197d2 100644 --- a/src/services/MiscService.js +++ b/src/services/MiscService.js @@ -19,20 +19,21 @@ const MEMBER_FINANCIAL_FIELDS = ['userId', 'amount', 'status', 'createdAt', 'upd */ async function getMemberFinancial (currentUser, handle, query) { // validate and parse query parameter - const fields = helper.parseCommaSeparatedString(query.fields, MEMBER_FINANCIAL_FIELDS) + const fields = helper.parseCommaSeparatedString(query.fields, MEMBER_FINANCIAL_FIELDS) || MEMBER_FINANCIAL_FIELDS // get member by handle const member = await helper.getMemberByHandle(handle) - // only admin, M2M or user himself can get financial data - if (!helper.canManageMember(currentUser, member)) { - throw new errors.ForbiddenError('You are not allowed to get financial data of the user.') - } - // get financial data by member user id - let data = await helper.getEntityByHashKey(handle, 'MemberFinancial', 'userId', member.userId, true) - // select fields if provided - if (fields) { - data = _.pick(data, fields) - } - return data + // // only admin, M2M or user himself can get financial data + // if (!helper.canManageMember(currentUser, member)) { + // throw new errors.ForbiddenError('You are not allowed to get financial data of the user.') + // } + // // get financial data by member user id + // let data = await helper.getEntityByHashKey(handle, 'MemberFinancial', 'userId', member.userId, true) + // // select fields if provided + // if (fields) { + // data = _.pick(data, fields) + // } + // return data + return { 'message': 'No Data' } } getMemberFinancial.schema = { diff --git a/src/services/SearchService.js b/src/services/SearchService.js index 6000fef..e3aa20c 100644 --- a/src/services/SearchService.js +++ b/src/services/SearchService.js @@ -8,14 +8,15 @@ const config = require('config') const helper = require('../common/helper') const logger = require('../common/logger') const statisticsService = require('./StatisticsService') +const memberService = require('./MemberService') const MEMBER_FIELDS = ['userId', 'handle', 'handleLower', 'firstName', 'lastName', 'status', 'addresses', 'photoURL', 'homeCountryCode', 'competitionCountryCode', 'description', 'email', 'tracks', 'maxRating', 'wins', 'createdAt', 'createdBy', 'updatedAt', 'updatedBy', 'skills', 'stats'] -// exclude 'skills' and 'stats' -const DEFAULT_MEMBER_FIELDS = MEMBER_FIELDS.slice(0, MEMBER_FIELDS.length - 2) +var MEMBER_STATS_FIELDS = ['userId', 'handle', 'handleLower', 'maxRating', + 'challenges', 'wins','DEVELOP', 'DESIGN', 'DATA_SCIENCE', 'copilot'] const esClient = helper.getESClient() @@ -27,27 +28,28 @@ const esClient = helper.getESClient() */ async function searchMembers (currentUser, query) { // validate and parse fields param - let fields = helper.parseCommaSeparatedString(query.fields, MEMBER_FIELDS) || DEFAULT_MEMBER_FIELDS + let fields = helper.parseCommaSeparatedString(query.fields, MEMBER_FIELDS) || MEMBER_FIELDS // if current user is not admin and not M2M, then exclude the admin/M2M only fields if (!currentUser || (!currentUser.isMachine && !helper.hasAdminRole(currentUser))) { fields = _.without(fields, ...config.SEARCH_SECURE_FIELDS) + MEMBER_STATS_FIELDS = _.without(MEMBER_STATS_FIELDS, ...config.STATISTICS_SECURE_FIELDS) } - // construct ES query - const esQuery = { - index: config.get('ES.ES_INDEX'), - type: config.get('ES.ES_TYPE'), + + //// construct ES query for members profile + let esQueryMembers = { + index: config.get('ES.MEMBER_PROFILE_ES_INDEX'), + type: config.get('ES.MEMBER_PROFILE_ES_TYPE'), size: query.perPage, - from: (query.page - 1) * query.perPage, // Es Index starts from 0 + from: (query.page - 1) * query.perPage, body: { - sort: [{ handle: { order: 'asc' } }] + sort: [{ handle: { order: query.sort } }] } } - const boolQuery = [] + const boolQueryMembers = [] if (query.query) { - // Allowing - 'homeCountryCode', 'handle', 'tracks', 'handleLower', 'firstName', 'lastName' - var notAllowedQueryFields = ['createdAt', 'createdBy', 'maxRating', 'photoURL', 'skills', 'stats', 'updatedAt', 'updatedBy', 'userId', 'wins', 'status', 'description', 'email'] - var allowedQueryFields = _.without(fields, ...notAllowedQueryFields) - boolQuery.push({ + // list of field allowed to be queried for + const allowedQueryFields = ['handle', 'handleLower', 'firstName', 'lastName', 'homeCountryCode', 'competitionCountryCode', 'tracks'] + boolQueryMembers.push({ simple_query_string: { query: query.query, fields: allowedQueryFields, @@ -56,61 +58,109 @@ async function searchMembers (currentUser, query) { }) } if (query.handleLower) { - boolQuery.push({ match_phrase: { handleLower: query.handleLower } }) + boolQueryMembers.push({ match_phrase: { handleLower: query.handleLower } }) } if (query.handle) { - boolQuery.push({ match_phrase: { handle: query.handle } }) + boolQueryMembers.push({ match_phrase: { handle: query.handle } }) } if (query.userId) { - boolQuery.push({ match_phrase: { userId: query.userId } }) - } - if (query.status) { - boolQuery.push({ match_phrase: { status: query.status } }) + boolQueryMembers.push({ match_phrase: { userId: query.userId } }) } - if (boolQuery.length > 0) { - esQuery.body.query = { + boolQueryMembers.push({ match_phrase: { status: "ACTIVE" } }) + if (boolQueryMembers.length > 0) { + esQueryMembers.body.query = { bool: { - filter: boolQuery + filter: boolQueryMembers } } } - // Search with constructed query - const docs = await esClient.search(esQuery) - // Extract data from hits - let total = docs.hits.total + // search with constructed query + const docsMembers = await esClient.search(esQueryMembers) + // extract data from hits + const total = docsMembers.hits.total if (_.isObject(total)) { total = total.value || 0 } - const result = _.map(docs.hits.hits, (item) => item._source) - for (let i = 0; i < result.length; i += 1) { - if (_.includes(fields, 'skills')) { - // get skills - const memberSkill = await statisticsService.getMemberSkills(currentUser, result[i].handleLower, {}, false) - result[i].skills = memberSkill.skills + const members = _.map(docsMembers.hits.hits, (item) => item._source) + + //// construct ES query for skills + const esQuerySkills = { + index: config.get('ES.MEMBER_SKILLS_ES_INDEX'), + type: config.get('ES.MEMBER_SKILLS_ES_TYPE'), + body: { + sort: [{ userHandle: { order: query.sort } }] + } + } + const boolQuerySkills = [] + // search for a list of members + const membersHandles = _.map(members, 'handleLower') + boolQuerySkills.push({ query: { terms : { handleLower: membersHandles } } } ) + esQuerySkills.body.query = { + bool: { + filter: boolQuerySkills + } + } + // search with constructed query + const docsSkiills = await esClient.search(esQuerySkills) + // extract data from hits + const mbrsSkills = _.map(docsSkiills.hits.hits, (item) => item._source) + + //// construct ES query for stats + const esQueryStats = { + index: config.get('ES.MEMBER_STATS_ES_INDEX'), + type: config.get('ES.MEMBER_STATS_ES_TYPE'), + body: { + sort: [{ handleLower: { order: query.sort } }] + } + } + const boolQueryStats = [] + boolQueryStats.push({ query: { terms : { handleLower: membersHandles } } }) + boolQueryStats.push({ match_phrase: { groupId : 10 } }) + esQueryStats.body.query = { + bool: { + filter: boolQueryStats } - if (_.includes(fields, 'stats')) { - // get statistics - const memberStats = await statisticsService.getMemberStats(result[i].handleLower, {}, false) - if (memberStats) { - // get stats - result[i].stats = memberStats - // update the maxRating - if (_.includes(fields, 'maxRating')) { - for (count = 0; count < result[i].stats.length; count++) { - if (result[i].stats[count].hasOwnProperty("maxRating")) { - result[i].maxRating = result[i].stats[count].maxRating - } - if (result[i].stats[count].hasOwnProperty("wins")) { - result[i].wins = result[i].stats[count].wins - } + } + // search with constructed query + const docsStats = await esClient.search(esQueryStats) + // extract data from hits + const mbrsSkillsStats = _.map(docsStats.hits.hits, (item) => item._source) + + //// merge members profile and there skills + const mergedMbrSkills = _.merge(_.keyBy(members, 'userId'), _.keyBy(mbrsSkills, 'userId')) + let resultMbrSkills = _.values(mergedMbrSkills) + resultMbrSkills = _.map(resultMbrSkills, function(item) { + if (!item.skills) { + item.skills = {} + } + return item; + }) + + //// merge overall members and stats + const mbrsSkillsStatsKeys = _.keyBy(mbrsSkillsStats, 'userId') + const resultMbrsSkillsStats = _.map(resultMbrSkills, function(item) { + if (mbrsSkillsStatsKeys[item.userId]) { + item.stats = [] + if (mbrsSkillsStatsKeys[item.userId].maxRating) { + // add the maxrating + item.maxRating = mbrsSkillsStatsKeys[item.userId].maxRating + // set the rating color + if (item.maxRating.hasOwnProperty("rating")) { + item.maxRating.ratingColor = helper.getRatingColor(item.maxRating.rating) } } + // clean up stats fileds and filter on stats fields + item.stats.push(_.pick(mbrsSkillsStatsKeys[item.userId], MEMBER_STATS_FIELDS)) + } else { + item.stats = [] } - } - // select fields - result[i] = _.pick(result[i], fields) - } - return { total, page: query.page, perPage: query.perPage, result } + return item; + }) + + // filter member and skills fields + let results = _.map(resultMbrsSkillsStats, (item) => _.pick(item, fields)) + + return { total, page: query.page, perPage: query.perPage, result: results } } searchMembers.schema = { @@ -120,10 +170,10 @@ searchMembers.schema = { handleLower: Joi.string(), handle: Joi.string(), userId: Joi.number(), - status: Joi.string(), fields: Joi.string(), page: Joi.page(), - perPage: Joi.perPage() + perPage: Joi.perPage(), + sort: Joi.sort(), }) } diff --git a/src/services/StatisticsService.js b/src/services/StatisticsService.js index b69c5fc..7b73f71 100644 --- a/src/services/StatisticsService.js +++ b/src/services/StatisticsService.js @@ -10,7 +10,8 @@ const logger = require('../common/logger') const errors = require('../common/errors') const esClient = helper.getESClient() -const DISTRIBUTION_FIELDS = ['track', 'subTrack', 'distribution', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'] +const DISTRIBUTION_FIELDS = ['track', 'subTrack', 'distribution', 'createdAt', 'updatedAt', + 'createdBy', 'updatedBy'] const HISTORY_STATS_FIELDS = ['userId', 'groupId', 'handle', 'handleLower', 'DEVELOP', 'DATA_SCIENCE', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'] @@ -31,7 +32,7 @@ var allTags */ async function getDistribution (query) { // validate and parse query parameter - const fields = helper.parseCommaSeparatedString(query.fields, DISTRIBUTION_FIELDS) + const fields = helper.parseCommaSeparatedString(query.fields, DISTRIBUTION_FIELDS) || DISTRIBUTION_FIELDS // find matched distribution records let criteria @@ -105,8 +106,10 @@ async function getHistoryStats (currentUser, handle, query) { if (!groupIds) { // get statistics by member user id from dynamodb let statsDb = await helper.getEntityByHashKey(handle, 'MemberHistoryStats', 'userId', member.userId, true) - statsDb.originalItem().groupId = 10 - overallStat.push(statsDb.originalItem()) + if(!_.isEmpty(statsDb)) { + statsDb.originalItem().groupId = 10 + overallStat.push(statsDb.originalItem()) + } } if (groupIds) { for (const groupId of groupIds.split(',')) { @@ -114,7 +117,9 @@ async function getHistoryStats (currentUser, handle, query) { if(groupId == "10") { // get statistics by member user id from dynamodb statsDb = await helper.getEntityByHashKey(handle, 'MemberHistoryStats', 'userId', member.userId, false) - statsDb.originalItem().groupId = 10 + if(!_.isEmpty(statsDb)) { + statsDb.originalItem().groupId = 10 + } } else { // get statistics private by member user id from dynamodb statsDb = await helper.getEntityByHashRangeKey(handle, 'MemberHistoryStatsPrivate', 'userId', member.userId, 'groupId', groupId, false) @@ -148,68 +153,77 @@ getHistoryStats.schema = { * @returns {Object} the member statistics */ async function getMemberStats (currentUser, handle, query, throwError) { - let overallStat = [] + let stats = [] // validate and parse query parameter - const fields = helper.parseCommaSeparatedString(query.fields, MEMBER_STATS_FIELDS) + const fields = helper.parseCommaSeparatedString(query.fields, MEMBER_STATS_FIELDS) || MEMBER_STATS_FIELDS // get member by handle const member = await helper.getMemberByHandle(handle) let groupIds = query.groupIds if (!groupIds) { - let stats + let stat try { // get statistics by member user id from Elasticsearch - stats = await esClient.get({ + stat = await esClient.get({ index: config.ES.MEMBER_STATS_ES_INDEX, type: config.ES.MEMBER_STATS_ES_TYPE, id: member.userId + "_10" }); - if (stats.hasOwnProperty("_source")) { - stats = stats._source + if (stat.hasOwnProperty("_source")) { + stat = stat._source } } catch (error) { if (error.displayName == "NotFound") { // get statistics by member user id from dynamodb - stats = await helper.getEntityByHashKey(handle, 'MemberStats', 'userId', member.userId, throwError) - if (!_.isEmpty(stats, true)) { - stats.groupId = 10 + stat = await helper.getEntityByHashKey(handle, 'MemberStats', 'userId', member.userId, throwError) + if (!_.isEmpty(stat, true)) { + stat.originalItem().groupId = 10 + stat = stat.originalItem } } } - overallStat.push(stats) + if (!_.isEmpty(stat, true)) { + stats.push(stat) + } } if (groupIds) { for (const groupId of groupIds.split(',')) { - let stats + let stat try { // get statistics private by member user id from Elasticsearch - stats = await esClient.get({ + stat = await esClient.get({ index: config.ES.MEMBER_STATS_ES_INDEX, type: config.ES.MEMBER_STATS_ES_TYPE, id: member.userId + "_" + groupId }); - if (stats.hasOwnProperty("_source")) { - stats = stats._source + if (stat.hasOwnProperty("_source")) { + stat = stat._source } } catch (error) { if (error.displayName == "NotFound") { if(groupId == "10") { // get statistics by member user id from dynamodb - stats = await helper.getEntityByHashKey(handle, 'MemberStats', 'userId', member.userId, false) - if (!_.isEmpty(stats, true)) { - stats.groupId = 10 + stat = await helper.getEntityByHashKey(handle, 'MemberStats', 'userId', member.userId, false) + if (!_.isEmpty(stat, true)) { + stat.originalItem().groupId = 10 + stat = stat.originalItem } } else { // get statistics private by member user id from dynamodb - stats = await helper.getEntityByHashRangeKey(handle, 'MemberStatsPrivate', 'userId', member.userId, 'groupId', groupId, false) + stat = await helper.getEntityByHashRangeKey(handle, 'MemberStatsPrivate', 'userId', member.userId, 'groupId', groupId, false) } } } - if(stats) { - overallStat.push(stats) + if (!_.isEmpty(stat, true)) { + stats.push(stat) } } } - return helper.cleanUpStatistics(overallStat, fields) + var result = helper.cleanUpStatistics(stats, fields) + // remove identifiable info fields if user is not admin, not M2M and not member himself + if (!helper.canManageMember(currentUser, member)) { + result = _.map(result, (item) => _.omit(item, config.STATISTICS_SECURE_FIELDS)) + } + return result } getMemberStats.schema = { @@ -230,7 +244,7 @@ getMemberStats.schema = { */ async function getMemberSkills (currentUser, handle, query, throwError) { // validate and parse query parameter - const fields = helper.parseCommaSeparatedString(query.fields, MEMBER_SKILL_FIELDS) + const fields = helper.parseCommaSeparatedString(query.fields, MEMBER_SKILL_FIELDS) || MEMBER_SKILL_FIELDS // get member by handle const member = await helper.getMemberByHandle(handle) // fetch tags data diff --git a/test/testHelper.js b/test/testHelper.js index dee217f..bd60a14 100644 --- a/test/testHelper.js +++ b/test/testHelper.js @@ -404,15 +404,15 @@ async function createData () { // create data in ES await esClient.create({ - index: config.ES.ES_INDEX, - type: config.ES.ES_TYPE, + index: config.ES.MEMBER_PROFILE_ES_INDEX, + type: config.ES.MEMBER_PROFILE_ES_TYPE, id: member1.handleLower, body: member1, refresh: 'true' // refresh ES so that it is visible for read operations instantly }) await esClient.create({ - index: config.ES.ES_INDEX, - type: config.ES.ES_TYPE, + index: config.ES.MEMBER_PROFILE_ES_INDEX, + type: config.ES.MEMBER_PROFILE_ES_TYPE, id: member2.handleLower, body: member2, refresh: 'true' // refresh ES so that it is visible for read operations instantly @@ -453,14 +453,14 @@ async function clearData () { // remove data in ES await esClient.delete({ - index: config.ES.ES_INDEX, - type: config.ES.ES_TYPE, + index: config.ES.MEMBER_PROFILE_ES_INDEX, + type: config.ES.MEMBER_PROFILE_ES_TYPE, id: member1.handleLower, refresh: 'true' // refresh ES so that it is effective for read operations instantly }) await esClient.delete({ - index: config.ES.ES_INDEX, - type: config.ES.ES_TYPE, + index: config.ES.MEMBER_PROFILE_ES_INDEX, + type: config.ES.MEMBER_PROFILE_ES_TYPE, id: member2.handleLower, refresh: 'true' // refresh ES so that it is effective for read operations instantly })