Skip to content

Commit 9bff750

Browse files
committed
feat: project members endpoint with additional fields
Added endpoint for getting project members with ability to get additional data form member service and pre-populate the member list. ref: Winner submission from challenge 30107353 - "Topcoder Project Service - Project Members Traits"
1 parent 9839c0e commit 9bff750

File tree

4 files changed

+144
-0
lines changed

4 files changed

+144
-0
lines changed

src/permissions/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ module.exports = () => {
2222
Authorizer.setPolicy('project.edit', projectEdit);
2323
Authorizer.setPolicy('project.delete', projectDelete);
2424
Authorizer.setPolicy('project.addMember', projectView);
25+
Authorizer.setPolicy('project.getMember', projectView);
2526
Authorizer.setPolicy('project.removeMember', projectMemberDelete);
2627
Authorizer.setPolicy('project.addAttachment', projectEdit);
2728
Authorizer.setPolicy('project.updateAttachment', projectAttachmentUpdate);

src/routes/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ router.route('/v4/projects/:projectId(\\d+)/scopeChangeRequests/:requestId(\\d+)
119119
// .delete(require('./scopeChangeRequests/delete'));
120120

121121
router.route('/v4/projects/:projectId(\\d+)/members')
122+
.get(require('./projectMembers/get'))
122123
.post(require('./projectMembers/create'));
123124

124125
router.route('/v4/projects/:projectId(\\d+)/members/:id(\\d+)')

src/routes/projectMembers/get.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/* eslint-disable max-len */
2+
import _ from 'lodash';
3+
import { middleware as tcMiddleware } from 'tc-core-library-js';
4+
import util from '../../util';
5+
6+
/**
7+
* API to add a project member.
8+
* add members directly (only managers and copilots)
9+
* user being added is current user
10+
*/
11+
const permissions = tcMiddleware.permissions;
12+
13+
14+
module.exports = [
15+
// handles request validations
16+
// validate(createProjectMemberValidations),
17+
permissions('project.getMember'),
18+
async (req, res) => {
19+
let members = req.context.currentProjectMembers;
20+
21+
22+
if (members.length && _.get(req, 'query.fields')) {
23+
const fields = req.query.fields.split(',');
24+
25+
const ModelFields = ['id', 'userId', 'role', 'isPrimary', 'deletedAt', 'createdAt', 'updatedAt', 'deletedBy', 'createdBy', 'updatedBy'];
26+
27+
const modelFields = _.intersection(ModelFields, fields);
28+
const hasUserIdField = _.indexOf(fields, 'userId') !== -1;
29+
if (hasUserIdField === false) {
30+
modelFields.push('userId');
31+
}
32+
// merge model fields
33+
members = _.map(members, m => _.pick(m, modelFields));
34+
35+
const MemberDetailFields = ['handle', 'firstName', 'lastName'];
36+
const memberDetailFields = _.intersection(MemberDetailFields, fields);
37+
38+
// has handleField
39+
const hasHandleField = _.indexOf(memberDetailFields, 'handle') !== -1;
40+
41+
if (hasHandleField === false) {
42+
memberDetailFields.push('handle');
43+
}
44+
45+
if (memberDetailFields.length) {
46+
const userIds = _.map(members, m => `userId:${m.userId}`) || [];
47+
48+
const logger = req.log;
49+
try {
50+
const memberDetails = await util.getMemberDetailsByUserIds(userIds, logger, req.id);
51+
52+
// merge detail fields
53+
_.forEach(members, (m) => {
54+
const detail = _.find(memberDetails, mt => mt.userId === m.userId);
55+
if (detail) {
56+
_.assign(m, _.pick(detail, memberDetailFields));
57+
}
58+
});
59+
60+
61+
const TraitFields = ['photoURL', 'workingHoursStart', 'workingHoursEnd', 'timeZone'];
62+
const traitFields = _.intersection(TraitFields, fields);
63+
if (traitFields.length) {
64+
const promises = [];
65+
_.forEach(members, (m) => {
66+
if (m.handle) {
67+
promises.push(util.getMemberTratisByHandle(m.handle, req.log, req.id));
68+
}
69+
});
70+
71+
const traits = await Promise.all(promises);
72+
73+
// remove photoURL, because connect_info also has photoURL
74+
const ConnectInfoFields = ['workingHoursStart', 'workingHoursEnd', 'timeZone'];
75+
const connectInfoFields = _.intersection(ConnectInfoFields, fields);
76+
77+
// merge traits
78+
_.forEach(members, (m) => {
79+
const traitsArr = _.find(traits, t => t[0].userId === m.userId);
80+
if (traitsArr) {
81+
if (traitFields[0] === 'photoURL') {
82+
_.assign(m, { photoURL: _.get(_.find(traitsArr, t => t.traitId === 'basic_info'), 'traits.data[0].photoURL') });
83+
if (traitFields.length > 1) {
84+
const traitInfo = _.get(_.find(traitsArr, t => t.traitId === 'connect_info'), 'traits.data[0]', {});
85+
_.assign(m, _.pick(traitInfo, connectInfoFields));
86+
}
87+
} else {
88+
const traitInfo = _.get(_.find(traitsArr, t => t.traitId === 'connect_info'), 'traits.data[0]', {});
89+
_.assign(m, _.pick(traitInfo, connectInfoFields));
90+
}
91+
}
92+
});
93+
}
94+
} catch (e) {
95+
if (hasUserIdField === false) {
96+
members = _.map(members, m => _.omit(m, ['userId']));
97+
}
98+
if (hasHandleField === false) {
99+
members = _.map(members, m => _.omit(m, ['handle']));
100+
}
101+
return res.json(util.wrapResponse(req.id, members));
102+
}
103+
}
104+
105+
if (hasUserIdField === false) {
106+
members = _.map(members, m => _.omit(m, ['userId']));
107+
}
108+
if (hasHandleField === false) {
109+
members = _.map(members, m => _.omit(m, ['handle']));
110+
}
111+
return res.json(util.wrapResponse(req.id, members));
112+
}
113+
114+
return res.json(util.wrapResponse(req.id, req.context.currentProjectMembers));
115+
},
116+
];

src/util.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,32 @@ _.assignIn(util, {
392392
return esClient;
393393
},
394394

395+
/**
396+
* Retrieve member traitbyhandle
397+
* @param {string} handle
398+
* @param {Object} logger req.logger
399+
* @return {string} requestId
400+
*/
401+
getMemberTratisByHandle: Promise.coroutine(function* (handle, logger, requestId) { // eslint-disable-line func-names
402+
try {
403+
const token = yield this.getM2MToken();
404+
const httpClient = this.getHttpClient({ id: requestId, log: logger });
405+
if (logger) {
406+
logger.trace(handle);
407+
}
408+
409+
return httpClient.get(`${config.memberServiceEndpoint}/${handle}/traits`, {
410+
params: {},
411+
headers: {
412+
'Content-Type': 'application/json',
413+
Authorization: `Bearer ${token}`,
414+
},
415+
}).then(res => _.get(res, 'data.result.content', null));
416+
} catch (err) {
417+
return Promise.reject(err);
418+
}
419+
}),
420+
395421
/**
396422
* Retrieve member details from userIds
397423
*/

0 commit comments

Comments
 (0)