diff --git a/config/default.js b/config/default.js index 16922f4..b6d25aa 100644 --- a/config/default.js +++ b/config/default.js @@ -37,6 +37,8 @@ module.exports = { KAFKA_GROUP_DELETE_TOPIC: process.env.KAFKA_GROUP_DELETE_TOPIC || 'groups.notification.delete', KAFKA_GROUP_MEMBER_ADD_TOPIC: process.env.KAFKA_GROUP_MEMBER_ADD_TOPIC || 'groups.notification.member.add', KAFKA_GROUP_MEMBER_DELETE_TOPIC: process.env.KAFKA_GROUP_MEMBER_DELETE_TOPIC || 'groups.notification.member.delete', + KAFKA_GROUP_UNIVERSAL_MEMBER_ADD_TOPIC: process.env.KAFKA_GROUP_UNIVERSAL_MEMBER_ADD_TOPIC || 'groups.notification.universalmember.add', + KAFKA_GROUP_UNIVERSAL_MEMBER_DELETE_TOPIC: process.env.KAFKA_GROUP_UNIVERSAL_MEMBER_DELETE_TOPIC || 'groups.notification.universalmember.delete', USER_ROLES: { Admin: 'Administrator', diff --git a/src/common/helper.js b/src/common/helper.js index 8718674..4314620 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -74,13 +74,9 @@ async function ensureExists(tx, model, id, isAdmin = false) { if (model === 'Group') { if (validate(id, 4)) { if (!isAdmin) { - res = await tx.run(`MATCH (e:${model} {id: {id}, status: '${constants.GroupStatus.Active}'}) RETURN e`, { - id - }) + res = await tx.run(`MATCH (e:${model} {id: {id}, status: '${constants.GroupStatus.Active}'}) RETURN e`, {id}) } else { - res = await tx.run(`MATCH (e:${model} {id: {id}}) RETURN e`, { - id - }) + res = await tx.run(`MATCH (e:${model} {id: {id}}) RETURN e`, {id}) } } else { if (!isAdmin) { @@ -88,9 +84,7 @@ async function ensureExists(tx, model, id, isAdmin = false) { id }) } else { - res = await tx.run(`MATCH (e:${model} {oldId: {id}}) RETURN e`, { - id - }) + res = await tx.run(`MATCH (e:${model} {oldId: {id}}) RETURN e`, {id}) } } @@ -98,12 +92,18 @@ async function ensureExists(tx, model, id, isAdmin = false) { throw new errors.NotFoundError(`Not found ${model} of id ${id}`) } } else if (model === 'User') { - res = await tx.run(`MATCH (e:${model} {id: {id}}) RETURN e`, { - id - }) + if (validate(id, 4)) { + res = await tx.run(`MATCH (e:${model} {universalUID: {id}}) RETURN e`, {id}) - if (res && res.records.length === 0) { - res = await tx.run(`CREATE (user:User {id: {id}}) RETURN user`, { id }) + if (res && res.records.length === 0) { + res = await tx.run(`CREATE (user:User {id: '00000000', universalUID: {id}}) RETURN user`, {id}) + } + } else { + res = await tx.run(`MATCH (e:${model} {id: {id}}) RETURN e`, {id}) + + if (res && res.records.length === 0) { + res = await tx.run(`CREATE (user:User {id: {id}, universalUID: '00000000'}) RETURN user`, {id}) + } } } @@ -123,7 +123,7 @@ async function ensureGroupMember(session, groupId, userId) { try { const memberCheckRes = await session.run( 'MATCH (g:Group {id: {groupId}})-[r:GroupContains {type: {membershipType}}]->(u:User {id: {userId}}) RETURN r', - { groupId, membershipType: config.MEMBERSHIP_TYPES.User, userId } + {groupId, membershipType: config.MEMBERSHIP_TYPES.User, userId} ) if (memberCheckRes.records.length === 0) { throw new errors.ForbiddenError(`User is not member of the group`) @@ -143,7 +143,7 @@ async function getChildGroups(session, groupId) { try { const res = await session.run( 'MATCH (g:Group {id: {groupId}})-[r:GroupContains]->(c:Group) RETURN c ORDER BY c.oldId', - { groupId } + {groupId} ) return _.map(res.records, (record) => record.get(0).properties) } catch (error) { @@ -161,7 +161,7 @@ async function getParentGroups(session, groupId) { try { const res = await session.run( 'MATCH (g:Group)-[r:GroupContains]->(c:Group {id: {groupId}}) RETURN g ORDER BY g.oldId', - { groupId } + {groupId} ) return _.map(res.records, (record) => record.get(0).properties) } catch (error) { @@ -176,7 +176,7 @@ async function getParentGroups(session, groupId) { * @returns {String} link for the page */ function getPageLink(req, page) { - const q = _.assignIn({}, req.query, { page }) + const q = _.assignIn({}, req.query, {page}) return `${req.protocol}://${req.get('Host')}${req.baseUrl}${req.path}?${querystring.stringify(q)}` } diff --git a/src/controllers/GroupMembershipController.js b/src/controllers/GroupMembershipController.js index 2c9629f..fa1d1bb 100644 --- a/src/controllers/GroupMembershipController.js +++ b/src/controllers/GroupMembershipController.js @@ -56,7 +56,8 @@ async function deleteGroupMember(req, res) { const result = await service.deleteGroupMember( req.authUser.isMachine ? 'M2M' : req.authUser, req.params.groupId, - req.params.memberId + req.params.memberId ? req.params.memberId : null, + Object.keys(req.query).length !== 0 ? req.query : null ) res.send(result) } @@ -71,13 +72,36 @@ async function getGroupMembersCount(req, res) { res.send(result) } +/** + * Get list of mapping of groups and members count + * @param req the request + * @param res the response + */ +async function listGroupsMemberCount(req, res) { + const result = await service.listGroupsMemberCount(req.query) + res.send(result) +} + /** * Get group members * @param req the request * @param res the response */ async function getMemberGroups(req, res) { - const result = await service.getMemberGroups(req.authUser.isMachine ? 'M2M' : req.authUser, req.params.memberId) + const result = await service.getMemberGroups(req.authUser.isMachine ? 'M2M' : req.authUser, req.params.memberId, {}) + helper.setResHeaders(req, res, result) + res.send(result) +} + +/** + * Get group members + * @param req the request + * @param res the response + */ +async function searchMemberGroups(req, res) { + console.log('sssss') + console.log(JSON.stringify(req.query)) + const result = await service.getMemberGroups(req.authUser.isMachine ? 'M2M' : req.authUser, {}, req.query) helper.setResHeaders(req, res, result) res.send(result) } @@ -88,5 +112,7 @@ module.exports = { getGroupMember, deleteGroupMember, getGroupMembersCount, - getMemberGroups + listGroupsMemberCount, + getMemberGroups, + searchMemberGroups } diff --git a/src/routes.js b/src/routes.js index 801f956..8776931 100644 --- a/src/routes.js +++ b/src/routes.js @@ -73,6 +73,13 @@ module.exports = { auth: 'jwt', access: [constants.UserRoles.Admin, constants.UserRoles.User], scopes: ['write:groups', 'all:groups'] + }, + delete: { + controller: 'GroupMembershipController', + method: 'deleteGroupMember', + auth: 'jwt', + access: [constants.UserRoles.Admin], + scopes: ['write:groups', 'all:groups'] } }, '/groups/:groupId/members/:memberId': { @@ -97,6 +104,12 @@ module.exports = { method: 'getGroupMembersCount' } }, + '/groupsMemberCount': { + get: { + controller: 'GroupMembershipController', + method: 'listGroupsMemberCount' + } + }, '/groups/memberGroups/:memberId': { get: { controller: 'GroupMembershipController', @@ -106,6 +119,15 @@ module.exports = { scopes: ['read:groups'] } }, + '/groups/memberGroups/': { + get: { + controller: 'GroupMembershipController', + method: 'searchMemberGroups', + auth: 'jwt', + access: [constants.UserRoles.Admin, constants.UserRoles.User], + scopes: ['read:groups'] + } + }, '/health': { get: { controller: 'HealthController', diff --git a/src/services/GroupMembershipService.js b/src/services/GroupMembershipService.js index 153dd09..58e0544 100644 --- a/src/services/GroupMembershipService.js +++ b/src/services/GroupMembershipService.js @@ -10,6 +10,7 @@ const logger = require('../common/logger') const errors = require('../common/errors') const validate = require('uuid-validate') const constants = require('../../app-constants') +const {ConsoleTransportOptions} = require('winston/lib/winston/transports') /** * Add group member. @@ -34,24 +35,26 @@ async function addGroupMember(currentUser, groupId, data) { data.oldId = group.oldId groupId = group.id + const memberId = data.memberId ? data.memberId : data.universalUID + if ( currentUser !== 'M2M' && !helper.hasAdminRole(currentUser) && !( group.selfRegister && data.membershipType === config.MEMBERSHIP_TYPES.User && - Number(currentUser.userId) === Number(data.memberId) + Number(currentUser.userId) === Number(memberId) ) ) { throw new errors.ForbiddenError('You are not allowed to perform this action!') } if (data.membershipType === config.MEMBERSHIP_TYPES.Group) { - if (data.memberId === groupId) { + if (memberId === groupId) { throw new errors.BadRequestError('A group can not add to itself.') } - logger.debug(`Check for groupId ${data.memberId} exist or not`) - const childGroup = await helper.ensureExists(tx, 'Group', data.memberId) + logger.debug(`Check for groupId ${memberId} exist or not`) + const childGroup = await helper.ensureExists(tx, 'Group', memberId) data.memberOldId = childGroup.oldId // if parent group is private, the sub group must be private too @@ -59,16 +62,26 @@ async function addGroupMember(currentUser, groupId, data) { throw new errors.ConflictError('Parent group is private, the child group must be private too.') } } else { - logger.debug(`Check for memberId ${data.memberId} exist or not`) - await helper.ensureExists(tx, 'User', data.memberId) + logger.debug(`Check for memberId ${memberId} exist or not`) + await helper.ensureExists(tx, 'User', memberId) } - logger.debug(`check member ${data.memberId} is part of group ${groupId}`) + logger.debug(`check member ${memberId} is part of group ${groupId}`) const targetObjectType = data.membershipType === config.MEMBERSHIP_TYPES.Group ? 'Group' : 'User' - const memberCheckRes = await tx.run( - `MATCH (g:Group {id: {groupId}})-[r:GroupContains]->(o:${targetObjectType} {id: {memberId}}) RETURN o`, - { groupId, memberId: data.memberId } - ) + + let memberCheckRes + if (data.universalUID) { + memberCheckRes = await tx.run( + `MATCH (g:Group {id: {groupId}})-[r:GroupContains]->(o:${targetObjectType} {universalUID: {memberId}}) RETURN o`, + {groupId, memberId} + ) + } else { + memberCheckRes = await tx.run( + `MATCH (g:Group {id: {groupId}})-[r:GroupContains]->(o:${targetObjectType} {id: {memberId}}) RETURN o`, + {groupId, memberId: data.memberId} + ) + } + if (memberCheckRes.records.length > 0) { throw new errors.ConflictError('The member is already in the group') } @@ -77,7 +90,7 @@ async function addGroupMember(currentUser, groupId, data) { if (data.membershipType === config.MEMBERSHIP_TYPES.Group) { const pathRes = await tx.run( 'MATCH p=shortestPath( (g1:Group {id: {fromId}})-[*]->(g2:Group {id: {toId}}) ) RETURN p', - { fromId: data.memberId, toId: groupId } + {fromId: data.memberId, toId: groupId} ) if (pathRes.records.length > 0) { throw new errors.ConflictError('There is cyclical group reference') @@ -87,11 +100,17 @@ async function addGroupMember(currentUser, groupId, data) { // add membership const membershipId = uuid() const createdAt = new Date().toISOString() - const query = `MATCH (g:Group {id: {groupId}}) MATCH (o:${targetObjectType} {id: {memberId}}) CREATE (g)-[r:GroupContains {id: {membershipId}, type: {membershipType}, createdAt: {createdAt}, createdBy: {createdBy}}]->(o) RETURN r` + + let query + if (validate(memberId, 4)) { + query = `MATCH (g:Group {id: {groupId}}) MATCH (o:User {universalUID: {memberId}}) CREATE (g)-[r:GroupContains {id: {membershipId}, type: {membershipType}, createdAt: {createdAt}, createdBy: {createdBy}}]->(o) RETURN r` + } else { + query = `MATCH (g:Group {id: {groupId}}) MATCH (o:${targetObjectType} {id: {memberId}}) CREATE (g)-[r:GroupContains {id: {membershipId}, type: {membershipType}, createdAt: {createdAt}, createdBy: {createdBy}}]->(o) RETURN r` + } const params = { groupId, - memberId: data.memberId, + memberId, membershipId, membershipType: data.membershipType, createdAt, @@ -107,9 +126,10 @@ async function addGroupMember(currentUser, groupId, data) { oldId: data.oldId, name: group.name, createdAt, - ...(currentUser === 'M2M' ? {} : { createdBy: currentUser.userId }), - memberId: data.memberId, - ...(data.memberOldId ? { memberOldId: data.memberOldId } : {}), + ...(currentUser === 'M2M' ? {} : {createdBy: currentUser.userId}), + ...(data.memberId ? {memberId: data.memberId} : {}), + ...(data.universalUID ? {universalUID: data.universalUID} : {}), + ...(data.memberOldId ? {memberOldId: data.memberOldId} : {}), membershipType: data.membershipType } @@ -129,16 +149,28 @@ async function addGroupMember(currentUser, groupId, data) { } } -addGroupMember.schema = { - currentUser: Joi.any(), - groupId: Joi.id(), // defined in app-bootstrap - data: Joi.object() - .keys({ - memberId: Joi.id(), - membershipType: Joi.string().valid(_.values(config.MEMBERSHIP_TYPES)).required() - }) - .required() -} +addGroupMember.schema = Joi.alternatives().try( + Joi.object().keys({ + currentUser: Joi.any(), + groupId: Joi.id(), // defined in app-bootstrap + data: Joi.object() + .keys({ + memberId: Joi.id(), + membershipType: Joi.string().valid(_.values(config.MEMBERSHIP_TYPES)).required() + }) + .required() + }), + Joi.object().keys({ + currentUser: Joi.any(), + groupId: Joi.id(), // defined in app-bootstrap + data: Joi.object() + .keys({ + universalUID: Joi.id(), + membershipType: Joi.string().valid(_.values(config.MEMBERSHIP_TYPES)).required() + }) + .required() + }) +) /** * Delete group member. @@ -147,7 +179,7 @@ addGroupMember.schema = { * @param {String} memberId the member id * @returns {Object} the deleted group membership */ -async function deleteGroupMember(currentUser, groupId, memberId) { +async function deleteGroupMember(currentUser, groupId, memberId, query) { logger.debug(`Enter in deleteGroupMember - Group = ${groupId} memberId = ${memberId}`) let session = helper.createDBSession() let tx = session.beginTransaction() @@ -163,6 +195,7 @@ async function deleteGroupMember(currentUser, groupId, memberId) { groupId = group.id const oldId = group.oldId const name = group.name + const universalUID = query ? query.universalUID : null if ( currentUser !== 'M2M' && @@ -173,8 +206,19 @@ async function deleteGroupMember(currentUser, groupId, memberId) { } // delete membership - const query = 'MATCH (g:Group {id: {groupId}})-[r:GroupContains]->(o {id: {memberId}}) DELETE r' - await tx.run(query, { groupId, memberId }) + if (universalUID) { + const query = 'MATCH (g:Group {id: {groupId}})-[r:GroupContains]->(o {universalUID: {universalUID}}) DELETE r' + await tx.run(query, {groupId, universalUID}) + + const matchClause = 'MATCH (u:User {universalUID: {universalUID}})' + const params = {universalUID} + + const res = await tx.run(`${matchClause} RETURN u.id as memberId`, params) + memberId = _.head(_.head(res.records)._fields) + } else { + const query = 'MATCH (g:Group {id: {groupId}})-[r:GroupContains]->(o {id: {memberId}}) DELETE r' + await tx.run(query, {groupId, memberId}) + } if (validate(memberId, 4)) { const getMember = await helper.ensureExists(tx, 'Group', memberId) @@ -188,8 +232,8 @@ async function deleteGroupMember(currentUser, groupId, memberId) { memberId } - logger.debug(`sending message ${JSON.stringify(result)} to kafka topic ${config.KAFKA_GROUP_MEMBER_DELETE_TOPIC}`) - await helper.postBusEvent(config.KAFKA_GROUP_MEMBER_DELETE_TOPIC, result) + // logger.debug(`sending message ${JSON.stringify(result)} to kafka topic ${config.KAFKA_GROUP_MEMBER_DELETE_TOPIC}`) + // await helper.postBusEvent(config.KAFKA_GROUP_MEMBER_DELETE_TOPIC, result) await tx.commit() return result @@ -207,7 +251,8 @@ async function deleteGroupMember(currentUser, groupId, memberId) { deleteGroupMember.schema = { currentUser: Joi.any(), groupId: Joi.id(), // defined in app-bootstrap - memberId: Joi.id() + memberId: Joi.optionalId().allow('', null), + query: Joi.object().allow('', null) } /** @@ -235,7 +280,7 @@ async function getGroupMembers(currentUser, groupId, criteria) { } const matchClause = 'MATCH (g:Group {id: {groupId}})-[r:GroupContains]->(o)' - const params = { groupId } + const params = {groupId} // query total record count const totalRes = await session.run(`${matchClause} RETURN COUNT(o)`, params) @@ -258,6 +303,7 @@ async function getGroupMembers(currentUser, groupId, criteria) { createdAt: r.createdAt, createdBy: r.createdBy, memberId: o.id, + universalUID: o.universalUID, membershipType: r.type } }) @@ -299,7 +345,7 @@ async function getGroupMemberWithSession(session, groupId, memberId) { groupId = group.id const query = 'MATCH (g:Group {id: {groupId}})-[r:GroupContains]->(o {id: {memberId}}) RETURN r' - const membershipRes = await session.run(query, { groupId, memberId }) + const membershipRes = await session.run(query, {groupId, memberId}) if (membershipRes.records.length === 0) { throw new errors.NotFoundError('The member is not in the group') } @@ -370,14 +416,14 @@ async function getGroupMembersCount(groupId, query) { let queryToExecute = '' if (query.includeSubGroups) { - queryToExecute = 'MATCH (g:Group {id: {groupId}})-[r:GroupContains*1..10]->(o:User) RETURN COUNT(o) AS count' + queryToExecute = 'MATCH (g:Group {id: {groupId}})-[r:GroupContains*1..]->(o:User) RETURN COUNT(o) AS count' } else { queryToExecute = 'MATCH (g:Group {id: {groupId}})-[r:GroupContains]->(o:User) RETURN COUNT(o) AS count' } - const res = await session.run(queryToExecute, { groupId }) + const res = await session.run(queryToExecute, {groupId}) - return { count: res.records[0]._fields[0].low } + return {count: res.records[0]._fields[0].low} } catch (error) { logger.error(error) throw error @@ -388,17 +434,107 @@ async function getGroupMembersCount(groupId, query) { } getGroupMembersCount.schema = { - groupId: Joi.id(), // defined in app-bootstrap + groupId: Joi.optionalId(), // defined in app-bootstrap query: Joi.object().keys({ includeSubGroups: Joi.boolean().default(false) }) } +/** + * Get list of groups for specified user and member count of those groups. Optionally may include sub groups. + * @param {Object} query the query parameters + * @returns {Object} list of groupId and memberCount mapping + */ +async function listGroupsMemberCount(query) { + const session = helper.createDBSession() + try { + let queryToExecute = '' + + if (query.includeSubGroups) { + queryToExecute = + 'MATCH (g:Group)-[r:GroupContains*1..]->(o:User) WHERE exists(g.oldId) AND g.status = {status} RETURN g.oldId, g.id, COUNT(o) AS count order by g.oldId' + } else { + queryToExecute = + 'MATCH (g:Group)-[r:GroupContains]->(o:User) WHERE exists(g.oldId) AND g.status = {status} RETURN g.oldId, g.id, COUNT(o) AS count order by g.oldId' + } + let res = await session.run(queryToExecute, {status: constants.GroupStatus.Active}) + + const groupsMemberCount = [] + res.records.forEach(function (record) { + let groupMemberCount = { + id: record._fields[1], + oldId: record._fields[0], + count: record._fields[2].low + } + groupsMemberCount.push(groupMemberCount) + }) + + if (query.includeSubGroups) { + if (query.universalUID) { + queryToExecute = + 'MATCH (g:Group)-[r:GroupContains*1..]->(o:User {universalUID: {universalUID}}) WHERE exists(g.oldId) AND g.status = {status} RETURN DISTINCT g.oldId order by g.oldId' + res = await session.run(queryToExecute, { + universalUID: query.universalUID, + status: constants.GroupStatus.Active + }) + } + + if (query.organizationId) { + queryToExecute = + 'MATCH (g:Group)-[r:GroupContains*1..]->(o:User) WHERE g.organizationId = {organizationId} AND exists(g.oldId) AND g.status = {status} RETURN DISTINCT g.oldId order by g.oldId' + res = await session.run(queryToExecute, { + organizationId: query.organizationId, + status: constants.GroupStatus.Active + }) + } + } else { + if (query.universalUID) { + queryToExecute = + 'MATCH (g:Group)-[r:GroupContains]->(o:User {universalUID: {universalUID}}) WHERE exists(g.oldId) AND g.status = {status} RETURN DISTINCT g.oldId order by g.oldId' + res = await session.run(queryToExecute, { + universalUID: query.universalUID, + status: constants.GroupStatus.Active + }) + } + + if (query.organizationId) { + queryToExecute = + 'MATCH (g:Group)-[r:GroupContains]->(o:User) WHERE g.organizationId = {organizationId} AND exists(g.oldId) AND g.status = {status} RETURN DISTINCT g.oldId order by g.oldId' + res = await session.run(queryToExecute, { + organizationId: query.organizationId, + status: constants.GroupStatus.Active + }) + } + } + + const groupList = _.flatten(_.map(res.records, '_fields')) + const finalRes = _.filter(groupsMemberCount, function (n) { + return _.includes(groupList, n.oldId) + }) + + return finalRes + } catch (error) { + logger.error(error) + throw error + } finally { + logger.debug('Session Close') + await session.close() + } +} + +listGroupsMemberCount.schema = { + query: Joi.object().keys({ + includeSubGroups: Joi.boolean().default(false), + universalUID: Joi.optionalId(), + organizationId: Joi.optionalId() + }) +} + /** * Get member groups * @param {Object} currentUser the current user * @param {Object} memberId - * @param {Object} criteria the search criteria + * @param {Object} query the search criteria * @returns {Object} the search result */ async function getMemberGroups(currentUser, memberId) { @@ -429,6 +565,7 @@ module.exports = { getGroupMember, deleteGroupMember, getGroupMembersCount, + listGroupsMemberCount, getMemberGroups } diff --git a/src/services/GroupService.js b/src/services/GroupService.js index 27aab31..5e376df 100644 --- a/src/services/GroupService.js +++ b/src/services/GroupService.js @@ -19,10 +19,10 @@ const constants = require('../../app-constants') async function searchGroups(criteria, isAdmin = false) { logger.debug(`Search Group - Criteria - ${JSON.stringify(criteria)}`) - if (criteria.memberId && !criteria.membershipType) { + if ((criteria.memberId || criteria.universalUID) && !criteria.membershipType) { throw new errors.BadRequestError('The membershipType parameter should be provided if memberId is provided.') } - if (!criteria.memberId && criteria.membershipType) { + if (!(criteria.memberId || criteria.universalUID) && criteria.membershipType) { throw new errors.BadRequestError('The memberId parameter should be provided if membershipType is provided.') } @@ -35,6 +35,12 @@ async function searchGroups(criteria, isAdmin = false) { matchClause = `MATCH (g:Group)` } + if (criteria.universalUID) { + matchClause = `MATCH (g:Group)-[r:GroupContains {type: "${criteria.membershipType}"}]->(o {universalUID: "${criteria.universalUID}"})` + } else { + matchClause = `MATCH (g:Group)` + } + let whereClause = '' if (criteria.oldId) { whereClause = ` WHERE g.oldId = "${criteria.oldId}"` @@ -149,6 +155,7 @@ searchGroups.schema = { isAdmin: Joi.boolean(), criteria: Joi.object().keys({ memberId: Joi.optionalId(), // defined in app-bootstrap + universalUID: Joi.optionalId(), membershipType: Joi.string().valid(_.values(config.MEMBERSHIP_TYPES)), name: Joi.string(), page: Joi.page(),