Skip to content

Commit a3190cf

Browse files
committed
winning submission from challenge 30085188 - Topcoder Connect - Handle invitation errors
1 parent b5b574c commit a3190cf

File tree

4 files changed

+170
-73
lines changed

4 files changed

+170
-73
lines changed

postman.json

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,31 @@
10411041
},
10421042
"response": []
10431043
},
1044+
{
1045+
"name": "Invite with userIds and emails - both success and failed",
1046+
"request": {
1047+
"url": "{{api-url}}/v4/projects/1/members/invite",
1048+
"method": "POST",
1049+
"header": [
1050+
{
1051+
"key": "Authorization",
1052+
"value": "Bearer {{jwt-token-manager-40051334}}",
1053+
"description": ""
1054+
},
1055+
{
1056+
"key": "Content-Type",
1057+
"value": "application/json",
1058+
"description": ""
1059+
}
1060+
],
1061+
"body": {
1062+
"mode": "raw",
1063+
"raw": "{\n\t\"param\": {\n\t\t\"userIds\": [40051331, 40051334],\n\t\t\"emails\": [\"divyalife526@gmail.com\"],\n\t\t\"role\": \"manager\"\n\t}\n}"
1064+
},
1065+
"description": ""
1066+
},
1067+
"response": []
1068+
},
10441069
{
10451070
"name": "Update invite status with userId",
10461071
"request": {
@@ -5497,4 +5522,4 @@
54975522
]
54985523
}
54995524
]
5500-
}
5525+
}

src/routes/projectMemberInvites/create.js

Lines changed: 48 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,12 @@ const addMemberValidations = {
3535
* @param {Object} invite invite to process
3636
* @param {Array} invites existent invites from DB
3737
* @param {Object} data template for new invites to be put in DB
38+
* @param {Array} failed failed invites error message
3839
*
3940
* @returns {Promise<Promise[]>} list of promises
4041
*/
41-
const buildCreateInvitePromises = (req, invite, invites, data) => {
42+
const buildCreateInvitePromises = (req, invite, invites, data, failed) => {
4243
const invitePromises = [];
43-
4444
if (invite.userIds) {
4545
// remove invites for users that are invited already
4646
_.remove(invite.userIds, u => _.some(invites, i => i.userId === u));
@@ -91,15 +91,15 @@ const buildCreateInvitePromises = (req, invite, invites, data) => {
9191

9292
invitePromises.push(models.ProjectMemberInvite.create(dataNew));
9393
});
94-
95-
return Promise.resolve(invitePromises);
94+
return invitePromises;
9695
}).catch((error) => {
9796
req.log.error(error);
98-
return Promise.reject(invitePromises);
97+
_.forEach(invite.emails, email => failed.push(_.assign({}, { email, message: error.statusText })));
98+
return invitePromises;
9999
});
100100
}
101101

102-
return Promise.resolve(invitePromises);
102+
return invitePromises;
103103
};
104104

