From f8efc417e4f1422ac49c7dc08a23e9adeec6c491 Mon Sep 17 00:00:00 2001 From: Muhamad Fikri Alhawarizmi Date: Fri, 5 Jul 2019 08:45:13 +0700 Subject: [PATCH 1/4] challenge/30095006 - Multiple invitations by email --- src/routes/projectMemberInvites/create.js | 2 +- .../projectMemberInvites/create.spec.js | 8 +-- src/util.js | 62 +++++++++++++++++++ 3 files changed, 67 insertions(+), 5 deletions(-) diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 9d19f410..5187b9b9 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -80,7 +80,7 @@ const buildCreateInvitePromises = (req, invite, invites, data, failed) => { if (invite.emails) { // if for some emails there are already existent users, we will invite them by userId, // to avoid sending them registration email - return util.lookupUserEmails(req, invite.emails) + return util.lookupMultipleUserEmails(req, invite.emails, 5) .then((existentUsers) => { // existent user we will invite by userId and email const existentUsersWithNumberId = existentUsers.map((user) => { diff --git a/src/routes/projectMemberInvites/create.spec.js b/src/routes/projectMemberInvites/create.spec.js index 90ff16a7..7b958266 100644 --- a/src/routes/projectMemberInvites/create.spec.js +++ b/src/routes/projectMemberInvites/create.spec.js @@ -147,9 +147,9 @@ describe('Project Member Invite create', () => { server.services.pubsub.publish.restore(); sinon.stub(server.services.pubsub, 'init', () => {}); sinon.stub(server.services.pubsub, 'publish', () => {}); - // by default mock lookupUserEmails return nothing so all the cases are not broken + // by default mock lookupMultipleUserEmails return nothing so all the cases are not broken sandbox.stub(util, 'getUserRoles', () => Promise.resolve([])); - sandbox.stub(util, 'lookupUserEmails', () => Promise.resolve([])); + sandbox.stub(util, 'lookupMultipleUserEmails', () => Promise.resolve([])); sandbox.stub(util, 'getMemberDetailsByUserIds', () => Promise.resolve([{ userId: 40051333, firstName: 'Admin', @@ -366,8 +366,8 @@ describe('Project Member Invite create', () => { }), }); sandbox.stub(util, 'getHttpClient', () => mockHttpClient); - util.lookupUserEmails.restore(); - sandbox.stub(util, 'lookupUserEmails', () => Promise.resolve([{ + util.lookupMultipleUserEmails.restore(); + sandbox.stub(util, 'lookupMultipleUserEmails', () => Promise.resolve([{ id: '12345', email: 'hello@world.com', }])); diff --git a/src/util.js b/src/util.js index 46cd75b2..2f3d1109 100644 --- a/src/util.js +++ b/src/util.js @@ -463,6 +463,68 @@ _.assignIn(util, { }); }, + /** + * Lookup user handles from multiple emails + * @param {Object} req request + * @param {Array} userEmails user emails + * @param {Number} maximumRequests limit number of request on one batch + * @param {Boolean} isPattern flag to indicate that pattern matching is required or not + * @return {Promise} promise + */ + lookupMultipleUserEmails(req, userEmails, maximumRequests, isPattern = false) { + req.log.debug(`identityServiceEndpoint: ${config.get('identityServiceEndpoint')}`); + + const httpClient = util.getHttpClient({ id: req.id, log: req.log }); + // request generator function + const generateRequest = ({ token, email }) => { + let filter = `email=${email}`; + if (isPattern) { + filter += '&like=true'; + } + return httpClient.get(`${config.get('identityServiceEndpoint')}users`, { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + params: { + fields: 'handle,id,email', + filter, + }, + // set longer timeout as default 3000 could be not enough for identity service response + timeout: 15000, + }); + }; + // send batch of requests, one batch at one time + const sendBatch = (options) => { + const token = options.token; + const emails = options.emails; + const users = options.users || []; + const batch = options.batch || 0; + const start = batch * maximumRequests; + const end = (batch + 1) * maximumRequests; + const requests = emails.slice(start, end).map(userEmail => + generateRequest({ token, email: userEmail })); + return Promise.all(requests) + .then((responses) => { + const data = responses.reduce((contents, response) => { + const content = _.get(response, 'data.result.content', []); + return _.concat(contents, content); + }, users); + req.log.debug(`UserHandle response batch-${batch}`, data); + if (end < emails.length) { + return sendBatch({ token, users: data, emails, batch: batch + 1 }); + } + return data; + }); + }; + return util.getM2MToken() + .then((m2mToken) => { + req.log.debug(`Bearer ${m2mToken}`); + return sendBatch({ token: m2mToken, emails: userEmails }); + }); + }, + /** * Filter only members of topcoder team * @param {Array} members project members From c95b0866216c693a0115bd2adf7647bc983ac8fe Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 9 Jul 2019 13:31:14 +0800 Subject: [PATCH 2/4] treat error in identity service as user is not found by email #334 --- src/util.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/util.js b/src/util.js index 2f3d1109..03c4a500 100644 --- a/src/util.js +++ b/src/util.js @@ -493,6 +493,10 @@ _.assignIn(util, { }, // set longer timeout as default 3000 could be not enough for identity service response timeout: 15000, + }).catch(() => { + // in case of any error happens during getting user by email + // we treat such users as not found and don't return error + // as per discussion in issue #334 }); }; // send batch of requests, one batch at one time From f369924a7e6d6caefa8906c7a1c0e4abddb7fc55 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 9 Jul 2019 13:35:34 +0800 Subject: [PATCH 3/4] added constants for maximum parallel requests quantity #334 --- src/constants.js | 2 ++ src/routes/projectMemberInvites/create.js | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/constants.js b/src/constants.js index f0ac0f21..3351fe2c 100644 --- a/src/constants.js +++ b/src/constants.js @@ -182,3 +182,5 @@ export const INVITE_STATUS = { REQUEST_APPROVED: 'request_approved', CANCELED: 'canceled', }; + +export const MAX_PARALLEL_REQUEST_QTY = 5; \ No newline at end of file diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js index 5187b9b9..7b76cecd 100644 --- a/src/routes/projectMemberInvites/create.js +++ b/src/routes/projectMemberInvites/create.js @@ -8,7 +8,7 @@ import { middleware as tcMiddleware } from 'tc-core-library-js'; import models from '../../models'; import util from '../../util'; import { PROJECT_MEMBER_ROLE, PROJECT_MEMBER_MANAGER_ROLES, - MANAGER_ROLES, INVITE_STATUS, EVENT, BUS_API_EVENT, USER_ROLE } from '../../constants'; + MANAGER_ROLES, INVITE_STATUS, EVENT, BUS_API_EVENT, USER_ROLE, MAX_PARALLEL_REQUEST_QTY } from '../../constants'; import { createEvent } from '../../services/busApi'; @@ -80,7 +80,7 @@ const buildCreateInvitePromises = (req, invite, invites, data, failed) => { if (invite.emails) { // if for some emails there are already existent users, we will invite them by userId, // to avoid sending them registration email - return util.lookupMultipleUserEmails(req, invite.emails, 5) + return util.lookupMultipleUserEmails(req, invite.emails, MAX_PARALLEL_REQUEST_QTY) .then((existentUsers) => { // existent user we will invite by userId and email const existentUsersWithNumberId = existentUsers.map((user) => { From 1e40ba0d833f836175f311c37b9aace64aa2b834 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 9 Jul 2019 13:38:15 +0800 Subject: [PATCH 4/4] fix lint --- src/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants.js b/src/constants.js index 3351fe2c..34b64999 100644 --- a/src/constants.js +++ b/src/constants.js @@ -183,4 +183,4 @@ export const INVITE_STATUS = { CANCELED: 'canceled', }; -export const MAX_PARALLEL_REQUEST_QTY = 5; \ No newline at end of file +export const MAX_PARALLEL_REQUEST_QTY = 5;