Skip to content

Commit a1a2a52

Browse files
authored
Merge pull request #150 from imcaizheng/new-endpoint-add-members
add new endpoint POST /taas-teams/:id/members
2 parents 8ec5de1 + 5fcd8b4 commit a1a2a52

File tree

10 files changed

+362
-7
lines changed

10 files changed

+362
-7
lines changed

app.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ app.use((err, req, res, next) => {
7575
}
7676

7777
if (err.response) {
78-
// extract error message from V3 API
79-
errorResponse.message = _.get(err, 'response.body.result.content')
78+
// extract error message from V3/V5 API
79+
errorResponse.message = _.get(err, 'response.body.result.content') || _.get(err, 'response.body.message')
8080
}
8181

8282
if (_.isUndefined(errorResponse.message)) {

config/default.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ module.exports = {
3939
TOPCODER_SKILL_PROVIDER_ID: process.env.TOPCODER_SKILL_PROVIDER_ID || '9cc0795a-6e12-4c84-9744-15858dba1861',
4040

4141
TOPCODER_USERS_API: process.env.TOPCODER_USERS_API || 'https://api.topcoder-dev.com/v3/users',
42+
// the api to find topcoder members
43+
TOPCODER_MEMBERS_API: process.env.TOPCODER_MEMBERS_API || 'https://api.topcoder-dev.com/v3/members',
44+
// rate limit of requests to user api
45+
MAX_PARALLEL_REQUEST_TOPCODER_USERS_API: process.env.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API || 100,
4246

4347
// PostgreSQL database url.
4448
DATABASE_URL: process.env.DATABASE_URL || 'postgres://postgres:postgres@localhost:5432/postgres',

docs/Topcoder-bookings-api.postman_collection.json

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4482,7 +4482,52 @@
44824482
}
44834483
},
44844484
"response": []
4485-
}
4485+
},
4486+
{
4487+
"name": "POST /taas-teams/:id/members",
4488+
"request": {
4489+
"method": "POST",
4490+
"header": [
4491+
{
4492+
"key": "Authorization",
4493+
"type": "text",
4494+
"value": "Bearer {{token_administrator}}"
4495+
},
4496+
{
4497+
"key": "Content-Type",
4498+
"type": "text",
4499+
"value": "application/json"
4500+
}
4501+
],
4502+
"body": {
4503+
"mode": "raw",
4504+
"raw": "{\n \"handles\": [\n \"tester1234\",\n \"non-existing\"\n ],\n \"emails\": [\n \"non-existing@domain.com\",\n \"email@domain.com\"\n ]\n}",
4505+
"options": {
4506+
"raw": {
4507+
"language": "json"
4508+
}
4509+
}
4510+
},
4511+
"url": {
4512+
"raw": "{{URL}}/taas-teams/:id/members",
4513+
"host": [
4514+
"{{URL}}"
4515+
],
4516+
"path": [
4517+
"taas-teams",
4518+
":id",
4519+
"members"
4520+
],
4521+
"variable": [
4522+
{
4523+
"key": "id",
4524+
"value": "16705"
4525+
}
4526+
]
4527+
}
4528+
},
4529+
"response": []
4530+
}
44864531
]
44874532
},
44884533
{

docs/swagger.yaml

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1523,6 +1523,63 @@ paths:
15231523
application/json:
15241524
schema:
15251525
$ref: '#/components/schemas/Error'
1526+
/taas-teams/{id}/members:
1527+
post:
1528+
tags:
1529+
- Teams
1530+
description: |
1531+
Add members to a team by handle or email.
1532+
security:
1533+
- bearerAuth: []
1534+
parameters:
1535+
- in: path
1536+
name: id
1537+
required: true
1538+
schema:
1539+
type: integer
1540+
description: The team/project id.
1541+
requestBody:
1542+
content:
1543+
application/json:
1544+
schema:
1545+
$ref: '#/components/schemas/AddMembersRequestBody'
1546+
responses:
1547+
'200':
1548+
description: OK
1549+
content:
1550+
application/json:
1551+
schema:
1552+
$ref: '#/components/schemas/AddMembersResponseBody'
1553+
'400':
1554+
description: Bad request
1555+
content:
1556+
application/json:
1557+
schema:
1558+
$ref: '#/components/schemas/Error'
1559+
'401':
1560+
description: Not authenticated
1561+
content:
1562+
application/json:
1563+
schema:
1564+
$ref: '#/components/schemas/Error'
1565+
'403':
1566+
description: Not authorized
1567+
content:
1568+
application/json:
1569+
schema:
1570+
$ref: '#/components/schemas/Error'
1571+
'404':
1572+
description: Not Found
1573+
content:
1574+
application/json:
1575+
schema:
1576+
$ref: '#/components/schemas/Error'
1577+
'500':
1578+
description: Internal Server Error
1579+
content:
1580+
application/json:
1581+
schema:
1582+
$ref: '#/components/schemas/Error'
15261583
/taas-teams/skills:
15271584
get:
15281585
tags:
@@ -2419,6 +2476,55 @@ components:
24192476
type: object
24202477
example: {"projectName": "TaaS Project Name", "projectId": 12345, "reportText": "I have issue with ..."}
24212478
description: "Arbitrary data to feed the specified template"
2479+
AddMembersRequestBody:
2480+
properties:
2481+
handles:
2482+
type: array
2483+
description: "The handles."
2484+
items:
2485+
type: string
2486+
description: "the handle of a member"
2487+
example: topcoder321
2488+
emails:
2489+
type: array
2490+
description: "The emails."
2491+
items:
2492+
type: string
2493+
description: "the email of a member"
2494+
example: 'xxx@xxx.com'
2495+
AddMembersResponseBody:
2496+
properties:
2497+
success:
2498+
type: array
2499+
description: "The handles."
2500+
items:
2501+
type: object
2502+
example: {"createdAt": "2021-02-18T19:58:50.610Z", "createdBy": -101, "email": "email@domain.com", "handle": "Scud", "id": 14155, "photoURL": "https://topcoder-dev-media.s3.amazonaws.com/member/profile/Scud-1450982908556.png", "role": "customer", "timeZone": null, "updatedAt": "2021-02-18T19:58:50.611Z", "updatedBy": -101, "userId": 1800091, "workingHourEnd": null, "workingHourStart": null}
2503+
failed:
2504+
type: array
2505+
description: "The emails."
2506+
items:
2507+
oneOf:
2508+
- type: object
2509+
properties:
2510+
error:
2511+
type: string
2512+
description: the error message
2513+
example: "User doesn't exist"
2514+
handle:
2515+
type: string
2516+
description: "the handle of a member"
2517+
example: topcoder321
2518+
- type: object
2519+
properties:
2520+
error:
2521+
type: string
2522+
description: the error message
2523+
example: "User is already added"
2524+
email:
2525+
type: string
2526+
description: "the email of a member"
2527+
example: 'xxx@xxx.com'
24222528
Error:
24232529
required:
24242530
- message

package-lock.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"@elastic/elasticsearch": "^7.9.1",
3535
"@topcoder-platform/topcoder-bus-api-wrapper": "github:topcoder-platform/tc-bus-api-wrapper",
3636
"aws-sdk": "^2.787.0",
37+
"bottleneck": "^2.19.5",
3738
"config": "^3.3.2",
3839
"cors": "^2.8.5",
3940
"date-fns": "^2.16.1",

src/common/helper.js

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
const fs = require('fs')
66
const querystring = require('querystring')
77
const Confirm = require('prompt-confirm')
8+
const Bottleneck = require('bottleneck')
89
const AWS = require('aws-sdk')
910
const config = require('config')
1011
const HttpStatus = require('http-status-codes')
@@ -968,6 +969,89 @@ async function checkIsMemberOfProject (userId, projectId) {
968969
}
969970
}
970971