105105
const sendInviteEmail = (req, projectId, invite) => {
@@ -157,6 +157,7 @@ module.exports = [
157157
validate(addMemberValidations),
158158
permissions('projectMemberInvite.create'),
159159
(req, res, next) => {
160+
let failed = [];
160161
const invite = req.body.param;
161162

162163
if (!invite.userIds && !invite.emails) {
@@ -192,80 +193,80 @@ module.exports = [
192193
if (invite.emails) {
193194
// email invites can only be used for CUSTOMER role
194195
if (invite.role !== PROJECT_MEMBER_ROLE.CUSTOMER) { // eslint-disable-line no-lonely-if
195-
const err = new Error(`Emails can only be used for ${PROJECT_MEMBER_ROLE.CUSTOMER}`);
196-
err.status = 400;
197-
return next(err);
196+
const message = `Emails can only be used for ${PROJECT_MEMBER_ROLE.CUSTOMER}`;
197+
failed = _.concat(failed, _.map(invite.emails, email => _.assign({}, { email, message })));
198+
delete invite.emails;
198199
}
199200
}
200-
201201
if (promises.length === 0) {
202202
promises.push(Promise.resolve());
203203
}
204204
return Promise.all(promises).then((rolesList) => {
205+
const updatedInvite = invite;
205206
if (!!invite.userIds && _.includes(PROJECT_MEMBER_MANAGER_ROLES, invite.role)) {
206207
req.log.debug('Chekcing if userId is allowed as manager');
207208
const forbidUserList = [];
208209
_.zip(invite.userIds, rolesList).forEach((data) => {
209210
const [userId, roles] = data;
210211
req.log.debug(roles);
211212

212-
if (!util.hasIntersection(MANAGER_ROLES, roles)) {
213+
if (roles && !util.hasIntersection(MANAGER_ROLES, roles)) {
213214
forbidUserList.push(userId);
214215
}
215216
});
216217
if (forbidUserList.length > 0) {
217-
const err = new Error(`${forbidUserList.join()} cannot be added with a Manager role to the project`);
218-
err.status = 403;
219-
return next(err);
218+
const message = 'cannot be added with a Manager role to the project';
219+
failed = _.concat(failed, _.map(forbidUserList, id => _.assign({}, { id, message })));
220+
updatedInvite.userIds = _.filter(invite.userIds, userId => !_.includes(forbidUserList, userId));
220221
}
221222
}
222223
return models.ProjectMemberInvite.getPendingInvitesForProject(projectId)
223224
.then((invites) => {
224225
const data = {
225226
projectId,
226-
role: invite.role,
227+
role: updatedInvite.role,
227228
// invite directly if user is admin or copilot manager
228-
status: (invite.role !== PROJECT_MEMBER_ROLE.COPILOT ||
229+
status: (updatedInvite.role !== PROJECT_MEMBER_ROLE.COPILOT ||
229230
util.hasRoles(req, [USER_ROLE.CONNECT_ADMIN, USER_ROLE.COPILOT_MANAGER]))
230231
? INVITE_STATUS.PENDING
231232
: INVITE_STATUS.REQUESTED,
232233
createdBy: req.authUser.userId,
233234
updatedBy: req.authUser.userId,
234235
};
235236

236-
return buildCreateInvitePromises(req, invite, invites, data)
237-
.then((invitePromises) => {
238-
if (invitePromises.length === 0) {
239-
return [];
240-
}
241-
242-
req.log.debug('Creating invites');
243-
return models.sequelize.Promise.all(invitePromises)
244-
.then((values) => {
245-
values.forEach((v) => {
246-
req.app.emit(EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_CREATED, {
247-
req,
248-
userId: v.userId,
249-
email: v.email,
250-
status: v.status,
251-
role: v.role,
252-
});
253-
req.app.services.pubsub.publish(
254-
EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_CREATED,
255-
v,
256-
{ correlationId: req.id },
257-
);
258-
// send email invite (async)
259-
if (v.email && !v.userId && v.status === INVITE_STATUS.PENDING) {
260-
sendInviteEmail(req, projectId, v);
261-
}
262-
});
263-
return values;
264-
}); // models.sequelize.Promise.all
265-
}); // buildCreateInvitePromises
237+
req.log.debug('Creating invites');
238+
return models.sequelize.Promise.all(buildCreateInvitePromises(req, updatedInvite, invites, data, failed))
239+
.then((values) => {
240+
values.forEach((v) => {
241+
req.app.emit(EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_CREATED, {
242+
req,
243+
userId: v.userId,
244+
email: v.email,
245+
status: v.status,
246+
role: v.role,
247+
});
248+
req.app.services.pubsub.publish(
249+
EVENT.ROUTING_KEY.PROJECT_MEMBER_INVITE_CREATED,
250+
v,
251+
{ correlationId: req.id },
252+
);
253+
// send email invite (async)
254+
if (v.email && !v.userId && v.status === INVITE_STATUS.PENDING) {
255+
sendInviteEmail(req, projectId, v);
256+
}
257+
});
258+
return values;
259+
}); // models.sequelize.Promise.all
266260
}); // models.ProjectMemberInvite.getPendingInvitesForProject
267261
})
268-
.then(values => res.status(201).json(util.wrapResponse(req.id, values, null, 201)))
262+
.then((values) => {
263+
const success = _.assign({}, { success: values });
264+
if (failed.length) {
265+
res.status(403).json(util.wrapResponse(req.id, _.assign({}, success, { failed }), null, 403));
266+
} else {
267+
res.status(201).json(util.wrapResponse(req.id, success, null, 201));
268+
}
269+
})
269270
.catch(err => next(err));
270271
},
271272
];

0 commit comments

Comments
 (0)