From ef79f763ad5a9a75bc195c1f7507fa970418f150 Mon Sep 17 00:00:00 2001 From: imcaizheng Date: Sat, 5 Dec 2020 03:34:42 +0800 Subject: [PATCH 1/2] GET /taas-teams supports `perPage`, `page`, `sortBy`, `sortOrder`, and `name` params --- app.js | 5 +- ...coder-bookings-api.postman_collection.json | 65 ++++++++++++------ docs/swagger.yaml | 67 +++++++++++++++++++ src/bootstrap.js | 2 + src/common/helper.js | 20 +++--- src/controllers/TeamController.js | 5 +- src/services/TeamService.js | 40 +++++++++-- 7 files changed, 166 insertions(+), 38 deletions(-) diff --git a/app.js b/app.js index a0a9e0e1..f60a0565 100644 --- a/app.js +++ b/app.js @@ -14,7 +14,10 @@ const logger = require('./src/common/logger') // setup express app const app = express() -app.use(cors()) +app.use(cors({ + // Allow browsers access pagination data in headers + exposedHeaders: ['X-Page', 'X-Per-Page', 'X-Total', 'X-Total-Pages', 'X-Prev-Page', 'X-Next-Page'] +})) app.use(express.json()) app.use(express.urlencoded({ extended: true })) app.set('port', config.PORT) diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index 39156ec7..46f07ebf 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "418b385c-5ce1-4810-b357-cddf05adf271", + "_postman_id": "282e8342-2d5a-4566-8509-f2f240a594a0", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -14,7 +14,7 @@ { "listen": "test", "script": { - "id": "ff119e52-58f4-4501-8193-dd8c75f9ba4a", + "id": "a2236151-22f2-46c7-869a-858eb7da1ab8", "exec": [ "var data = JSON.parse(responseBody);\r", "postman.setEnvironmentVariable(\"jobId\",data.id);" @@ -59,7 +59,7 @@ { "listen": "test", "script": { - "id": "66169f57-ba0e-4a88-85c3-e27ee380c332", + "id": "91236482-d7b2-4acd-a47d-e9f9ac833bad", "exec": [ "var data = JSON.parse(responseBody);\r", "postman.setEnvironmentVariable(\"jobId\",data.id);" @@ -104,7 +104,7 @@ { "listen": "test", "script": { - "id": "31016a7d-5de6-4802-92f8-b1c7a9afcc4a", + "id": "1e5b5bdf-6ff0-4d96-8018-8698bc359674", "exec": [ "var data = JSON.parse(responseBody);", "postman.setEnvironmentVariable(\"jobIdCreatedByMember\",data.id);" @@ -149,7 +149,7 @@ { "listen": "test", "script": { - "id": "0b96c49d-6fb7-49ab-bfe1-4623ffe1be10", + "id": "0b469a8d-8119-4907-ac37-a69a1766754d", "exec": [ "" ], @@ -193,7 +193,7 @@ { "listen": "test", "script": { - "id": "bce80082-b100-466d-a196-73e9c38e2b9d", + "id": "fec863ef-1db6-4008-8a9d-ef616e3cac4d", "exec": [ "" ], @@ -785,7 +785,7 @@ { "listen": "test", "script": { - "id": "40f37d6f-508f-47ba-bbd4-41e1e0c3a4a2", + "id": "28720714-8006-4fd5-b3b8-0c588266bb5e", "exec": [ "pm.test(\"Status code is 403\", function () {", " pm.response.to.have.status(403);", @@ -997,7 +997,7 @@ { "listen": "test", "script": { - "id": "5d25999b-14ec-432c-bf11-028ec73a7140", + "id": "aa7fa1f8-cca2-4323-8266-ff4e770dae10", "exec": [ "pm.test(\"Status code is 403\", function () {", " pm.response.to.have.status(403);", @@ -1209,7 +1209,7 @@ { "listen": "test", "script": { - "id": "5f89405c-6907-41c8-8630-ccb5d5d640e6", + "id": "8299d83c-e5c3-4639-8c4a-bf783f178fb2", "exec": [ "pm.test(\"Status code is 403\", function () {", " pm.response.to.have.status(403);", @@ -1328,7 +1328,7 @@ { "listen": "test", "script": { - "id": "865f0f13-444b-4247-902d-b3e6cdb1898e", + "id": "cbee0ff6-5824-447c-915d-466bfd5242bc", "exec": [ "var data = JSON.parse(responseBody);\r", "postman.setEnvironmentVariable(\"jobCandidateId\",data.id);" @@ -1373,7 +1373,7 @@ { "listen": "test", "script": { - "id": "37973c93-a13f-448d-8f02-af7f18818df2", + "id": "6a1b4404-55ef-40e8-981a-3cb9e960d6ea", "exec": [ "var data = JSON.parse(responseBody);\r", "postman.setEnvironmentVariable(\"jobCandidateId\",data.id);" @@ -1418,7 +1418,7 @@ { "listen": "test", "script": { - "id": "79490454-9392-42f1-8f07-7ab1950318df", + "id": "a42f6045-fdb2-4d61-8dd4-a919573f5d24", "exec": [ "var data = JSON.parse(responseBody);\r", "postman.setEnvironmentVariable(\"jobCandidateId\",data.id);" @@ -1463,7 +1463,7 @@ { "listen": "test", "script": { - "id": "99a176e1-645b-47ca-91b4-940fbebc65f6", + "id": "b133db18-fa52-4a59-9e35-5e7d340b7f1d", "exec": [ "var data = JSON.parse(responseBody);\r", "postman.setEnvironmentVariable(\"jobCandidateId\",data.id);" @@ -1508,7 +1508,7 @@ { "listen": "test", "script": { - "id": "92b01e89-a317-4663-95f0-ecb861fceefb", + "id": "6cd47ee8-4139-4309-8cb1-ceb74d2d06eb", "exec": [ "" ], @@ -2325,7 +2325,7 @@ { "listen": "test", "script": { - "id": "d8fcb38d-6699-4710-9db2-13fb3c814dff", + "id": "2722fecc-08fa-4d6e-9895-e7ed844f21c9", "exec": [ "var data = JSON.parse(responseBody);\r", "postman.setEnvironmentVariable(\"resourceBookingId\",data.id);" @@ -2370,7 +2370,7 @@ { "listen": "test", "script": { - "id": "45daab6c-adbd-4083-84e9-0e64640e6d95", + "id": "86edd7ff-eef6-4976-ae59-101fbe154e2c", "exec": [ "var data = JSON.parse(responseBody);\r", "postman.setEnvironmentVariable(\"resourceBookingId\",data.id);" @@ -2415,7 +2415,7 @@ { "listen": "test", "script": { - "id": "daa443f9-6ba1-48b0-9c59-b7e7e4192244", + "id": "10be7afd-f528-4abf-a524-a9642678c436", "exec": [ "" ], @@ -2459,7 +2459,7 @@ { "listen": "test", "script": { - "id": "693dffa5-7de5-4940-8e1b-3053cb6a5684", + "id": "ad50f131-aff8-4e32-acda-d647889d4dc3", "exec": [ "" ], @@ -2503,7 +2503,7 @@ { "listen": "test", "script": { - "id": "dba6c30b-b9c8-4750-8f97-e98bb21aa436", + "id": "ebe085f5-b1ce-455b-b147-c4c6f4cd4283", "exec": [ "" ], @@ -3365,12 +3365,37 @@ } ], "url": { - "raw": "{{URL}}/taas-teams", + "raw": "{{URL}}/taas-teams?perPage=10&page=1&name=*taas*&sortBy=lastActivityAt&sortOrder=desc", "host": [ "{{URL}}" ], "path": [ "taas-teams" + ], + "query": [ + { + "key": "perPage", + "value": "10" + }, + { + "key": "page", + "value": "1" + }, + { + "key": "name", + "value": "*taas*", + "description": "case-insensitive; support wildcard match" + }, + { + "key": "sortBy", + "value": "lastActivityAt", + "description": "allows: createdAt, updatedAt, lastActivityAt, id, status, name, type, best match" + }, + { + "key": "sortOrder", + "value": "desc", + "description": "allows: asc, desc" + } ] } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 0ce3b6cf..380c4c14 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1278,6 +1278,44 @@ paths: - Teams description: | Search my teams. Teams is project in topcoder with type=='talent-as-a-service' + parameters: + - in: query + name: page + required: false + schema: + type: integer + default: 1 + description: The page number. + - in: query + name: perPage + required: false + schema: + type: integer + default: 20 + description: The number of items to list per page. + - in: query + name: sortBy + required: false + schema: + type: string + default: createdAt + enum: ['createdAt', 'updatedAt', 'lastActivityAt', 'id', 'status', 'name', 'type', 'best match'] + description: The sort by column. + - in: query + name: sortOrder + required: false + schema: + type: string + default: desc + enum: ['desc','asc'] + description: The sort order. Not allowed when sortBy is `best match`. + - in: query + name: name + required: false + schema: + type: string + description: filter by name, case-insensitive; support wildcard match. + example: '*taas*' security: - bearerAuth: [] responses: @@ -1289,6 +1327,35 @@ paths: type: array items: $ref: '#/components/schemas/Team' + headers: + X-Next-Page: + schema: + type: integer + description: The index of the next page + X-Page: + schema: + type: integer + description: The index of the current page (starting at 1) + X-Per-Page: + schema: + type: integer + description: The number of items to list per page + X-Prev-Page: + schema: + type: integer + description: The index of the previous page + X-Total: + schema: + type: integer + description: The total number of items + X-Total-Pages: + schema: + type: integer + description: The total number of pages + Link: + schema: + type: string + description: Pagination link header. '400': description: Bad request content: diff --git a/src/bootstrap.js b/src/bootstrap.js index 7a191878..a35212e0 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -3,6 +3,8 @@ const Joi = require('joi') const path = require('path') const logger = require('./common/logger') +Joi.page = () => Joi.number().integer().min(1).default(1) +Joi.perPage = () => Joi.number().integer().min(1).default(20) Joi.rateType = () => Joi.string().valid('hourly', 'daily', 'weekly', 'monthly') Joi.jobStatus = () => Joi.string().valid('sourcing', 'in-review', 'assigned', 'closed', 'cancelled') Joi.workload = () => Joi.string().valid('full-time', 'fractional') diff --git a/src/common/helper.js b/src/common/helper.js index ca714eef..441d7f25 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -124,14 +124,6 @@ function setResHeaders (req, res, result) { } res.set('Link', link) } - - // Allow browsers access pagination data in headers - let accessControlExposeHeaders = res.get('Access-Control-Expose-Headers') || '' - accessControlExposeHeaders += accessControlExposeHeaders ? ', ' : '' - // append new values, to not override values set by someone else - accessControlExposeHeaders += 'X-Page, X-Per-Page, X-Total, X-Total-Pages, X-Prev-Page, X-Next-Page' - - res.set('Access-Control-Expose-Headers', accessControlExposeHeaders) } /** @@ -305,19 +297,27 @@ function isDocumentMissingException (err) { /** * Function to get projects * @param {String} token the user request token + * @param {Object} criteria the search criteria * @returns the request result */ -async function getProjects (token) { +async function getProjects (token, criteria = {}) { const url = `${config.TC_API}/projects?type=talent-as-a-service` const res = await request .get(url) + .query(criteria) .set('Authorization', token) .set('Content-Type', 'application/json') .set('Accept', 'application/json') localLogger.debug({ context: 'getProjects', message: `response body: ${JSON.stringify(res.body)}` }) - return _.map(res.body, item => { + const result = _.map(res.body, item => { return _.pick(item, ['id', 'name']) }) + return { + total: Number(_.get(res.headers, 'x-total')), + page: Number(_.get(res.headers, 'x-page')), + perPage: Number(_.get(res.headers, 'x-per-page')), + result + } } /** diff --git a/src/controllers/TeamController.js b/src/controllers/TeamController.js index f29e11e1..b7dfe5d8 100644 --- a/src/controllers/TeamController.js +++ b/src/controllers/TeamController.js @@ -2,6 +2,7 @@ * Controller for TaaS teams endpoints */ const service = require('../services/TeamService') +const helper = require('../common/helper') /** * Search teams @@ -9,7 +10,9 @@ const service = require('../services/TeamService') * @param res the response */ async function searchTeams (req, res) { - res.send(await service.searchTeams(req.authUser)) + const result = await service.searchTeams(req.authUser, req.query) + helper.setResHeaders(req, res, result) + res.send({ result: result.result }) } /** diff --git a/src/services/TeamService.js b/src/services/TeamService.js index 09a5411a..3d08b956 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -37,17 +37,45 @@ async function _getJobsByProjectIds (projectIds) { /** * List teams * @param {Object} currentUser the user who perform this operation + * @param {Object} criteria the search criteria * @returns {Object} the search result, contain total/page/perPage and result array */ -async function searchTeams (currentUser) { - // Get projects from /v5/projects - const projects = await helper.getProjects(currentUser.jwtToken) - - return await getTeamDetail(currentUser, projects) +async function searchTeams (currentUser, criteria) { + let sort = `${criteria.sortBy}` + if (criteria.sortOrder) { + sort = `${criteria.sortBy} ${criteria.sortOrder}` + } + // Get projects from /v5/projects with searching criteria + const { total, page, perPage, result: projects } = await helper.getProjects( + currentUser.jwtToken, + { + page: criteria.page, + perPage: criteria.perPage, + name: criteria.name, + sort + } + ) + return { + total, + page, + perPage, + result: await getTeamDetail(currentUser, projects) + } } searchTeams.schema = Joi.object().keys({ - currentUser: Joi.object().required() + currentUser: Joi.object().required(), + criteria: Joi.object().keys({ + page: Joi.page(), + perPage: Joi.perPage(), + sortBy: Joi.string().valid('createdAt', 'updatedAt', 'lastActivityAt', 'id', 'status', 'name', 'type', 'best match').default('createdAt'), + sortOrder: Joi.when('sortBy', { + is: 'best match', + then: Joi.forbidden().label('sortOrder(with sortBy being `best match`)'), + otherwise: Joi.string().valid('asc', 'desc').default('desc') + }), + name: Joi.string() + }).required() }).required() /** From bf56071790fb4d7e29281731e582978e96067787 Mon Sep 17 00:00:00 2001 From: imcaizheng Date: Sat, 5 Dec 2020 03:58:59 +0800 Subject: [PATCH 2/2] remove unnecessary code --- src/services/TeamService.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/services/TeamService.js b/src/services/TeamService.js index 3d08b956..e6f75278 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -41,10 +41,7 @@ async function _getJobsByProjectIds (projectIds) { * @returns {Object} the search result, contain total/page/perPage and result array */ async function searchTeams (currentUser, criteria) { - let sort = `${criteria.sortBy}` - if (criteria.sortOrder) { - sort = `${criteria.sortBy} ${criteria.sortOrder}` - } + const sort = `${criteria.sortBy} ${criteria.sortOrder}` // Get projects from /v5/projects with searching criteria const { total, page, perPage, result: projects } = await helper.getProjects( currentUser.jwtToken,