972+
/**
973+
* Find topcoder members by handles.
974+
*
975+
* @param {Array} handles the array of handles
976+
* @returns {Array} the member details
977+
*/
978+
async function getMemberDetailsByHandles (handles) {
979+
if (!handles.length) {
980+
return []
981+
}
982+
const token = await getM2MToken()
983+
const res = await request
984+
.get(`${config.TOPCODER_MEMBERS_API}/_search`)
985+
.query({
986+
query: _.map(handles, handle => `handleLower:${handle.toLowerCase()}`).join(' OR '),
987+
fields: 'userId,handle,firstName,lastName,email'
988+
})
989+
.set('Authorization', `Bearer ${token}`)
990+
.set('Accept', 'application/json')
991+
localLogger.debug({ context: 'getMemberDetailsByHandles', message: `response body: ${JSON.stringify(res.body)}` })
992+
return _.get(res.body, 'result.content')
993+
}
994+
995+
/**
996+
* Find topcoder members by email.
997+
*
998+
* @param {String} token the auth token
999+
* @param {String} email the email
1000+
* @returns {Array} the member details
1001+
*/
1002+
async function _getMemberDetailsByEmail (token, email) {
1003+
const res = await request
1004+
.get(config.TOPCODER_USERS_API)
1005+
.query({
1006+
filter: `email=${email}`,
1007+
fields: 'handle,id,email'
1008+
})
1009+
.set('Authorization', `Bearer ${token}`)
1010+
.set('Accept', 'application/json')
1011+
localLogger.debug({ context: '_getMemberDetailsByEmail', message: `response body: ${JSON.stringify(res.body)}` })
1012+
return _.get(res.body, 'result.content')
1013+
}
1014+
1015+
/**
1016+
* Find topcoder members by emails.
1017+
* Maximum concurrent requests is limited by MAX_PARALLEL_REQUEST_TOPCODER_USERS_API.
1018+
*
1019+
* @param {Array} emails the array of emails
1020+
* @returns {Array} the member details
1021+
*/
1022+
async function getMemberDetailsByEmails (emails) {
1023+
const token = await getM2MToken()
1024+
const limiter = new Bottleneck({ maxConcurrent: config.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API })
1025+
const membersArray = await Promise.all(emails.map(email => limiter.schedule(() => _getMemberDetailsByEmail(token, email)
1026+
.catch(() => {
1027+
localLogger.error({ context: 'getMemberDetailsByEmails', message: `email: ${email} user not found` })
1028+
return []
1029+
})
1030+
)))
1031+
return _.flatten(membersArray)
1032+
}
1033+
1034+
/**
1035+
* Add a member to a project.
1036+
*
1037+
* @param {Number} projectId project id
1038+
* @param {Object} data the userId and the role of the member
1039+
* @param {Object} criteria the filtering criteria
1040+
* @returns {Object} the member created
1041+
*/
1042+
async function createProjectMember (projectId, data, criteria) {
1043+
const m2mToken = await getM2MToken()
1044+
const { body: member } = await request
1045+
.post(`${config.TC_API}/projects/${projectId}/members`)
1046+
.set('Authorization', `Bearer ${m2mToken}`)
1047+
.set('Content-Type', 'application/json')
1048+
.set('Accept', 'application/json')
1049+
.query(criteria)
1050+
.send(data)
1051+
localLogger.debug({ context: 'createProjectMember', message: `response body: ${JSON.stringify(member)}` })
1052+
return member
1053+
}
1054+
9711055
module.exports = {
9721056
getParamFromCliArgs,
9731057
promptUser,
@@ -1002,5 +1086,8 @@ module.exports = {
10021086
ensureJobById,
10031087
ensureUserById,
10041088
getAuditM2Muser,
1005-
checkIsMemberOfProject
1089+
checkIsMemberOfProject,
1090+
getMemberDetailsByHandles,
1091+
getMemberDetailsByEmails,
1092+
createProjectMember
10061093
}

src/controllers/TeamController.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,19 @@ async function sendEmail (req, res) {
4444
res.status(HttpStatus.NO_CONTENT).end()
4545
}
4646

47+
/**
48+
* Add members to a team.
49+
* @param req the request
50+
* @param res the response
51+
*/
52+
async function addMembers (req, res) {
53+
res.send(await service.addMembers(req.authUser, req.params.id, req.body))
54+
}
55+
4756
module.exports = {
4857
searchTeams,
4958
getTeam,
5059
getTeamJob,
51-
sendEmail
60+
sendEmail,
61+
addMembers
5262
}

src/routes/TeamRoutes.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,13 @@ module.exports = {
4343
auth: 'jwt',
4444
scopes: [constants.Scopes.READ_TAAS_TEAM]
4545
}
46+
},
47+
'/taas-teams/:id/members': {
48+
post: {
49+
controller: 'TeamController',
50+
method: 'addMembers',
51+
auth: 'jwt',
52+
scopes: [constants.Scopes.READ_TAAS_TEAM]
53+
}
4654
}
4755
}

0 commit comments

Comments
 (0)