Skip to content

feat: project member & invites endpoint with additional fields #414

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/permissions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ module.exports = () => {
Authorizer.setPolicy('project.view', projectView);
Authorizer.setPolicy('project.edit', projectEdit);
Authorizer.setPolicy('project.delete', projectDelete);
Authorizer.setPolicy('project.getMember', projectView);
Authorizer.setPolicy('project.addMember', projectView);
Authorizer.setPolicy('project.listMembers', projectView);
Authorizer.setPolicy('project.removeMember', projectMemberDelete);
Expand Down Expand Up @@ -86,6 +87,7 @@ module.exports = () => {
Authorizer.setPolicy('projectMemberInvite.create', projectView);
Authorizer.setPolicy('projectMemberInvite.put', true);
Authorizer.setPolicy('projectMemberInvite.get', true);
Authorizer.setPolicy('projectMemberInvite.list', projectView);

Authorizer.setPolicy('form.create', projectAdmin);
Authorizer.setPolicy('form.edit', projectAdmin);
Expand Down
4 changes: 4 additions & 0 deletions src/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ router.route('/v4/projects/:projectId(\\d+)/members')
.post(require('./projectMembers/create'));

router.route('/v4/projects/:projectId(\\d+)/members/:id(\\d+)')
.get(require('./projectMembers/get'))
.delete(require('./projectMembers/delete'))
.patch(require('./projectMembers/update'));

Expand Down Expand Up @@ -230,6 +231,9 @@ router.route('/v4/timelines/metadata/milestoneTemplates/:milestoneTemplateId(\\d
.patch(require('./milestoneTemplates/update'))
.delete(require('./milestoneTemplates/delete'));

router.route('/v4/projects/:projectId(\\d+)/members/invites')
.get(require('./projectMemberInvites/list'));

router.route('/v4/projects/:projectId(\\d+)/members/invite')
.post(require('./projectMemberInvites/create'))
.put(require('./projectMemberInvites/update'))
Expand Down
56 changes: 36 additions & 20 deletions src/routes/projectMemberInvites/get.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@


import _ from 'lodash';
import Joi from 'joi';
import validate from 'express-validation';
import { middleware as tcMiddleware } from 'tc-core-library-js';
import models from '../../models';
import util from '../../util';
Expand All @@ -9,27 +9,43 @@ import util from '../../util';
* API to update invite member to project.
*
*/
const schema = {
query: {
fields: Joi.string().optional(),
},
};
const permissions = tcMiddleware.permissions;

module.exports = [
// handles request validations
validate(schema),
permissions('projectMemberInvite.get'),
(req, res, next) => {
const projectId = _.parseInt(req.params.projectId);
const currentUserId = req.authUser.userId;
let invite;
return models.ProjectMemberInvite.getPendingInviteByEmailOrUserId(projectId, req.authUser.email, currentUserId)
.then((_invite) => {
invite = _invite;
if (!invite) {
// check there is an existing invite for the user with status PENDING
// handle 404
const err = new Error('invite not found for project id ' +
`${projectId}, userId ${currentUserId}, email ${req.authUser.email}`);
err.status = 404;
return next(err);
}
return res.json(util.wrapResponse(req.id, invite));
});
async (req, res, next) => {
try {
const projectId = _.parseInt(req.params.projectId);
const currentUserId = req.authUser.userId;
const invite = await models.ProjectMemberInvite.getPendingInviteByEmailOrUserId(
projectId, req.authUser.email, currentUserId,
);
if (!invite) {
// check there is an existing invite for the user with status PENDING
// handle 404
const err = new Error(
'invite not found for project id ' +
`${projectId}, userId ${currentUserId}, email ${req.authUser.email}`,
);
err.status = 404;
throw err;
}

let fields = null;
if (req.query.fields) {
fields = req.query.fields.split(',');
}
const [inviteWithDetails] = await util.getObjectsWithMemberDetails([invite], fields, req);

return res.json(util.wrapResponse(req.id, inviteWithDetails));
} catch (err) {
return next(err);
}
},
];
37 changes: 37 additions & 0 deletions src/routes/projectMemberInvites/list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import _ from 'lodash';
import Joi from 'joi';
import validate from 'express-validation';
import { middleware as tcMiddleware } from 'tc-core-library-js';
import models from '../../models';
import util from '../../util';

/**
* API to list all project member invites.
*
*/
const permissions = tcMiddleware.permissions;

const schema = {
query: {
fields: Joi.string().optional(),
},
};

module.exports = [
validate(schema),
permissions('projectMemberInvite.list'),
async (req, res, next) => {
try {
let fields = null;
if (req.query.fields) {
fields = req.query.fields.split(',');
}
const projectId = _.parseInt(req.params.projectId);
const invites = await models.ProjectMemberInvite.getPendingInvitesForProject(projectId);
const invitesWithDetails = await util.getObjectsWithMemberDetails(invites, fields, req);
return res.json(util.wrapResponse(req.id, invitesWithDetails));
} catch (err) {
return next(err);
}
},
];
46 changes: 46 additions & 0 deletions src/routes/projectMembers/get.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@


import _ from 'lodash';
import Joi from 'joi';
import validate from 'express-validation';
import { middleware as tcMiddleware } from 'tc-core-library-js';
import util from '../../util';

/**
* API to get a project member in a project.
*/
const permissions = tcMiddleware.permissions;

const schema = {
query: {
fields: Joi.string().optional(),
},
};

module.exports = [
validate(schema),
permissions('project.getMember'),
async (req, res, next) => {
try {
const projectId = _.parseInt(req.params.projectId);
let fields = null;
if (req.query.fields) {
fields = req.query.fields.split(',');
}
const memberId = _.parseInt(req.params.id);
let member = _.find(req.context.currentProjectMembers, user => user.id === memberId);
if (!member) {
const err = new Error(
`member not found for project id ${projectId}, userId ${memberId}`,
);
err.status = 404;
throw err;
}
const memberDetails = await util.getObjectsWithMemberDetails([member], fields, req);
member = _.first(memberDetails);
return res.json(util.wrapResponse(req.id, member));
} catch (err) {
return next(err);
}
},
];
135 changes: 22 additions & 113 deletions src/routes/projectMembers/list.js
Original file line number Diff line number Diff line change
@@ -1,124 +1,33 @@
/**
* Endpoint to list project members.
*/
import _ from 'lodash';
import Joi from 'joi';
import validate from 'express-validation';
import { middleware as tcMiddleware } from 'tc-core-library-js';
import util from '../../util';

/**
* API to list all project members.
*
*/
const permissions = tcMiddleware.permissions;

const schema = {
query: {
fields: Joi.string().optional(),
},
};

module.exports = [
validate(schema),
permissions('project.listMembers'),
async (req, res) => {
let members = req.context.currentProjectMembers;


if (members.length && _.get(req, 'query.fields')) {
const fields = req.query.fields.split(',');

const ModelFields = [
'id',
'userId',
'role',
'isPrimary',
'deletedAt',
'createdAt',
'updatedAt',
'deletedBy',
'createdBy',
'updatedBy',
];

const modelFields = _.intersection(ModelFields, fields);
const hasUserIdField = _.indexOf(fields, 'userId') !== -1;
if (hasUserIdField === false) {
modelFields.push('userId');
}
// merge model fields
members = _.map(members, m => _.pick(m, modelFields));

const MemberDetailFields = ['handle', 'firstName', 'lastName'];
const memberDetailFields = _.intersection(MemberDetailFields, fields);

// has handleField
const hasHandleField = _.indexOf(memberDetailFields, 'handle') !== -1;

if (hasHandleField === false) {
memberDetailFields.push('handle');
}

if (memberDetailFields.length) {
const userIds = _.map(members, m => `userId:${m.userId}`) || [];

const logger = req.log;
try {
const memberDetails = await util.getMemberDetailsByUserIds(userIds, logger, req.id);

// merge detail fields
_.forEach(members, (m) => {
const detail = _.find(memberDetails, mt => mt.userId === m.userId);
if (detail) {
_.assign(m, _.pick(detail, memberDetailFields));
}
});


const TraitFields = ['photoURL', 'workingHourStart', 'workingHourEnd', 'timeZone'];
const traitFields = _.intersection(TraitFields, fields);
if (traitFields.length) {
const promises = [];
_.forEach(members, (m) => {
if (m.handle) {
promises.push(util.getMemberTratisByHandle(m.handle, req.log, req.id));
}
});

const traits = await Promise.all(promises);

// remove photoURL, because connect_info also has photoURL
const ConnectInfoFields = ['workingHourStart', 'workingHourEnd', 'timeZone'];
const connectInfoFields = _.intersection(ConnectInfoFields, fields);

// merge traits
_.forEach(members, (m) => {
const traitsArr = _.find(traits, t => t[0].userId === m.userId);
if (traitsArr) {
if (traitFields[0] === 'photoURL') {
_.assign(m, {
photoURL: _.get(_.find(traitsArr, { traitId: 'basic_info' }), 'traits.data[0].photoURL'),
});
if (traitFields.length > 1) {
const traitInfo = _.get(_.find(traitsArr, { traitId: 'connect_info' }), 'traits.data[0]', {});
_.assign(m, _.pick(traitInfo, connectInfoFields));
}
} else {
const traitInfo = _.get(_.find(traitsArr, { traitId: 'connect_info' }), 'traits.data[0]', {});
_.assign(m, _.pick(traitInfo, connectInfoFields));
}
}
});
}
} catch (e) {
logger.error('Error getting member details', e);
if (hasUserIdField === false) {
members = _.map(members, m => _.omit(m, ['userId']));
}
if (hasHandleField === false) {
members = _.map(members, m => _.omit(m, ['handle']));
}
return res.json(util.wrapResponse(req.id, members));
}
}

if (hasUserIdField === false) {
members = _.map(members, m => _.omit(m, ['userId']));
}
if (hasHandleField === false) {
members = _.map(members, m => _.omit(m, ['handle']));
}
async (req, res, next) => {
let fields = null;
if (req.query.fields) {
fields = req.query.fields.split(',');
}
try {
const members = await util.getObjectsWithMemberDetails(req.context.currentProjectMembers, fields, req);
return res.json(util.wrapResponse(req.id, members));
} catch (err) {
return next(err);
}

return res.json(util.wrapResponse(req.id, req.context.currentProjectMembers));
},
];
Loading