diff --git a/.circleci/config.yml b/.circleci/config.yml index 2b41d1d2..78e4ffa1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -68,6 +68,8 @@ workflows: branches: only: - dev + - feature/es-segregation + - feature/integration-test-fix # Production builds are exectuted only on tagged commits to the # master branch. diff --git a/.gitignore b/.gitignore index d5a06f7a..d4e0230b 100644 --- a/.gitignore +++ b/.gitignore @@ -113,4 +113,7 @@ dist .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz -.pnp.* \ No newline at end of file +.pnp.* + +# api.env +api.env \ No newline at end of file diff --git a/README.md b/README.md index ff0fcab9..88bca715 100644 --- a/README.md +++ b/README.md @@ -19,27 +19,49 @@ The following parameters can be set in config files or in env variables: - `BASE_PATH`: the server api base path - `AUTH_SECRET`: The authorization secret used during token verification. - `VALID_ISSUERS`: The valid issuer of tokens, a json array contains valid issuer. + - `AUTH0_URL`: Auth0 URL, used to get TC M2M token - `AUTH0_AUDIENCE`: Auth0 audience, used to get TC M2M token +- `AUTH0_AUDIENCE_FOR_BUS_API`: Auth0 audience, used to get TC M2M token to be used in bus api client - `TOKEN_CACHE_TIME`: Auth0 token cache time, used to get TC M2M token - `AUTH0_CLIENT_ID`: Auth0 client id, used to get TC M2M token - `AUTH0_CLIENT_SECRET`: Auth0 client secret, used to get TC M2M token - `AUTH0_PROXY_SERVER_URL`: Proxy Auth0 URL, used to get TC M2M token + - `DATABASE_URL`: PostgreSQL database url. - `DB_SCHEMA_NAME`: string - PostgreSQL database target schema - `PROJECT_API_URL`: the project service url - `TC_API`: the Topcoder v5 url - `ORG_ID`: the organization id -- `HOST`: the elasticsearch host -- `ES_INDEX_JOB`: the job index -- `ES_INDEX_JOB_CANDIDATE`: the job candidate index -- `ES_INDEX_RESOURCE_BOOKING`: the resource booking index + +- `esConfig.HOST`: the elasticsearch host +- `esConfig.ES_INDEX_JOB`: the job index +- `esConfig.ES_INDEX_JOB_CANDIDATE`: the job candidate index +- `esConfig.ES_INDEX_RESOURCE_BOOKING`: the resource booking index +- `esConfig.AWS_REGION`: The Amazon region to use when using AWS Elasticsearch service +- `esConfig.ELASTICCLOUD.id`: The elastic cloud id, if your elasticsearch instance is hosted on elastic cloud. DO NOT provide a value for ES_HOST if you are using this +- `esConfig.ELASTICCLOUD.username`: The elastic cloud username for basic authentication. Provide this only if your elasticsearch instance is hosted on elastic cloud +- `esConfig.ELASTICCLOUD.password`: The elastic cloud password for basic authentication. Provide this only if your elasticsearch instance is hosted on elastic cloud + +- `BUSAPI_URL`: Topcoder Bus API URL +- `KAFKA_ERROR_TOPIC`: The error topic at which bus api will publish any errors +- `KAFKA_MESSAGE_ORIGINATOR`: The originator value for the kafka messages + +- `TAAS_JOB_CREATE_TOPIC`: the create job entity Kafka message topic +- `TAAS_JOB_UPDATE_TOPIC`: the update job entity Kafka message topic +- `TAAS_JOB_DELETE_TOPIC`: the delete job entity Kafka message topic +- `TAAS_JOB_CANDIDATE_CREATE_TOPIC`: the create job candidate entity Kafka message topic +- `TAAS_JOB_CANDIDATE_UPDATE_TOPIC`: the update job candidate entity Kafka message topic +- `TAAS_JOB_CANDIDATE_DELETE_TOPIC`: the delete job candidate entity Kafka message topic +- `TAAS_RESOURCE_BOOKING_CREATE_TOPIC`: the create resource booking entity Kafka message topic +- `TAAS_RESOURCE_BOOKING_UPDATE_TOPIC`: the update resource booking entity Kafka message topic +- `TAAS_RESOURCE_BOOKING_DELETE_TOPIC`: the delete resource booking entity Kafka message topic ## PostgreSQL Database Setup - Go to https://www.postgresql.org/ download and install the PostgreSQL. - Modify `DATABASE_URL` under `config/default.js` to meet your environment. -- Run `npm run init-db` to create table +- Run `npm run init-db` to create table(run `npm run init-db force` to force creating table) ## ElasticSearch Setup - Go to https://www.elastic.co/downloads/ download and install the elasticsearch. @@ -52,17 +74,41 @@ The following parameters can be set in config files or in env variables: - Install dependencies `npm install` - Run lint `npm run lint` - Run lint fix `npm run lint:fix` -- Clear and init db `npm run init-db` -- Clear and create es index `npm run delete-index && npm run create-index` +- Clear and init db `npm run init-db force` +- Clear and create es index + + ``` bash + npm run delete-index # run this if you already created index + npm run create-index + ``` + - Start app `npm start` - App is running at `http://localhost:3000` -## Docker Deployment -- Run `docker-compose up` +## Local Deployment with Docker + +Make sure all config values are right, and you can run on local successful, then run below commands + +1. Navigate to the directory `docker` + +2. Rename the file `sample.api.env` to `api.env` + +3. Set the required AUTH0 configurations, PostgreSQL Database url and ElasticSearch host in the file `api.env` + + Note that you can also add other variables to `api.env`, with `=` format per line. + If using AWS ES you should add `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` variables as well. + +4. Once that is done, run the following command + + ```bash + docker-compose up + ``` + +5. When you are running the application for the first time, It will take some time initially to download the image and install the dependencies ## Testing - Run `npm run test` to execute unit tests - Run `npm run cov` to execute unit tests and generate coverage report. ## Verification -Refer to the verification document [Verification.md](Verification.md) \ No newline at end of file +Refer to the verification document [Verification.md](Verification.md) diff --git a/Verification.md b/Verification.md index 4aa42dc0..4867b6ea 100644 --- a/Verification.md +++ b/Verification.md @@ -1,35 +1,40 @@ # Topcoder Bookings API ## Postman test -- Refer `ReadMe.md` to start the app and postgreSQL database -- Run `npm run init-db` to init db before testing. -- Run `npm run create-index` to create es index before testing +- start PostgreSQL and ElasticSearch +- Refer `README.md#Local Deployment` to start the app - Import Postman collection and environment file in the `docs` folder to Postman and execute the scripts to validate the app from top to bottom. +## Note About Testing `/taas-teams` Endpoints +Before you run tests against the `taas-teams` endpoints, you should insert the dedicated test data by running `npm run test-data`. ## Unit test Coverage - 63 passing (43s) +``` bash + 96 passing (170ms) - -File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s -----------------------------|---------|----------|---------|---------|------------------- -All files | 99.49 | 97.62 | 100 | 99.74 | - config | 100 | 100 | 100 | 100 | - default.js | 100 | 100 | 100 | 100 | - test.js | 100 | 100 | 100 | 100 | - src | 90.48 | 50 | 100 | 94.12 | - bootstrap.js | 90.48 | 50 | 100 | 94.12 | 18 - src/common | 100 | 100 | 100 | 100 | - errors.js | 100 | 100 | 100 | 100 | - helper.js | 100 | 100 | 100 | 100 | - src/models | 100 | 92.86 | 100 | 100 | - Job.js | 100 | 100 | 100 | 100 | - JobCandidate.js | 100 | 100 | 100 | 100 | - ResourceBooking.js | 100 | 100 | 100 | 100 | - index.js | 100 | 80 | 100 | 100 | 29 - src/services | 100 | 100 | 100 | 100 | - JobCandidateService.js | 100 | 100 | 100 | 100 | - JobService.js | 100 | 100 | 100 | 100 | - ResourceBookingService.js | 100 | 100 | 100 | 100 | +----------------------------|---------|----------|---------|---------|---------------------------- +File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s +----------------------------|---------|----------|---------|---------|---------------------------- +All files | 98.43 | 91.03 | 100 | 98.56 | + config | 100 | 100 | 100 | 100 | + default.js | 100 | 100 | 100 | 100 | + test.js | 100 | 100 | 100 | 100 | + src | 90.91 | 50 | 100 | 94.44 | + bootstrap.js | 90.91 | 50 | 100 | 94.44 | 18 + src/common | 97.69 | 90.91 | 100 | 97.66 | + errors.js | 100 | 50 | 100 | 100 | 23 + helper.js | 97.5 | 92.86 | 100 | 97.46 | 94,176,284 + src/models | 100 | 92.86 | 100 | 100 | + Job.js | 100 | 100 | 100 | 100 | + JobCandidate.js | 100 | 100 | 100 | 100 | + ResourceBooking.js | 100 | 100 | 100 | 100 | + index.js | 100 | 80 | 100 | 100 | 29 + src/services | 98.81 | 89.5 | 100 | 98.8 | + JobCandidateService.js | 98.77 | 88 | 100 | 98.77 | 37 + JobService.js | 97.37 | 85.37 | 100 | 97.32 | 73,285,326 + ResourceBookingService.js | 98.86 | 93.1 | 100 | 98.86 | 54 + TeamService.js | 100 | 90.7 | 100 | 100 | 19,135-138,188-202,251,267 +----------------------------|---------|----------|---------|---------|---------------------------- +``` diff --git a/app.js b/app.js index 0c70b726..a0a9e0e1 100644 --- a/app.js +++ b/app.js @@ -8,8 +8,8 @@ const config = require('config') const express = require('express') const cors = require('cors') const HttpStatus = require('http-status-codes') -const logger = require('./src/common/logger') const interceptor = require('express-interceptor') +const logger = require('./src/common/logger') // setup express app const app = express() @@ -52,7 +52,7 @@ require('./app-routes')(app) // The error handler // eslint-disable-next-line no-unused-vars app.use((err, req, res, next) => { - logger.logFullError(err, req.signature || `${req.method} ${req.url}`) + logger.logFullError(err, { component: 'app', signature: req.signature || `${req.method}_${req.url}` }) const errorResponse = {} const status = err.isJoi ? HttpStatus.BAD_REQUEST : (err.status || err.httpStatus || HttpStatus.INTERNAL_SERVER_ERROR) @@ -87,7 +87,7 @@ app.use((err, req, res, next) => { }) const server = app.listen(app.get('port'), () => { - logger.info(`Express server listening on port ${app.get('port')}`) + logger.info({ component: 'app', message: `Express server listening on port ${app.get('port')}` }) }) if (process.env.NODE_ENV === 'test') { diff --git a/config/default.js b/config/default.js index be8346ef..333d5f55 100644 --- a/config/default.js +++ b/config/default.js @@ -8,6 +8,7 @@ module.exports = { VALID_ISSUERS: process.env.VALID_ISSUERS || '["https://api.topcoder-dev.com", "https://api.topcoder.com", "https://topcoder-dev.auth0.com/", "https://auth.topcoder-dev.com/"]', AUTH0_URL: process.env.AUTH0_URL, AUTH0_AUDIENCE: process.env.AUTH0_AUDIENCE, + AUTH0_AUDIENCE_FOR_BUS_API: process.env.AUTH0_AUDIENCE_FOR_BUS_API, TOKEN_CACHE_TIME: process.env.TOKEN_CACHE_TIME, AUTH0_CLIENT_ID: process.env.AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET: process.env.AUTH0_CLIENT_SECRET, @@ -22,8 +23,33 @@ module.exports = { esConfig: { HOST: process.env.ES_HOST || 'http://localhost:9200', + + ELASTICCLOUD: { + id: process.env.ELASTICCLOUD_ID, + username: process.env.ELASTICCLOUD_USERNAME, + password: process.env.ELASTICCLOUD_PASSWORD + }, + + AWS_REGION: process.env.AWS_REGION || 'us-east-1', // AWS Region to be used if we use AWS ES + ES_INDEX_JOB: process.env.ES_INDEX_JOB || 'job', ES_INDEX_JOB_CANDIDATE: process.env.ES_INDEX_JOB_CANDIDATE || 'job_candidate', ES_INDEX_RESOURCE_BOOKING: process.env.ES_INDEX_RESOURCE_BOOKING || 'resource_booking' - } + }, + + BUSAPI_URL: process.env.BUSAPI_URL || 'https://api.topcoder-dev.com/v5', + KAFKA_ERROR_TOPIC: process.env.KAFKA_ERROR_TOPIC || 'common.error.reporting', + KAFKA_MESSAGE_ORIGINATOR: process.env.KAFKA_MESSAGE_ORIGINATOR || 'taas-api', + // topics for job service + TAAS_JOB_CREATE_TOPIC: process.env.TAAS_JOB_CREATE_TOPIC || 'taas.job.create', + TAAS_JOB_UPDATE_TOPIC: process.env.TAAS_JOB_UPDATE_TOPIC || 'taas.job.update', + TAAS_JOB_DELETE_TOPIC: process.env.TAAS_JOB_DELETE_TOPIC || 'taas.job.delete', + // topics for jobcandidate service + TAAS_JOB_CANDIDATE_CREATE_TOPIC: process.env.TAAS_JOB_CANDIDATE_CREATE_TOPIC || 'taas.jobcandidate.create', + TAAS_JOB_CANDIDATE_UPDATE_TOPIC: process.env.TAAS_JOB_CANDIDATE_UPDATE_TOPIC || 'taas.jobcandidate.update', + TAAS_JOB_CANDIDATE_DELETE_TOPIC: process.env.TAAS_JOB_CANDIDATE_DELETE_TOPIC || 'taas.jobcandidate.delete', + // topics for job service + TAAS_RESOURCE_BOOKING_CREATE_TOPIC: process.env.TAAS_RESOURCE_BOOKING_CREATE_TOPIC || 'taas.resourcebooking.create', + TAAS_RESOURCE_BOOKING_UPDATE_TOPIC: process.env.TAAS_RESOURCE_BOOKING_UPDATE_TOPIC || 'taas.resourcebooking.update', + TAAS_RESOURCE_BOOKING_DELETE_TOPIC: process.env.TAAS_RESOURCE_BOOKING_DELETE_TOPIC || 'taas.resourcebooking.delete' } diff --git a/config/test.js b/config/test.js index a818158a..baa8e7f8 100644 --- a/config/test.js +++ b/config/test.js @@ -1,3 +1,8 @@ module.exports = { - LOG_LEVEL: process.env.LOG_LEVEL || 'info' + LOG_LEVEL: process.env.LOG_LEVEL || 'info', + AUTH0_URL: 'http://example.com', + AUTH0_AUDIENCE: 'http://example.com', + AUTH0_AUDIENCE_FOR_BUS_API: 'http://example.com', + AUTH0_CLIENT_ID: 'fake_id', + AUTH0_CLIENT_SECRET: 'fake_secret' } diff --git a/docker/Dockerfile b/docker/Dockerfile index a0c7e4e8..ff4253fb 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -14,4 +14,4 @@ RUN npm install COPY . . # Run the app -CMD [ "node", "app.js" ] \ No newline at end of file +CMD [ "npm", "start" ] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 00000000..55b6a95a --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,11 @@ +version: '3' +services: + taas_api: + image: taas_api:latest + build: + context: ../ + dockerfile: docker/Dockerfile + env_file: + - api.env + ports: + - "3000:3000" diff --git a/docker/sample.api.env b/docker/sample.api.env new file mode 100644 index 00000000..f7ed6a2d --- /dev/null +++ b/docker/sample.api.env @@ -0,0 +1,9 @@ +DATABASE_URL= +ES_HOST= + +AUTH0_URL= +AUTH0_AUDIENCE= +AUTH0_AUDIENCE_FOR_BUS_API= +TOKEN_CACHE_TIME=500000 +AUTH0_CLIENT_ID= +AUTH0_CLIENT_SECRET= diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index c95d79d2..df9b154b 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "4eab05e9-0474-4c51-8129-fff46034fae7", + "_postman_id": "266c56d9-a96a-4f45-9220-95620f026041", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -14,7 +14,7 @@ { "listen": "test", "script": { - "id": "08a44bb2-3d15-4f33-be09-9b8baefbe967", + "id": "4f296739-8f36-4d76-be66-269aa4c6c34a", "exec": [ "var data = JSON.parse(responseBody);\r", "postman.setEnvironmentVariable(\"jobId\",data.id);" @@ -34,7 +34,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"projectId\": 21,\r\n \"externalId\": \"1212\",\r\n \"description\": \"Dummy Description\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"numPositions\": 13,\r\n \"resourceType\": \"Dummy Resource Type\",\r\n \"rateType\": \"hourly\",\r\n \"skills\": [\r\n \"56fdc405-eccc-4189-9e83-c78abf844f50\",\r\n \"f91ae184-aba2-4485-a8cb-9336988c05ab\",\r\n \"edfc7b4f-636f-44bd-96fc-949ffc58e38b\",\r\n \"4ca63bb6-f515-4ab0-a6bc-c2d8531e084f\",\r\n \"ee03c041-d53b-4c08-b7d9-80d7461da3e4\"\r\n ]\r\n}", + "raw": "{\r\n \"projectId\": {{projectId}},\r\n \"externalId\": \"1212\",\r\n \"description\": \"Dummy Description\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"numPositions\": 13,\r\n \"resourceType\": \"Dummy Resource Type\",\r\n \"rateType\": \"hourly\",\r\n \"skills\": [\r\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\r\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\r\n \"cbac57a3-7180-4316-8769-73af64893158\",\r\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\r\n ]\r\n}\r\n", "options": { "raw": { "language": "json" @@ -59,7 +59,7 @@ { "listen": "test", "script": { - "id": "067f1c4a-f69a-4ad5-9a6f-731aee2c8540", + "id": "5d118bcb-ea69-4f92-b267-7a807deef522", "exec": [ "var data = JSON.parse(responseBody);\r", "postman.setEnvironmentVariable(\"jobId\",data.id);" @@ -79,7 +79,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"projectId\": 21,\r\n \"externalId\": \"1212\",\r\n \"description\": \"Dummy Description\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"numPositions\": 13,\r\n \"resourceType\": \"Dummy Resource Type\",\r\n \"rateType\": \"hourly\",\r\n \"skills\": [\r\n \"56fdc405-eccc-4189-9e83-c78abf844f50\",\r\n \"f91ae184-aba2-4485-a8cb-9336988c05ab\",\r\n \"edfc7b4f-636f-44bd-96fc-949ffc58e38b\",\r\n \"4ca63bb6-f515-4ab0-a6bc-c2d8531e084f\",\r\n \"ee03c041-d53b-4c08-b7d9-80d7461da3e4\"\r\n ]\r\n}", + "raw": "{\r\n \"projectId\": {{projectId}},\r\n \"externalId\": \"1212\",\r\n \"description\": \"Dummy Description\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"numPositions\": 13,\r\n \"resourceType\": \"Dummy Resource Type\",\r\n \"rateType\": \"hourly\",\r\n \"skills\": [\r\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\r\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\r\n \"cbac57a3-7180-4316-8769-73af64893158\",\r\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\r\n ]\r\n}", "options": { "raw": { "language": "json" @@ -104,7 +104,7 @@ { "listen": "test", "script": { - "id": "67f54beb-d9bd-437f-a6a0-8087526a36af", + "id": "cb499304-14a4-4c9e-a43f-4a3356f917aa", "exec": [ "" ], @@ -123,7 +123,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"projectId\": 21,\r\n \"externalId\": \"1212\",\r\n \"description\": \"Dummy Description\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"numPositions\": 13,\r\n \"resourceType\": \"Dummy Resource Type\",\r\n \"rateType\": \"hourly\",\r\n \"skills\": [\r\n \"56fdc405-eccc-4189-9e83-c78abf844f50\",\r\n \"f91ae184-aba2-4485-a8cb-9336988c05ab\",\r\n \"edfc7b4f-636f-44bd-96fc-949ffc58e38b\",\r\n \"4ca63bb6-f515-4ab0-a6bc-c2d8531e084f\",\r\n \"ee03c041-d53b-4c08-b7d9-80d7461da3e4\"\r\n ]\r\n}", + "raw": "{\r\n \"projectId\": {{projectId}},\r\n \"externalId\": \"1212\",\r\n \"description\": \"Dummy Description\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"numPositions\": 13,\r\n \"resourceType\": \"Dummy Resource Type\",\r\n \"rateType\": \"hourly\",\r\n \"skills\": [\r\n \"56fdc405-eccc-4189-9e83-c78abf844f50\",\r\n \"f91ae184-aba2-4485-a8cb-9336988c05ab\",\r\n \"edfc7b4f-636f-44bd-96fc-949ffc58e38b\",\r\n \"4ca63bb6-f515-4ab0-a6bc-c2d8531e084f\",\r\n \"ee03c041-d53b-4c08-b7d9-80d7461da3e4\"\r\n ]\r\n}", "options": { "raw": { "language": "json" @@ -148,7 +148,7 @@ { "listen": "test", "script": { - "id": "69e81fc3-2660-4780-983c-54dcb8ea2f70", + "id": "c0f62918-f916-468a-9e81-5151f1869f9f", "exec": [ "" ], @@ -167,7 +167,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"projectId\": 21,\r\n \"externalId\": \"1212\",\r\n \"description\": \"Dummy Description\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"numPositions\": 13,\r\n \"resourceType\": \"Dummy Resource Type\",\r\n \"rateType\": \"hourly\",\r\n \"skills\": [\r\n \"56fdc405-eccc-4189-9e83-c78abf844f50\",\r\n \"f91ae184-aba2-4485-a8cb-9336988c05ab\",\r\n \"edfc7b4f-636f-44bd-96fc-949ffc58e38b\",\r\n \"4ca63bb6-f515-4ab0-a6bc-c2d8531e084f\",\r\n \"ee03c041-d53b-4c08-b7d9-80d7461da3e4\"\r\n ]\r\n}", + "raw": "{\r\n \"projectId\": {{projectId}},\r\n \"externalId\": \"1212\",\r\n \"description\": \"Dummy Description\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"numPositions\": 13,\r\n \"resourceType\": \"Dummy Resource Type\",\r\n \"rateType\": \"hourly\",\r\n \"skills\": [\r\n \"56fdc405-eccc-4189-9e83-c78abf844f50\",\r\n \"f91ae184-aba2-4485-a8cb-9336988c05ab\",\r\n \"edfc7b4f-636f-44bd-96fc-949ffc58e38b\",\r\n \"4ca63bb6-f515-4ab0-a6bc-c2d8531e084f\",\r\n \"ee03c041-d53b-4c08-b7d9-80d7461da3e4\"\r\n ]\r\n}", "options": { "raw": { "language": "json" @@ -192,7 +192,7 @@ { "listen": "test", "script": { - "id": "754b1988-7cb2-4841-87bb-f349d2bb6ead", + "id": "ab099735-7af0-4f99-b274-a9be3f70ffd3", "exec": [ "" ], @@ -211,7 +211,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"projectId\": 21,\r\n \"externalId\": \"1212\",\r\n \"description\": \"Dummy Description\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"numPositions\": 13,\r\n \"resourceType\": \"Dummy Resource Type\",\r\n \"rateType\": \"hourly\",\r\n \"skills\": [\r\n \"56fdc405-eccc-4189-9e83-c78abf844f50\",\r\n \"f91ae184-aba2-4485-a8cb-9336988c05ab\",\r\n \"edfc7b4f-636f-44bd-96fc-949ffc58e38b\",\r\n \"4ca63bb6-f515-4ab0-a6bc-c2d8531e084f\",\r\n \"ee03c041-d53b-4c08-b7d9-80d7461da3e4\"\r\n ]\r\n}", + "raw": "{\r\n \"projectId\": {{projectId}},\r\n \"externalId\": \"1212\",\r\n \"description\": \"Dummy Description\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"numPositions\": 13,\r\n \"resourceType\": \"Dummy Resource Type\",\r\n \"rateType\": \"hourly\",\r\n \"skills\": [\r\n \"56fdc405-eccc-4189-9e83-c78abf844f50\",\r\n \"f91ae184-aba2-4485-a8cb-9336988c05ab\",\r\n \"edfc7b4f-636f-44bd-96fc-949ffc58e38b\",\r\n \"4ca63bb6-f515-4ab0-a6bc-c2d8531e084f\",\r\n \"ee03c041-d53b-4c08-b7d9-80d7461da3e4\"\r\n ]\r\n}", "options": { "raw": { "language": "json" @@ -254,6 +254,36 @@ }, "response": [] }, + { + "name": "get job with booking manager from db", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/jobs/{{jobId}}?fromDb=true", + "host": [ + "{{URL}}" + ], + "path": [ + "jobs", + "{{jobId}}" + ], + "query": [ + { + "key": "fromDb", + "value": "true" + } + ] + } + }, + "response": [] + }, { "name": "get job with connect user", "request": { @@ -695,7 +725,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"projectId\": 21,\r\n \"externalId\": \"1212\",\r\n \"description\": \"Dummy Description\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"numPositions\": 13,\r\n \"resourceType\": \"Dummy Resource Type\",\r\n \"rateType\": \"hourly\",\r\n \"skills\": [\r\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\r\n \"cc41ddc4-cacc-4570-9bdb-1229c12b9784\"\r\n ],\r\n \"status\": \"sourcing\"\r\n}", + "raw": "{\r\n \"projectId\": {{projectId}},\r\n \"externalId\": \"1212\",\r\n \"description\": \"Dummy Description\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"numPositions\": 13,\r\n \"resourceType\": \"Dummy Resource Type\",\r\n \"rateType\": \"hourly\",\r\n \"skills\": [\r\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\r\n \"cc41ddc4-cacc-4570-9bdb-1229c12b9784\"\r\n ],\r\n \"status\": \"sourcing\"\r\n}", "options": { "raw": { "language": "json" @@ -728,7 +758,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"projectId\": 21,\r\n \"externalId\": \"1212\",\r\n \"description\": \"Dummy Description\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"numPositions\": 13,\r\n \"resourceType\": \"Dummy Resource Type\",\r\n \"rateType\": \"hourly\",\r\n \"skills\": [\r\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\r\n \"cc41ddc4-cacc-4570-9bdb-1229c12b9784\"\r\n ],\r\n \"status\": \"sourcing\"\r\n}", + "raw": "{\r\n \"projectId\": {{projectId}},\r\n \"externalId\": \"1212\",\r\n \"description\": \"Dummy Description\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"numPositions\": 13,\r\n \"resourceType\": \"Dummy Resource Type\",\r\n \"rateType\": \"hourly\",\r\n \"skills\": [\r\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\r\n \"cc41ddc4-cacc-4570-9bdb-1229c12b9784\"\r\n ],\r\n \"status\": \"sourcing\"\r\n}", "options": { "raw": { "language": "json" @@ -761,7 +791,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"projectId\": 21,\r\n \"externalId\": \"1212\",\r\n \"description\": \"Dummy Description\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"numPositions\": 13,\r\n \"resourceType\": \"Dummy Resource Type\",\r\n \"rateType\": \"hourly\",\r\n \"skills\": [\r\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\r\n \"cc41ddc4-cacc-4570-9bdb-1229c12b9784\"\r\n ],\r\n \"status\": \"sourcing\"\r\n}", + "raw": "{\r\n \"projectId\": {{projectId}},\r\n \"externalId\": \"1212\",\r\n \"description\": \"Dummy Description\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"numPositions\": 13,\r\n \"resourceType\": \"Dummy Resource Type\",\r\n \"rateType\": \"hourly\",\r\n \"skills\": [\r\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\r\n \"cc41ddc4-cacc-4570-9bdb-1229c12b9784\"\r\n ],\r\n \"status\": \"sourcing\"\r\n}", "options": { "raw": { "language": "json" @@ -794,7 +824,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"projectId\": 21,\r\n \"externalId\": \"1212\",\r\n \"description\": \"Dummy Description\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"numPositions\": 13,\r\n \"resourceType\": \"Dummy Resource Type\",\r\n \"rateType\": \"hourly\",\r\n \"skills\": [\r\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\r\n \"cc41ddc4-cacc-4570-9bdb-1229c12b9784\"\r\n ],\r\n \"status\": \"sourcing\"\r\n}", + "raw": "{\r\n \"projectId\": {{projectId}},\r\n \"externalId\": \"1212\",\r\n \"description\": \"Dummy Description\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"numPositions\": 13,\r\n \"resourceType\": \"Dummy Resource Type\",\r\n \"rateType\": \"hourly\",\r\n \"skills\": [\r\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\r\n \"cc41ddc4-cacc-4570-9bdb-1229c12b9784\"\r\n ],\r\n \"status\": \"sourcing\"\r\n}", "options": { "raw": { "language": "json" @@ -827,7 +857,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"projectId\": 21,\r\n \"externalId\": \"1212\",\r\n \"description\": \"Dummy Description\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"numPositions\": 13,\r\n \"resourceType\": \"Dummy Resource Type\",\r\n \"rateType\": \"hourly\",\r\n \"skills\": [\r\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\r\n \"cc41ddc4-cacc-4570-9bdb-1229c12b9784\"\r\n ],\r\n \"status\": \"sourcing\"\r\n}", + "raw": "{\r\n \"projectId\": {{projectId}},\r\n \"externalId\": \"1212\",\r\n \"description\": \"Dummy Description\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"numPositions\": 13,\r\n \"resourceType\": \"Dummy Resource Type\",\r\n \"rateType\": \"hourly\",\r\n \"skills\": [\r\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\r\n \"cc41ddc4-cacc-4570-9bdb-1229c12b9784\"\r\n ],\r\n \"status\": \"sourcing\"\r\n}", "options": { "raw": { "language": "json" @@ -1156,7 +1186,7 @@ { "listen": "test", "script": { - "id": "6123ade2-c217-4cc1-b15a-287b6fb5830e", + "id": "6b168a78-7f6b-4000-bd44-3b9c76f0277e", "exec": [ "var data = JSON.parse(responseBody);\r", "postman.setEnvironmentVariable(\"jobCandidateId\",data.id);" @@ -1201,7 +1231,7 @@ { "listen": "test", "script": { - "id": "9b969b29-a02a-4020-a15d-4e3e2b7438d3", + "id": "bc965016-a5d4-4f08-9994-3f4865915125", "exec": [ "var data = JSON.parse(responseBody);\r", "postman.setEnvironmentVariable(\"jobCandidateId\",data.id);" @@ -1246,7 +1276,7 @@ { "listen": "test", "script": { - "id": "692f7d69-3e02-4d8b-8e6d-d25d832b798e", + "id": "751ec141-ef8d-4893-8802-d814006c8304", "exec": [ "var data = JSON.parse(responseBody);\r", "postman.setEnvironmentVariable(\"jobCandidateId\",data.id);" @@ -1291,7 +1321,7 @@ { "listen": "test", "script": { - "id": "dd78126f-682a-4b26-ad83-1abfc442f76a", + "id": "0fbba452-0387-494b-b09d-4efc7805a273", "exec": [ "var data = JSON.parse(responseBody);\r", "postman.setEnvironmentVariable(\"jobCandidateId\",data.id);" @@ -1336,7 +1366,7 @@ { "listen": "test", "script": { - "id": "d5b63f44-3ffc-417a-8e17-6e2ab19ec043", + "id": "c7ad1e24-852b-4f35-9131-81e0470f82be", "exec": [ "" ], @@ -1398,6 +1428,36 @@ }, "response": [] }, + { + "name": "get job candidate with booking manager from db", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/jobCandidates/{{jobCandidateId}}?fromDb=true", + "host": [ + "{{URL}}" + ], + "path": [ + "jobCandidates", + "{{jobCandidateId}}" + ], + "query": [ + { + "key": "fromDb", + "value": "true" + } + ] + } + }, + "response": [] + }, { "name": "get job candidate with connect user", "request": { @@ -2123,7 +2183,7 @@ { "listen": "test", "script": { - "id": "71bb710a-9c6a-4192-be70-6a3801ce6514", + "id": "84511971-c664-4334-b3de-2d5e3642ce95", "exec": [ "var data = JSON.parse(responseBody);\r", "postman.setEnvironmentVariable(\"resourceBookingId\",data.id);" @@ -2143,7 +2203,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"projectId\": 21,\r\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\r\n \"jobId\": \"{{jobId}}\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"memberRate\": 13.23,\r\n \"customerRate\": 13,\r\n \"rateType\": \"hourly\"\r\n}", + "raw": "{\r\n \"projectId\": {{projectId}},\r\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\r\n \"jobId\": \"{{jobId}}\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"memberRate\": 13.23,\r\n \"customerRate\": 13,\r\n \"rateType\": \"hourly\"\r\n}", "options": { "raw": { "language": "json" @@ -2168,7 +2228,7 @@ { "listen": "test", "script": { - "id": "a6a7f344-824d-4947-9fca-1b10545900e1", + "id": "fde28c86-d477-4214-8441-f74d99f4b654", "exec": [ "var data = JSON.parse(responseBody);\r", "postman.setEnvironmentVariable(\"resourceBookingId\",data.id);" @@ -2188,7 +2248,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"projectId\": 21,\r\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\r\n \"jobId\": \"{{jobId}}\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"memberRate\": 13.23,\r\n \"customerRate\": 13,\r\n \"rateType\": \"hourly\"\r\n}", + "raw": "{\r\n \"projectId\": {{projectId}},\r\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\r\n \"jobId\": \"{{jobId}}\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"memberRate\": 13.23,\r\n \"customerRate\": 13,\r\n \"rateType\": \"hourly\"\r\n}", "options": { "raw": { "language": "json" @@ -2213,7 +2273,7 @@ { "listen": "test", "script": { - "id": "65a44572-451b-41d0-8946-dd14bac1b731", + "id": "7e928dd2-24be-4349-9e7c-6e8ccec4d6e7", "exec": [ "" ], @@ -2232,7 +2292,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"projectId\": 21,\r\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\r\n \"jobId\": \"{{jobId}}\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"memberRate\": 13.23,\r\n \"customerRate\": 13,\r\n \"rateType\": \"hourly\"\r\n}", + "raw": "{\r\n \"projectId\": {{projectId}},\r\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\r\n \"jobId\": \"{{jobId}}\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"memberRate\": 13.23,\r\n \"customerRate\": 13,\r\n \"rateType\": \"hourly\"\r\n}", "options": { "raw": { "language": "json" @@ -2257,7 +2317,7 @@ { "listen": "test", "script": { - "id": "4994a8e1-eac7-4ee1-8fcc-6b53bbffe3f3", + "id": "25e3434c-0935-4aed-ab5f-7dc240acf95e", "exec": [ "" ], @@ -2276,7 +2336,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"projectId\": 21,\r\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\r\n \"jobId\": \"{{jobId}}\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"memberRate\": 13.23,\r\n \"customerRate\": 13,\r\n \"rateType\": \"hourly\"\r\n}", + "raw": "{\r\n \"projectId\": {{projectId}},\r\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\r\n \"jobId\": \"{{jobId}}\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"memberRate\": 13.23,\r\n \"customerRate\": 13,\r\n \"rateType\": \"hourly\"\r\n}", "options": { "raw": { "language": "json" @@ -2301,7 +2361,7 @@ { "listen": "test", "script": { - "id": "e9acb6ed-1931-493d-abd6-6bee2ab560fa", + "id": "ce94fcb6-b450-4cb6-bf65-18ab34a168ec", "exec": [ "" ], @@ -2320,7 +2380,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"projectId\": 21,\r\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\r\n \"jobId\": \"{{jobId}}\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"memberRate\": 13.23,\r\n \"customerRate\": 13,\r\n \"rateType\": \"hourly\"\r\n}", + "raw": "{\r\n \"projectId\": {{projectId}},\r\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\r\n \"jobId\": \"{{jobId}}\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"memberRate\": 13.23,\r\n \"customerRate\": 13,\r\n \"rateType\": \"hourly\"\r\n}", "options": { "raw": { "language": "json" @@ -2363,6 +2423,36 @@ }, "response": [] }, + { + "name": "get resource booking with booking manager from db", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings/{{resourceBookingId}}?fromDb=true", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings", + "{{resourceBookingId}}" + ], + "query": [ + { + "key": "fromDb", + "value": "true" + } + ] + } + }, + "response": [] + }, { "name": "get resource booking with connect user", "request": { @@ -2666,7 +2756,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"projectId\": 21,\r\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\r\n \"jobId\": \"{{jobId}}\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"memberRate\": 13.23,\r\n \"customerRate\": 13,\r\n \"rateType\": \"hourly\",\r\n \"status\": \"assigned\"\r\n}", + "raw": "{\r\n \"projectId\": {{projectId}},\r\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\r\n \"jobId\": \"{{jobId}}\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"memberRate\": 13.23,\r\n \"customerRate\": 13,\r\n \"rateType\": \"hourly\",\r\n \"status\": \"assigned\"\r\n}", "options": { "raw": { "language": "json" @@ -2699,7 +2789,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"projectId\": 21,\r\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\r\n \"jobId\": \"{{jobId}}\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"memberRate\": 13.23,\r\n \"customerRate\": 13,\r\n \"rateType\": \"hourly\",\r\n \"status\": \"assigned\"\r\n}", + "raw": "{\r\n \"projectId\": {{projectId}},\r\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\r\n \"jobId\": \"{{jobId}}\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"memberRate\": 13.23,\r\n \"customerRate\": 13,\r\n \"rateType\": \"hourly\",\r\n \"status\": \"assigned\"\r\n}", "options": { "raw": { "language": "json" @@ -2732,7 +2822,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"projectId\": 21,\r\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\r\n \"jobId\": \"{{jobId}}\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"memberRate\": 13.23,\r\n \"customerRate\": 13,\r\n \"rateType\": \"hourly\",\r\n \"status\": \"assigned\"\r\n}", + "raw": "{\r\n \"projectId\": {{projectId}},\r\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\r\n \"jobId\": \"{{jobId}}\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"memberRate\": 13.23,\r\n \"customerRate\": 13,\r\n \"rateType\": \"hourly\",\r\n \"status\": \"assigned\"\r\n}", "options": { "raw": { "language": "json" @@ -2765,7 +2855,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"projectId\": 21,\r\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\r\n \"jobId\": \"{{jobId}}\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"memberRate\": 13.23,\r\n \"customerRate\": 13,\r\n \"rateType\": \"hourly\",\r\n \"status\": \"assigned\"\r\n}", + "raw": "{\r\n \"projectId\": {{projectId}},\r\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\r\n \"jobId\": \"{{jobId}}\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"memberRate\": 13.23,\r\n \"customerRate\": 13,\r\n \"rateType\": \"hourly\",\r\n \"status\": \"assigned\"\r\n}", "options": { "raw": { "language": "json" @@ -2798,7 +2888,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"projectId\": 21,\r\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\r\n \"jobId\": \"{{jobId}}\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"memberRate\": 13.23,\r\n \"customerRate\": 13,\r\n \"rateType\": \"hourly\",\r\n \"status\": \"assigned\"\r\n}", + "raw": "{\r\n \"projectId\": {{projectId}},\r\n \"userId\": \"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a\",\r\n \"jobId\": \"{{jobId}}\",\r\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\r\n \"memberRate\": 13.23,\r\n \"customerRate\": 13,\r\n \"rateType\": \"hourly\",\r\n \"status\": \"assigned\"\r\n}", "options": { "raw": { "language": "json" @@ -3117,6 +3207,124 @@ } ], "protocolProfileBehavior": {} + }, + { + "name": "Taas Teams", + "item": [ + { + "name": "GET /taas-teams", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_connectUser}}", + "type": "text" + } + ], + "url": { + "raw": "{{URL}}/taas-teams", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams" + ] + } + }, + "response": [] + }, + { + "name": "GET /taas-teams/:id", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_connectUser}}", + "type": "text" + } + ], + "url": { + "raw": "{{URL}}/taas-teams/:projectId", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + ":projectId" + ], + "variable": [ + { + "key": "projectId", + "value": "16705" + } + ] + } + }, + "response": [] + }, + { + "name": "GET /taas-teams/:id/jobs/:jobId", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_connectUser}}", + "type": "text" + } + ], + "url": { + "raw": "{{URL}}/taas-teams/:projectId/jobs/:jobId", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + ":projectId", + "jobs", + ":jobId" + ], + "variable": [ + { + "key": "projectId", + "value": "16705" + }, + { + "key": "jobId", + "value": "948a25a6-086f-4a96-aad5-9ccd2d3e87b2" + } + ] + } + }, + "response": [] + } + ], + "protocolProfileBehavior": {} + }, + { + "name": "health check", + "item": [ + { + "name": "health check", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{URL}}/health", + "host": [ + "{{URL}}" + ], + "path": [ + "health" + ] + } + }, + "response": [] + } + ], + "protocolProfileBehavior": {} } ], "protocolProfileBehavior": {} diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 6eacb75a..8cc73679 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -13,6 +13,7 @@ tags: - name: Jobs - name: JobCandidates - name: ResourceBookings + - name: Teams paths: /jobs: post: @@ -240,6 +241,12 @@ paths: required: true schema: type: string + - in: query + name: fromDb + description: get data from db or not. + required: false + schema: + type: boolean responses: '200': description: OK @@ -636,6 +643,12 @@ paths: schema: type: string format: uuid + - in: query + name: fromDb + description: get data from db or not. + required: false + schema: + type: boolean responses: '200': description: OK @@ -1039,6 +1052,12 @@ paths: schema: type: string format: uuid + - in: query + name: fromDb + description: get data from db or not. + required: false + schema: + type: boolean responses: '200': description: OK @@ -1246,6 +1265,159 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /taas-teams: + get: + tags: + - Teams + description: | + Search my teams. Teams is project in topcoder with type=='talent-as-a-service' + security: + - bearerAuth: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Team' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Not authenticated + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /taas-teams/{id}: + get: + tags: + - Teams + description: | + Gets the team details. Should check if user has permission on the project or not by checking if he is connect user or not + security: + - bearerAuth: [] + parameters: + - in: path + name: id + required: true + schema: + type: integer + description: The team/project id. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/TeamDetail' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Not authenticated + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Not authorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /taas-teams/{id}/jobs/{jobId}: + get: + tags: + - Teams + description: | + Gets the job details including all candidates. Should check if user has permission on the project or not by checking if he is connect user or not + security: + - bearerAuth: [] + parameters: + - in: path + name: id + required: true + schema: + type: integer + description: The team/project id. + - in: path + name: jobId + required: true + schema: + type: string + format: uuid + description: The job id. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/JobDetail' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Not authenticated + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Not authorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /health: + get: + tags: + - Health + description: | + Get health status of the app. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CheckRun' + '503': + description: Service unavailable + content: + application/json: + schema: + $ref: '#/components/schemas/CheckRun' components: securitySchemes: bearerAuth: @@ -1641,6 +1813,215 @@ components: type: string enum: ['hourly', 'daily', 'weekly', 'monthly'] description: "The rate type of the job." + CheckRun: + type: object + properties: + checksRun: + type: integer + required: + - checksRun + Team: + properties: + id: + type: string + format: uuid + description: "The team id." + name: + type: string + example: "1212" + description: "The team name." + startDate: + type: string + format: date-time + example: "2020-09-27T04:17:23.131Z" + description: "The start date." + endDate: + type: string + format: date-time + example: "2020-09-27T04:17:23.131Z" + description: "The end date." + weeklyCost: + type: number + format: decimal + example: 5000.50 + description: "The average weekly cost" + totalPositions: + type: integer + description: 'The sum of number of positions opening in all job' + resources: + type: array + description: "The rosources that are assigned" + items: + $ref: '#/components/schemas/User' + User: + properties: + id: + type: string + format: uuid + description: "The user id." + handle: + type: string + example: "Tony" + description: "The project id." + photo_url: + type: string + format: url + example: "https://topcoder-dev-media.s3.amazonaws.com/member/profile/TonyJ-1604301092491.jpeg" + description: "The user avatar." + firstName: + type: string + example: 'Tony' + description: 'The first name of user' + lastName: + type: string + example: 'J' + description: 'The last name of user' + TeamDetail: + properties: + id: + type: string + format: uuid + description: "The team id." + name: + type: string + example: "1212" + description: "The team name." + startDate: + type: string + format: date-time + example: "2020-09-27T04:17:23.131Z" + description: "The start date." + endDate: + type: string + format: date-time + example: "2020-09-27T04:17:23.131Z" + description: "The end date." + weeklyCost: + type: number + format: decimal + example: 5000.50 + description: "The average weekly cost" + resources: + type: array + description: "The rosources that are assigned" + items: + allOf: + - $ref: '#/components/schemas/User' + type: object + properties: + customerRate: + type: integer + format: float + example: 13 + description: "The customer rate." + skills: + type: array + items: + $ref: '#/components/schemas/Skill' + skillMatched: + type: integer + description: The percentage of skill matched for that particular job he/she is assinged + job: + type: object + properties: + id: + type: string + format: uuid + description: The job id the member is assinged at + name: + type: string + description: The job name the member is assinged at + jobs: + type: array + description: "The jobs which are opened" + items: + $ref: '#/components/schemas/JobForTeam' + Skill: + type: object + properties: + id: + type: string + format: uuid + description: "The skill id." + name: + type: string + example: 'React' + description: The skill name. + JobForTeam: + properties: + id: + type: string + format: uuid + description: "The job id." + description: + type: string + example: "Dummy Description" + description: "The description." + startDate: + type: string + format: date-time + example: "2020-09-27T04:17:23.131Z" + description: "The job start date." + endDate: + type: string + format: date-time + example: "2020-09-27T04:17:23.131Z" + description: "The job end date." + numPositions: + type: integer + example: 13 + description: "The number of positions for the job." + rateType: + type: string + enum: ['hourly', 'daily', 'weekly', 'monthly'] + description: "The rate type of the job." + skills: + type: array + description: "The skills." + items: + $ref: '#/components/schemas/Skill' + customerRate: + type: integer + format: float + example: 5500 + description: "The customer rate." + status: + type: string + enum: ['sourcing', 'in-review', 'assigned', 'closed', 'cancelled'] + description: "The job status." + JobDetail: + type: object + properties: + id: + type: string + format: uuid + description: "The job id." + description: + type: string + example: "Dummy Description" + description: "The description." + candidates: + type: array + items: + allOf: + - $ref: '#/components/schemas/User' + type: object + properties: + resumeLink: + type: string + format: url + description: 'The link for the resume that can be downloaded' + status: + type: string + enum: ['open', 'selected', 'shortlist', 'rejected'] + description: "The job status." + skills: + type: array + items: + $ref: '#/components/schemas/Skill' + skillMatched: + type: integer + description: The percentage of skill matched for that particular job he/she is assinged Error: required: - message diff --git a/docs/topcoder-bookings.postman_environment.json b/docs/topcoder-bookings.postman_environment.json index c181f6aa..f87f1fe1 100644 --- a/docs/topcoder-bookings.postman_environment.json +++ b/docs/topcoder-bookings.postman_environment.json @@ -1,5 +1,5 @@ { - "id": "34635c8b-1454-4ede-8a2f-d52b0df73f4d", + "id": "cc8450af-34ea-4981-8736-4cf27d947306", "name": "topcoder-bookings", "values": [ { @@ -41,9 +41,14 @@ "key": "token_userId_not_exist", "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ik5VSkZORGd4UlRVME5EWTBOVVkzTlRkR05qTXlRamxETmpOQk5UYzVRVUV3UlRFeU56TTJRUSJ9.eyJodHRwczovL3RvcGNvZGVyLWRldi5jb20vcm9sZXMiOlsiVG9wY29kZXIgVXNlciIsImNvcGlsb3QiLCJDb25uZWN0IE1hbmFnZXIiLCJib29raW5nbWFuYWdlciIsInUtYmFobiJdLCJodHRwczovL3RvcGNvZGVyLWRldi5jb20vdXNlcklkIjoibm90X2V4aXN0IiwiaHR0cHM6Ly90b3Bjb2Rlci1kZXYuY29tL2hhbmRsZSI6InBzaGFoX21hbmFnZXIiLCJodHRwczovL3RvcGNvZGVyLWRldi5jb20vdXNlcl9pZCI6ImF1dGgwfDQwMTUyODU2IiwiaHR0cHM6Ly90b3Bjb2Rlci1kZXYuY29tL3Rjc3NvIjoiNDAxNTI4NTZ8ODEzNGY0OGViZTExYTg0OGEzNzU5ZTVlZjllOTJmMjE0NjkyZTIxMTMwNDBjODJiNWQ4ZjU4MWM2ZGZjY2M4OCIsImh0dHBzOi8vdG9wY29kZXItZGV2LmNvbS9hY3RpdmUiOnRydWUsIm5pY2tuYW1lIjoicHNoYWhfbWFuYWdlciIsIm5hbWUiOiJ2aWthcy5hZ2Fyd2FsK3BzaGFoX21hbmFnZXJAdG9wY29kZXIuY29tIiwicGljdHVyZSI6Imh0dHBzOi8vcy5ncmF2YXRhci5jb20vYXZhdGFyLzkyYWZiMmYwZWQ1MmZkZmFlMWYzNzEwMjFhZTY1MDEzP3M9NDgwJnI9cGcmZD1odHRwcyUzQSUyRiUyRmNkbi5hdXRoMC5jb20lMkZhdmF0YXJzJTJGdmkucG5nIiwidXBkYXRlZF9hdCI6IjIwMjAtMTAtMjRUMDg6Mjg6MjQuMTg0WiIsImVtYWlsIjoidmlrYXMuYWdhcndhbCtwc2hhaF9tYW5hZ2VyQHRvcGNvZGVyLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJpc3MiOiJodHRwczovL2F1dGgudG9wY29kZXItZGV2LmNvbS8iLCJzdWIiOiJhdXRoMHw0MDE1Mjg1NiIsImF1ZCI6IkJYV1hVV25pbFZVUGROMDF0MlNlMjlUdzJaWU5HWnZIIiwiaWF0IjoxNjAzNTQzMzM4LCJleHAiOjMzMTYwNDUyNzM4LCJub25jZSI6IlIxQTJjell1VlRabWJqWkhSRzkyVmw5RFNVSjZVbGx2UVhjM1JIaDVTM1pXZFdaRGNETlhNRVoxWHc9PSJ9.411P_6dTLAJ8TuwdUeyo9Pggzmmtjv37trsvK7ydWns", "enabled": true + }, + { + "key": "projectId", + "value": "111", + "enabled": true } ], "_postman_variable_scope": "environment", - "_postman_exported_at": "2020-10-25T14:47:11.930Z", - "_postman_exported_using": "Postman/7.34.0" -} \ No newline at end of file + "_postman_exported_at": "2020-11-19T12:26:05.800Z", + "_postman_exported_using": "Postman/7.29.0" +} diff --git a/package-lock.json b/package-lock.json index 82571617..6f940aa0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -479,6 +479,104 @@ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" }, + "@topcoder-platform/topcoder-bus-api-wrapper": { + "version": "github:topcoder-platform/tc-bus-api-wrapper#f8cbd335a0e0b4d6edd7cae859473593271fd97f", + "from": "github:topcoder-platform/tc-bus-api-wrapper", + "requires": { + "joi": "^13.4.0", + "lodash": "^4.17.15", + "superagent": "^3.8.3", + "tc-core-library-js": "github:appirio-tech/tc-core-library-js#v2.6.4" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "joi": { + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-13.7.0.tgz", + "integrity": "sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q==", + "requires": { + "hoek": "5.x.x", + "isemail": "3.x.x", + "topo": "3.x.x" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "superagent": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "requires": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.2.0", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.3.5" + } + }, + "tc-core-library-js": { + "version": "github:appirio-tech/tc-core-library-js#df0b36c51cf80918194cbff777214b3c0cf5a151", + "from": "github:appirio-tech/tc-core-library-js#v2.6.4", + "requires": { + "axios": "^0.19.0", + "bunyan": "^1.8.12", + "jsonwebtoken": "^8.5.1", + "jwks-rsa": "^1.6.0", + "lodash": "^4.17.15", + "millisecond": "^0.1.2", + "r7insight_node": "^1.8.4", + "request": "^2.88.0" + } + } + } + }, "@types/body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", @@ -818,6 +916,29 @@ "array-filter": "^1.0.0" } }, + "aws-sdk": { + "version": "2.787.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.787.0.tgz", + "integrity": "sha512-3WlUdWqUB8Vhdvj/7TENr/7SEmQzxmnHxOJ8l2WjZbcMRSuI0/9Ym4p1TC3hf21VDVDhkdGlw60QqpZQ1qb+Mg==", + "requires": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.15.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.3.2", + "xml2js": "0.4.19" + }, + "dependencies": { + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + } + } + }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -857,6 +978,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" + }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -980,6 +1106,16 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -1987,6 +2123,11 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + }, "express": { "version": "4.17.1", "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", @@ -2540,6 +2681,11 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, + "hoek": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", + "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" + }, "hosted-git-info": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", @@ -2647,6 +2793,11 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, "ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", @@ -2973,6 +3124,14 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, + "isemail": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz", + "integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==", + "requires": { + "punycode": "2.x.x" + } + }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3179,6 +3338,11 @@ "iterate-iterator": "^1.0.1" } }, + "jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" + }, "joi": { "version": "17.2.1", "resolved": "https://registry.npmjs.org/joi/-/joi-17.2.1.tgz", @@ -4824,6 +4988,11 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, "r7insight_node": { "version": "1.8.4", "resolved": "https://registry.npmjs.org/r7insight_node/-/r7insight_node-1.8.4.tgz", @@ -5132,6 +5301,11 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" + }, "secure-json-parse": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.1.0.tgz", @@ -5722,6 +5896,21 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, + "topo": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/topo/-/topo-3.0.3.tgz", + "integrity": "sha512-IgpPtvD4kjrJ7CRA3ov2FhWQADwv+Tdqbsf1ZnPUSAtCJ9e1Z44MmoSGDXGk4IppoZA7jd/QRkNddlLJWlUZsQ==", + "requires": { + "hoek": "6.x.x" + }, + "dependencies": { + "hoek": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.3.tgz", + "integrity": "sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ==" + } + } + }, "toposort-class": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", @@ -5920,6 +6109,22 @@ "punycode": "^2.1.0" } }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + } + } + }, "url-parse-lax": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", @@ -6205,6 +6410,20 @@ "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", "dev": true }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index a82b5c59..bad6ecac 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "init-db": "node src/init-db.js", "create-index": "node scripts/createIndex.js", "delete-index": "node scripts/deleteIndex.js", + "test-data": "node scripts/insert-es-data.js", "test": "mocha test/unit/*.test.js --timeout 30000 --exit", "cov": "nyc --reporter=html --reporter=text mocha test/unit/*.test.js --timeout 30000 --exit" }, @@ -19,6 +20,8 @@ "license": "ISC", "dependencies": { "@elastic/elasticsearch": "^7.9.1", + "@topcoder-platform/topcoder-bus-api-wrapper": "github:topcoder-platform/tc-bus-api-wrapper", + "aws-sdk": "^2.787.0", "config": "^3.3.2", "cors": "^2.8.5", "dotenv": "^8.2.0", diff --git a/scripts/createIndex.js b/scripts/createIndex.js index 853aec42..ad5fa2c0 100644 --- a/scripts/createIndex.js +++ b/scripts/createIndex.js @@ -7,7 +7,6 @@ const logger = require('../src/common/logger') const helper = require('../src/common/helper') async function createIndex () { - logger.info('ES Index creation started!') const esClient = helper.getESClient() const indices = [ @@ -75,12 +74,12 @@ async function createIndex () { for (const index of indices) { await esClient.indices.create(index) - logger.info(`ES Index ${index.index} creation succeeded!`) + logger.info({ component: 'createIndex', message: `ES Index ${index.index} creation succeeded!` }) } process.exit(0) } createIndex().catch((err) => { - logger.logFullError(err) + logger.logFullError(err, { component: 'createIndex' }) process.exit(1) }) diff --git a/scripts/deleteIndex.js b/scripts/deleteIndex.js index 8ae7916b..dd7ca7e6 100644 --- a/scripts/deleteIndex.js +++ b/scripts/deleteIndex.js @@ -7,7 +7,7 @@ const logger = require('../src/common/logger') const helper = require('../src/common/helper') async function deleteIndex () { - logger.info('ES Index deletion started!') + logger.info({ component: 'deleteIndex', message: 'ES Index deletion started!' }) const esClient = helper.getESClient() const indices = [config.get('esConfig.ES_INDEX_JOB'), config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), @@ -16,11 +16,11 @@ async function deleteIndex () { await esClient.indices.delete({ index }) - logger.info(`ES Index ${index} deletion succeeded!`) + logger.info({ component: 'deleteIndex', message: `ES Index ${index} deletion succeeded!` }) } process.exit(0) } deleteIndex().catch((err) => { - logger.logFullError(err) + logger.logFullError(err, { component: 'deleteIndex' }) process.exit(1) }) diff --git a/scripts/feed-data/jobCandidates.json b/scripts/feed-data/jobCandidates.json new file mode 100644 index 00000000..f76c8978 --- /dev/null +++ b/scripts/feed-data/jobCandidates.json @@ -0,0 +1,152 @@ +{ + "result": [{ + "jobId": "948a25a6-086f-4a96-aad5-9ccd2d3e87b2", + "userId": "df2f0027-f74f-45fa-85cd-84c9fdc2faf4", + "createdAt": "2020-11-18T14:46:06.146Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "open", + "id": "11a64405-3b70-4e40-9f88-bbf12819a62b" + }, { + "jobId": "7489c927-7e19-404c-9947-6039ede6123b", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "createdAt": "2020-11-18T14:46:06.146Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "open", + "id": "f9b59481-a44a-4575-acda-49dcb271e507" + }, { + "jobId": "0c1e518f-7aad-47f6-81e1-1d0aedb1e9b6", + "userId": "3f64739e-10bf-42ca-8314-8aea0245cd0f", + "createdAt": "2020-11-09T14:51:18.579Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "open", + "id": "f61e555a-4682-4f3d-b3d4-8f705002612b" + }, { + "jobId": "2de6b167-8c6a-44dd-a6a2-8abd8bf6443b", + "userId": "df2f0027-f74f-45fa-85cd-84c9fdc2faf4", + "createdAt": "2020-11-18T16:22:21.927Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "open", + "id": "dd209fc4-73c5-49cb-b8aa-1f00b73ded74" + }, { + "jobId": "07102f1a-6399-4ec6-9198-0156a5bbc991", + "userId": "3f64739e-10bf-42ca-8314-8aea0245cd0f", + "createdAt": "2020-11-19T09:29:03.237Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "open", + "id": "d8a7617d-d9f9-4ba5-8f60-9c205a1966ac" + }, { + "jobId": "7489c927-7e19-404c-9947-6039ede6123b", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "createdAt": "2020-11-18T14:46:09.052Z", + "createdBy": "0bcb0d86-09bb-410a-b2b1-fba90d1a7699", + "status": "open", + "id": "cd4d117a-0b56-40ae-bf9c-3d820e9829ae" + }, { + "jobId": "cd7b9ca7-8b5a-4cfb-b04a-5a6c5214efe7", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "createdAt": "2020-11-18T14:47:53.977Z", + "createdBy": "0bcb0d86-09bb-410a-b2b1-fba90d1a7699", + "status": "open", + "id": "a8d82e96-2307-4dd9-bab8-0c151ce6f3a7" + }, { + "jobId": "cd7b9ca7-8b5a-4cfb-b04a-5a6c5214efe7", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "createdAt": "2020-11-18T14:47:49.785Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "open", + "id": "8ff03c86-bd6e-43e6-ad2b-c7e65f142fcf" + }, { + "jobId": "1a5f81ca-65e1-4022-b997-6ab0f9140aa0", + "userId": "3f64739e-10bf-42ca-8314-8aea0245cd0f", + "createdAt": "2020-11-19T09:51:55.236Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "open", + "id": "8eb66f6c-be1a-4fde-8705-15dbbb33f9b1" + }, { + "jobId": "2393adca-0253-4423-a8b4-55194d383d44", + "userId": "1b88e433-828b-4e0d-9fb5-ef75b9dcca6e", + "createdAt": "2020-11-19T14:39:13.332Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "open", + "id": "84abfdc5-7af6-4b03-bde8-7afeadf705a3" + }, { + "jobId": "d14418d5-0a55-45b2-b203-cd7235818732", + "userId": "07102f1a-6399-4ec6-9198-0156a5bbc991", + "createdAt": "2020-11-19T09:25:53.636Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "open", + "id": "83c3d6d0-d6af-49e5-acbf-b35ba99e5901" + }, { + "jobId": "cd7b9ca7-8b5a-4cfb-b04a-5a6c5214efe7", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "createdAt": "2020-11-18T14:47:58.079Z", + "createdBy": "0bcb0d86-09bb-410a-b2b1-fba90d1a7699", + "status": "open", + "id": "6ca8e095-c3c1-405d-8fb1-64b1f193271b" + }, { + "jobId": "1a5f81ca-65e1-4022-b997-6ab0f9140aa0", + "userId": "1b88e433-828b-4e0d-9fb5-ef75b9dcca6e", + "createdAt": "2020-11-19T14:35:20.119Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "open", + "id": "6aefe263-bb94-471b-a0f4-4f6748dc3819" + }, { + "jobId": "a3b4e6ba-af63-4d6e-a878-faac1b67ab05", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "createdAt": "2020-11-18T14:39:58.951Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "open", + "id": "51b922dd-dcde-4864-b83c-62eb5e53b3eb" + }, { + "jobId": "05c7a62e-ba48-4c6b-90f5-48080daecb98", + "userId": "3f64739e-10bf-42ca-8314-8aea0245cd0f", + "createdAt": "2020-11-16T09:44:54.228Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "open", + "id": "43dcc6f1-653f-4741-b3b7-83a098456986" + }, { + "jobId": "d14418d5-0a55-45b2-b203-cd7235818732", + "userId": "3f64739e-10bf-42ca-8314-8aea0245cd0f", + "createdAt": "2020-11-17T08:07:59.641Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "open", + "id": "421954de-04d0-4f9e-a310-c9c0df889649" + }, { + "jobId": "a3b4e6ba-af63-4d6e-a878-faac1b67ab05", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "createdAt": "2020-11-18T14:40:04.437Z", + "createdBy": "0bcb0d86-09bb-410a-b2b1-fba90d1a7699", + "status": "open", + "id": "3c3f2b10-2867-484f-b855-b37f2832b5f5" + }, { + "jobId": "1304d5a4-cae9-46fe-aada-0470c349636b", + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "createdAt": "2020-11-18T15:12:33.132Z", + "createdBy": "0bcb0d86-09bb-410a-b2b1-fba90d1a7699", + "status": "shortlist", + "updatedBy": "0bcb0d86-09bb-410a-b2b1-fba90d1a7699", + "updatedAt": "2020-11-18T15:12:42.714Z", + "id": "3b9bc2a2-db47-4012-8213-9ec559690146" + }, { + "jobId": "0c1e518f-7aad-47f6-81e1-1d0aedb1e9b6", + "userId": "3f64739e-10bf-42ca-8314-8aea0245cd0f", + "createdAt": "2020-11-04T08:35:54.342Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "open", + "id": "37b5b42d-c3d3-4877-9f9d-3df67a467707" + }, { + "jobId": "05645f48-6d8d-44cd-accb-a8bb8f21a7b9", + "userId": "3f64739e-10bf-42ca-8314-8aea0245cd0f", + "createdAt": "2020-11-18T16:12:13.637Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "open", + "id": "1e7bf292-3859-4571-94e3-d3ad80924515" + }, { + "jobId": "0c1e518f-7aad-47f6-81e1-1d0aedb1e9b6", + "userId": "3f64739e-10bf-42ca-8314-8aea0245cd0f", + "createdAt": "2020-11-09T12:53:33.710Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "open", + "id": "181242e5-8bda-4f03-956a-7ade75952d86" + }] +} diff --git a/scripts/feed-data/jobs.json b/scripts/feed-data/jobs.json new file mode 100644 index 00000000..4106bf0a --- /dev/null +++ b/scripts/feed-data/jobs.json @@ -0,0 +1,302 @@ +{ + "result": [{ + "projectId": 9050, + "externalId": "1212", + "description": "Dummy Description", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "numPositions": 13, + "resourceType": "Dummy Resource Type", + "rateType": "hourly", + "skills": ["faaa0c21-2352-4e27-a1cf-dc05345a86ba"], + "createdAt": "2020-11-18T16:56:11.875Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "sourcing", + "id": "f0e8ab04-8659-4ecb-bf7b-f81f24a995f1" + }, { + "projectId": 26, + "externalId": "1211", + "description": "Dummy123 Description", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "numPositions": 2, + "resourceType": "Dummy Resource Type", + "rateType": "weekly", + "skills": ["3fa85f64-5717-4562-b3fc-2c963f66afa6"], + "createdAt": "2020-11-04T10:13:15.651Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "sourcing", + "id": "ebb35606-8951-4b18-b825-db3d97946930" + }, { + "projectId": 60, + "externalId": "0", + "description": "Dummy59 Description", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "numPositions": 60, + "resourceType": "Dummy Resource Type", + "rateType": "weekly", + "skills": ["3fa85f64-5717-4562-b3fc-2c963f66afa6"], + "createdAt": "2020-11-18T15:12:35.798Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "sourcing", + "id": "e6653ff9-a4bf-4127-a4f2-397613652d3a" + }, { + "projectId": 59, + "externalId": "0", + "description": "Dummy59 Description", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "numPositions": 59, + "resourceType": "Dummy Resource Type", + "rateType": "weekly", + "skills": ["3fa85f64-5717-4562-b3fc-2c963f66afa6"], + "createdAt": "2020-11-17T08:02:01.505Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "sourcing", + "id": "d14418d5-0a55-45b2-b203-cd7235818732", + "candidates": [{ + "jobId": "d14418d5-0a55-45b2-b203-cd7235818732", + "userId": "3f64739e-10bf-42ca-8314-8aea0245cd0f", + "createdAt": "2020-11-17T08:07:59.641Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "open", + "id": "421954de-04d0-4f9e-a310-c9c0df889649" + }, { + "jobId": "d14418d5-0a55-45b2-b203-cd7235818732", + "userId": "07102f1a-6399-4ec6-9198-0156a5bbc991", + "createdAt": "2020-11-19T09:25:53.636Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "open", + "id": "83c3d6d0-d6af-49e5-acbf-b35ba99e5901" + }] + }, { + "projectId": 109, + "externalId": "34192329", + "description": " Dummy109 Description", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "numPositions": 9, + "resourceType": "Dummy9 Resource Type", + "rateType": "weekly", + "skills": ["3fa85f64-5717-4562-b3fc-2c963f66afa6"], + "createdAt": "2020-11-04T10:15:57.442Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "sourcing", + "updatedBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "updatedAt": "2020-11-14T12:32:30.584Z", + "id": "b5b2bcb3-4c3b-44d6-ae28-8b8ad721a30b" + }, { + "projectId": 26, + "externalId": "1211", + "description": "Dummy123 Description", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "numPositions": 2, + "resourceType": "Dummy Resource Type", + "rateType": "weekly", + "skills": ["3fa85f64-5717-4562-b3fc-2c963f66afa6"], + "createdAt": "2020-11-04T16:57:36.775Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "sourcing", + "id": "b3e49039-aaf8-4a7b-90ff-cacc05b85259" + }, { + "projectId": 9050, + "externalId": "1212", + "description": "Dummy Description", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "numPositions": 13, + "resourceType": "Dummy Resource Type", + "rateType": "hourly", + "skills": ["56fdc405-eccc-4189-9e83-c78abf844f50", "f91ae184-aba2-4485-a8cb-9336988c05ab", "edfc7b4f-636f-44bd-96fc-949ffc58e38b", "4ca63bb6-f515-4ab0-a6bc-c2d8531e084f", "ee03c041-d53b-4c08-b7d9-80d7461da3e4"], + "createdAt": "2020-11-18T16:50:04.244Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "sourcing", + "id": "b014c046-cc3d-42a4-80e4-a29280a39479" + }, { + "projectId": 21, + "externalId": "1212", + "description": "Dummy Description", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "numPositions": 13, + "resourceType": "Dummy Resource Type", + "rateType": "hourly", + "skills": ["56fdc405-eccc-4189-9e83-c78abf844f50", "f91ae184-aba2-4485-a8cb-9336988c05ab", "edfc7b4f-636f-44bd-96fc-949ffc58e38b", "4ca63bb6-f515-4ab0-a6bc-c2d8531e084f", "ee03c041-d53b-4c08-b7d9-80d7461da3e4"], + "createdAt": "2020-11-17T21:00:15.689Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "sourcing", + "id": "a938da41-9dd0-44e0-8b0e-271e98646967" + }, { + "projectId": 111, + "externalId": "1212", + "description": "Dummy Description", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "numPositions": 13, + "resourceType": "Dummy Resource Type", + "rateType": "hourly", + "skills": ["56fdc405-eccc-4189-9e83-c78abf844f50", "f91ae184-aba2-4485-a8cb-9336988c05ab", "edfc7b4f-636f-44bd-96fc-949ffc58e38b", "4ca63bb6-f515-4ab0-a6bc-c2d8531e084f", "ee03c041-d53b-4c08-b7d9-80d7461da3e4"], + "createdAt": "2020-11-19T09:40:20.142Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "sourcing", + "id": "a0172c49-6607-476c-81f2-5808adf3e4cf" + }, { + "projectId": 111, + "externalId": "1212", + "description": "Dummy Description", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "numPositions": 13, + "resourceType": "Dummy Resource Type", + "rateType": "hourly", + "skills": ["56fdc405-eccc-4189-9e83-c78abf844f50", "f91ae184-aba2-4485-a8cb-9336988c05ab", "edfc7b4f-636f-44bd-96fc-949ffc58e38b", "4ca63bb6-f515-4ab0-a6bc-c2d8531e084f", "ee03c041-d53b-4c08-b7d9-80d7461da3e4"], + "createdAt": "2020-11-19T09:59:53.343Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "sourcing", + "id": "9f14c68f-baad-4fd6-aa8d-d76ede8eaf65" + }, { + "projectId": 111, + "externalId": "1212", + "description": "Dummy Description", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "numPositions": 13, + "resourceType": "Dummy Resource Type", + "rateType": "hourly", + "skills": ["56fdc405-eccc-4189-9e83-c78abf844f50", "f91ae184-aba2-4485-a8cb-9336988c05ab", "edfc7b4f-636f-44bd-96fc-949ffc58e38b", "4ca63bb6-f515-4ab0-a6bc-c2d8531e084f", "ee03c041-d53b-4c08-b7d9-80d7461da3e4"], + "createdAt": "2020-11-18T15:07:00.946Z", + "createdBy": "0bcb0d86-09bb-410a-b2b1-fba90d1a7699", + "status": "sourcing", + "id": "9bfe3042-eee2-4621-a59c-c41b9577b029" + }, { + "projectId": 16705, + "externalId": "0", + "description": "Dummy16705-uniq1 Description", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "numPositions": 60, + "resourceType": "Dummy Resource Type", + "rateType": "weekly", + "skills": ["faaa0c21-2352-4e27-a1cf-dc05345a86ba"], + "createdAt": "2020-11-18T16:56:15.437Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "sourcing", + "id": "948a25a6-086f-4a96-aad5-9ccd2d3e87b2" + }, { + "projectId": 111, + "externalId": "1212", + "description": "Dummy Description", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "numPositions": 13, + "resourceType": "Dummy Resource Type", + "rateType": "hourly", + "skills": ["56fdc405-eccc-4189-9e83-c78abf844f50", "f91ae184-aba2-4485-a8cb-9336988c05ab", "edfc7b4f-636f-44bd-96fc-949ffc58e38b", "4ca63bb6-f515-4ab0-a6bc-c2d8531e084f", "ee03c041-d53b-4c08-b7d9-80d7461da3e4"], + "createdAt": "2020-11-18T14:44:41.794Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "sourcing", + "id": "90d969d7-992c-429d-82d5-bb875ad7990a" + }, { + "projectId": 21, + "externalId": "1212", + "description": "Dummy Description", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "numPositions": 13, + "resourceType": "Dummy Resource Type", + "rateType": "hourly", + "skills": ["56fdc405-eccc-4189-9e83-c78abf844f50", "f91ae184-aba2-4485-a8cb-9336988c05ab", "edfc7b4f-636f-44bd-96fc-949ffc58e38b", "4ca63bb6-f515-4ab0-a6bc-c2d8531e084f", "ee03c041-d53b-4c08-b7d9-80d7461da3e4"], + "createdAt": "2020-11-18T14:12:12.543Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "sourcing", + "id": "8a46556c-b820-4aae-92f1-11371dd783fc" + }, { + "projectId": 21, + "externalId": "1212", + "description": "Dummy Description", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "numPositions": 13, + "resourceType": "Dummy Resource Type", + "rateType": "hourly", + "skills": ["56fdc405-eccc-4189-9e83-c78abf844f50", "f91ae184-aba2-4485-a8cb-9336988c05ab", "edfc7b4f-636f-44bd-96fc-949ffc58e38b", "4ca63bb6-f515-4ab0-a6bc-c2d8531e084f", "ee03c041-d53b-4c08-b7d9-80d7461da3e4"], + "createdAt": "2020-11-18T13:57:28.300Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "sourcing", + "id": "8875c462-3562-4c43-8614-ce063b24c32f" + }, { + "projectId": 26, + "externalId": "1212", + "description": "Dummy535353 Description", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "numPositions": 1, + "resourceType": "Dummy12353 Resource Type", + "rateType": "hourly", + "skills": ["3fa85f64-5717-4562-b3fc-2c963f66afa6"], + "createdAt": "2020-11-16T09:38:35.099Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "sourcing", + "updatedBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "updatedAt": "2020-11-16T09:43:33.664Z", + "id": "8444b21a-9970-487a-9c3d-b927378fafd8" + }, { + "projectId": 9050, + "externalId": "1212", + "description": "Dummy Description", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "numPositions": 13, + "resourceType": "Dummy Resource Type", + "rateType": "hourly", + "skills": ["faaa0c21-2352-4e27-a1cf-dc05345a86ba"], + "createdAt": "2020-11-19T09:41:04.100Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "sourcing", + "id": "79a056b8-bdbf-4135-b828-3265f9a32763" + }, { + "projectId": 111, + "externalId": "1212", + "description": "Dummy Description", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "numPositions": 13, + "resourceType": "Dummy Resource Type", + "rateType": "hourly", + "skills": ["56fdc405-eccc-4189-9e83-c78abf844f50", "f91ae184-aba2-4485-a8cb-9336988c05ab", "edfc7b4f-636f-44bd-96fc-949ffc58e38b", "4ca63bb6-f515-4ab0-a6bc-c2d8531e084f", "ee03c041-d53b-4c08-b7d9-80d7461da3e4"], + "createdAt": "2020-11-18T14:23:37.829Z", + "createdBy": "0bcb0d86-09bb-410a-b2b1-fba90d1a7699", + "status": "sourcing", + "id": "75cd1ff7-ee40-44c9-9ca4-20491ec76536" + }, { + "projectId": 59, + "externalId": "0", + "description": "Dummy59 Description", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "numPositions": 59, + "resourceType": "Dummy Resource Type", + "rateType": "weekly", + "skills": ["3fa85f64-5717-4562-b3fc-2c963f66afa6"], + "createdAt": "2020-11-17T08:06:35.838Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "sourcing", + "id": "74a2f422-7d60-495e-a330-8f277042474d" + }, { + "projectId": 111, + "externalId": "1212", + "description": "Dummy Description", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "numPositions": 13, + "resourceType": "Dummy Resource Type", + "rateType": "hourly", + "skills": ["56fdc405-eccc-4189-9e83-c78abf844f50", "f91ae184-aba2-4485-a8cb-9336988c05ab", "edfc7b4f-636f-44bd-96fc-949ffc58e38b", "4ca63bb6-f515-4ab0-a6bc-c2d8531e084f", "ee03c041-d53b-4c08-b7d9-80d7461da3e4"], + "createdAt": "2020-11-18T14:37:00.936Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "sourcing", + "id": "60a7db48-7d86-4612-bf11-a173b1bc6295" + }] +} \ No newline at end of file diff --git a/scripts/feed-data/resourceBookings.json b/scripts/feed-data/resourceBookings.json new file mode 100644 index 00000000..5c33b333 --- /dev/null +++ b/scripts/feed-data/resourceBookings.json @@ -0,0 +1,195 @@ +{ + "result": [{ + "projectId": 16704, + "userId": "3f64739e-10bf-42ca-8314-8aea0245cd0f", + "jobId": "1a5f81ca-65e1-4022-b997-6ab0f9140aa0", + "startDate": "2020-11-27T04:17:23.131Z", + "endDate": "2020-12-27T04:17:23.131Z", + "memberRate": 13.23, + "customerRate": 113, + "rateType": "weekly", + "createdAt": "2020-11-19T09:55:40.896Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "sourcing", + "updatedBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "updatedAt": "2020-11-19T09:58:08.413Z", + "id": "cd32fce8-7cbb-4a8e-ad32-b5dc8e495f47" + }, { + "projectId": 61, + "userId": "3f64739e-10bf-42ca-8314-8aea0245cd0f", + "jobId": "0c1e518f-7aad-47f6-81e1-1d0aedb1e9b6", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "memberRate": 61, + "customerRate": 61, + "rateType": "weekly", + "createdAt": "2020-11-18T08:24:59.286Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "sourcing", + "id": "c8189e84-6cd6-41f8-bbf5-0c692a38594d" + }, { + "projectId": 16704, + "userId": "3f64739e-10bf-42ca-8314-8aea0245cd0f", + "jobId": "05645f48-6d8d-44cd-accb-a8bb8f21a7b9", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "memberRate": 61, + "customerRate": 13, + "rateType": "hourly", + "createdAt": "2020-11-18T16:14:35.995Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "assigned", + "updatedBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "updatedAt": "2020-11-18T16:15:23.546Z", + "id": "c7c1ee0f-84cd-4153-8f8c-d06e4bf89dff" + }, { + "projectId": 16705, + "userId": "df2f0027-f74f-45fa-85cd-84c9fdc2faf4", + "jobId": "2de6b167-8c6a-44dd-a6a2-8abd8bf6443b", + "startDate": "2020-11-15T04:17:23.131Z", + "endDate": "2020-12-15T04:17:23.131Z", + "memberRate": 200, + "customerRate": 200, + "rateType": "weekly", + "createdAt": "2020-11-18T16:24:02.682Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "assigned", + "updatedBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "updatedAt": "2020-11-18T16:24:56.797Z", + "id": "a7a65205-d7c5-4725-a1db-923543e621b1" + }, { + "projectId": 56, + "userId": "3f64739e-10bf-42ca-8314-8aea0245cd0f", + "jobId": "0c1e518f-7aad-47f6-81e1-1d0aedb1e9b6", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "memberRate": 13.23, + "customerRate": 13, + "rateType": "hourly", + "createdAt": "2020-11-06T08:20:59.895Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "sourcing", + "id": "9c8144c9-bb38-4a0a-98e4-270001abf040" + }, { + "projectId": 55, + "userId": "3f64739e-10bf-42ca-8314-8aea0245cd0f", + "jobId": "0c1e518f-7aad-47f6-81e1-1d0aedb1e9b6", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "memberRate": 13.23, + "customerRate": 13, + "rateType": "hourly", + "createdAt": "2020-11-06T08:18:54.803Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "sourcing", + "id": "926c6dde-e9b8-4f8e-816a-6ada8f458faf" + }, { + "projectId": 111, + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "jobId": "1304d5a4-cae9-46fe-aada-0470c349636b", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "memberRate": 13.23, + "customerRate": 13, + "rateType": "hourly", + "createdAt": "2020-11-18T15:12:47.545Z", + "createdBy": "0bcb0d86-09bb-410a-b2b1-fba90d1a7699", + "status": "assigned", + "updatedBy": "0bcb0d86-09bb-410a-b2b1-fba90d1a7699", + "updatedAt": "2020-11-18T15:12:56.010Z", + "id": "52093ad7-aea3-48ae-88b9-6055f4c49e22" + }, { + "projectId": 111, + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "jobId": "7489c927-7e19-404c-9947-6039ede6123b", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "memberRate": 13.23, + "customerRate": 13, + "rateType": "hourly", + "createdAt": "2020-11-18T14:46:14.088Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "sourcing", + "id": "496a0f53-8d6c-44a8-ac46-0198f88692aa" + }, { + "projectId": 25, + "userId": "3f64739e-10bf-42ca-8314-8aea0245cd0f", + "jobId": "0c1e518f-7aad-47f6-81e1-1d0aedb1e9b6", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "memberRate": 13.23, + "customerRate": 13, + "rateType": "hourly", + "createdAt": "2020-11-04T08:37:19.635Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "sourcing", + "id": "41fc7d75-b41c-4a07-af37-2d411bf0ee0f" + }, { + "projectId": 61, + "userId": "3f64739e-10bf-42ca-8314-8aea0245cd0f", + "jobId": "0c1e518f-7aad-47f6-81e1-1d0aedb1e9b6", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "memberRate": 13.23, + "customerRate": 13, + "rateType": "hourly", + "createdAt": "2020-11-06T08:37:25.696Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "sourcing", + "id": "40193e5f-59f4-4c73-986b-da3e5d8caee2" + }, { + "projectId": 16704, + "userId": "1b88e433-828b-4e0d-9fb5-ef75b9dcca6e", + "jobId": "2393adca-0253-4423-a8b4-55194d383d44", + "startDate": "2020-11-27T04:17:23.131Z", + "endDate": "2020-12-27T04:17:23.131Z", + "memberRate": 13.23, + "customerRate": 113, + "rateType": "weekly", + "createdAt": "2020-11-19T14:40:43.025Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "assigned", + "updatedBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "updatedAt": "2020-11-19T14:41:52.415Z", + "id": "38bdcff6-89b2-4e3d-9899-5dd9b2cd344c" + }, { + "projectId": 25, + "userId": "3f64739e-10bf-42ca-8314-8aea0245cd0f", + "jobId": "0c1e518f-7aad-47f6-81e1-1d0aedb1e9b6", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "memberRate": 13.23, + "customerRate": 13, + "rateType": "hourly", + "createdAt": "2020-11-06T07:32:11.293Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "sourcing", + "id": "1e40607c-d6e1-4ab2-ba94-ff7ac8d0347e" + }, { + "projectId": 111, + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "jobId": "cd7b9ca7-8b5a-4cfb-b04a-5a6c5214efe7", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "memberRate": 13.23, + "customerRate": 13, + "rateType": "hourly", + "createdAt": "2020-11-18T14:48:03.482Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "sourcing", + "id": "1c73a4f3-1704-4ecb-85a7-a40d376b3eea" + }, { + "projectId": 60, + "userId": "3f64739e-10bf-42ca-8314-8aea0245cd0f", + "jobId": "0c1e518f-7aad-47f6-81e1-1d0aedb1e9b6", + "startDate": "2020-09-27T04:17:23.131Z", + "endDate": "2020-09-27T04:17:23.131Z", + "memberRate": 13.23, + "customerRate": 13, + "rateType": "hourly", + "createdAt": "2020-11-06T08:37:13.681Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "sourcing", + "id": "16df6970-b92a-4da7-98a3-b9f8041b5a67" + }] +} \ No newline at end of file diff --git a/scripts/insert-es-data.js b/scripts/insert-es-data.js new file mode 100644 index 00000000..09ac281b --- /dev/null +++ b/scripts/insert-es-data.js @@ -0,0 +1,92 @@ +/** + * Import data into ES. + */ +const config = require('config') +const _ = require('lodash') +const logger = require('../src/common/logger') +const helper = require('../src/common/helper') + +const jobs = require('./feed-data/jobs.json').result + +const jobCandidates = require('./feed-data/jobCandidates.json').result + +const resourceBookings = require('./feed-data/resourceBookings.json').result + +const insertESData = async () => { + logger.info('Inserting ES Data started!') + const esClient = helper.getESClient() + + await esClient.deleteByQuery({ + index: config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), + body: { + query: { + match_all: { } + } + } + }) + logger.info('Clear all ES Data on ' + config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')) + + await esClient.deleteByQuery({ + index: config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), + body: { + query: { + match_all: { } + } + } + }) + logger.info('Clear all ES Data on ' + config.get('esConfig.ES_INDEX_JOB_CANDIDATE')) + + await esClient.deleteByQuery({ + index: config.get('esConfig.ES_INDEX_JOB'), + body: { + query: { + match_all: { } + } + } + }) + logger.info('Clear all ES Data on ' + config.get('esConfig.ES_INDEX_JOB')) + + for (const job of jobs) { + await esClient.create({ + index: config.get('esConfig.ES_INDEX_JOB'), + id: job.id, + body: _.omit(job, 'id'), + refresh: 'true' + }) + } + logger.info('Insert ES Data on ' + config.get('esConfig.ES_INDEX_JOB')) + + for (const jobCandidate of jobCandidates) { + await esClient.create({ + index: config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), + id: jobCandidate.id, + body: _.omit(jobCandidate, 'id'), + refresh: 'true' + }) + } + logger.info('Insert ES Data on ' + config.get('esConfig.ES_INDEX_JOB_CANDIDATE')) + + for (const resourceBooking of resourceBookings) { + await esClient.create({ + index: config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), + id: resourceBooking.id, + body: _.omit(resourceBooking, 'id'), + refresh: 'true' + }) + } + logger.info('Insert ES Data on ' + config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')) +} + +if (!module.parent) { + insertESData().then(() => { + logger.info('Inserting ES Data successfully') + process.exit() + }).catch((e) => { + logger.logFullError(e) + process.exit(1) + }) +} + +module.exports = { + insertESData: insertESData +} diff --git a/src/bootstrap.js b/src/bootstrap.js index 56aa515f..4e6d87f7 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -17,7 +17,8 @@ function buildServices (dir) { if (stats.isDirectory()) { buildServices(curPath) } else if (path.extname(file) === '.js') { - logger.buildService(require(curPath)); // eslint-disable-line + const serviceName = path.basename(file, '.js') + logger.buildService(require(curPath), serviceName) } }) }) diff --git a/src/common/helper.js b/src/common/helper.js index a59cd11e..4df5766b 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -3,17 +3,43 @@ */ const querystring = require('querystring') +const AWS = require('aws-sdk') const config = require('config') const _ = require('lodash') const request = require('superagent') const elasticsearch = require('@elastic/elasticsearch') const errors = require('../common/errors') +const logger = require('./logger') +const busApi = require('@topcoder-platform/topcoder-bus-api-wrapper') + +const localLogger = { + debug: (message) => logger.debug({ component: 'helper', context: message.context, message: message.message }) +} + +AWS.config.region = config.esConfig.AWS_REGION const m2mAuth = require('tc-core-library-js').auth.m2m -//const m2m = m2mAuth(_.pick(config, ['AUTH0_URL', 'AUTH0_AUDIENCE', 'TOKEN_CACHE_TIME', 'AUTH0_PROXY_SERVER_URL'])) -const m2m = m2mAuth(_.pick(config, ['AUTH0_URL', 'AUTH0_AUDIENCE', 'AUTH0_CLIENT_ID','AUTH0_CLIENT_SECRET', 'AUTH0_PROXY_SERVER_URL'])) +// const m2m = m2mAuth(_.pick(config, ['AUTH0_URL', 'AUTH0_AUDIENCE', 'TOKEN_CACHE_TIME', 'AUTH0_PROXY_SERVER_URL'])) +const m2m = m2mAuth(_.pick(config, ['AUTH0_URL', 'AUTH0_AUDIENCE', 'AUTH0_CLIENT_ID', 'AUTH0_CLIENT_SECRET', 'AUTH0_PROXY_SERVER_URL'])) +let busApiClient + +/** + * Get bus api client. + * + * @returns {Object} the bus api client + */ +function getBusApiClient () { + if (busApiClient) { + return busApiClient + } + busApiClient = busApi({ + AUTH0_AUDIENCE: config.AUTH0_AUDIENCE_FOR_BUS_API, + ..._.pick(config, ['AUTH0_URL', 'TOKEN_CACHE_TIME', 'AUTH0_CLIENT_ID', 'AUTH0_CLIENT_SECRET', 'BUSAPI_URL', 'KAFKA_ERROR_TOPIC', 'AUTH0_PROXY_SERVER_URL']) + }) + return busApiClient +} // ES Client mapping const esClients = {} @@ -68,6 +94,9 @@ function getPageLink (req, page) { * @param {Object} result the operation result */ function setResHeaders (req, res, result) { + if (result.fromDb) { + return + } const totalPages = Math.ceil(result.total / result.perPage) if (result.page > 1) { res.set('X-Prev-Page', result.page - 1) @@ -140,10 +169,26 @@ async function isConnectMember (projectId, jwtToken) { * @return {Object} Elastic Host Client Instance */ function getESClient () { - const esHost = config.get('esConfig.HOST') - if (!esClients.client) { + if (esClients.client) { + return esClients.client + } + + const host = config.esConfig.HOST + const cloudId = config.esConfig.ELASTICCLOUD.id + if (cloudId) { + // Elastic Cloud configuration + esClients.client = new elasticsearch.Client({ + cloud: { + id: cloudId + }, + auth: { + username: config.esConfig.ELASTICCLOUD.username, + password: config.esConfig.ELASTICCLOUD.password + } + }) + } else { esClients.client = new elasticsearch.Client({ - node: esHost + node: host }) } return esClients.client @@ -154,7 +199,7 @@ function getESClient () { * @returns {Promise} */ const getM2Mtoken = async () => { - return m2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) + return await m2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) } /** @@ -196,6 +241,7 @@ async function getUserIds (userId) { .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') + localLogger.debug({ context: 'getUserIds', message: `response body: ${JSON.stringify(res.body)}` }) return res.body } @@ -207,16 +253,166 @@ async function getUserIds (userId) { async function getUserId (userId) { const ids = await getUserIds(userId) if (_.isEmpty(ids)) { - throw new errors.NotFoundError('user id not found') + throw new errors.NotFoundError(`userId: ${userId} "user" not found`) } return ids[0].id } +/** + * Send Kafka event message + * @params {String} topic the topic name + * @params {Object} payload the payload + */ +async function postEvent (topic, payload) { + logger.debug({ component: 'helper', context: 'postEvent', message: `Posting event to Kafka topic ${topic}, ${JSON.stringify(payload)}` }) + const client = getBusApiClient() + const message = { + topic, + originator: config.KAFKA_MESSAGE_ORIGINATOR, + timestamp: new Date().toISOString(), + 'mime-type': 'application/json', + payload + } + await client.postEvent(message) +} + +/** + * Test if an error is document missing exception + * + * @param {Object} err the err + * @returns {Boolean} the result + */ +function isDocumentMissingException (err) { + if (err.statusCode === 404) { + return true + } + return false +} + +/** + * Function to get projects + * @param {String} token the user request token + * @returns the request result + */ +async function getProjects (token) { + const url = `${config.TC_API}/projects?type=talent-as-a-service` + const res = await request + .get(url) + .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 => { + return _.pick(item, ['id', 'name']) + }) +} + +/** + * Function to get users + * @param {String} token the user request token + * @param {String} userId the user id + * @returns the request result + */ +async function getUserById (token, userId) { + const res = await request + .get(`${config.TC_API}/users/${userId}`) + .set('Authorization', token) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + localLogger.debug({ context: 'getUserById', message: `response body: ${JSON.stringify(res.body)}` }) + return _.pick(res.body, ['id', 'handle', 'firstName', 'lastName']) +} + +/** + * Function to get members + * @param {String} token the user request token + * @param {Array} handles the handle array + * @returns the request result + */ +async function getMembers (token, handles) { + const handlesStr = _.map(handles, handle => { + return '%22' + handle.toLowerCase() + '%22' + }).join(',') + const url = `${config.TC_API}/members?fields=userId,handleLower,photoURL&handlesLower=[${handlesStr}]` + + const res = await request + .get(url) + .set('Authorization', token) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + localLogger.debug({ context: 'getMembers', message: `response body: ${JSON.stringify(res.body)}` }) + return res.body +} + +/** + * Function to get project by id + * @param {String} token the user request token + * @param {Number} id project id + * @returns the request result + */ +async function getProjectById (token, id) { + const url = `${config.TC_API}/projects/${id}` + const res = await request + .get(url) + .set('Authorization', token) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + localLogger.debug({ context: 'getProjectById', message: `response body: ${JSON.stringify(res.body)}` }) + return _.pick(res.body, ['id', 'name']) +} + +/** + * Function to get skill by id + * @param {String} token the user request token + * @param {String} skillId the skill Id + * @returns the request result + */ +async function getSkillById (token, skillId) { + const res = await request + .get(`${config.TC_API}/skills/${skillId}`) + .set('Authorization', token) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + localLogger.debug({ context: 'getSkillById', message: `response body: ${JSON.stringify(res.body)}` }) + return _.pick(res.body, ['id', 'name']) +} + +/** + * Function to get user skills + * @param {String} token the user request token + * @param {String} userId user id + * @returns the request result + */ +async function getUserSkill (token, userId) { + const url = `${config.TC_API}/users/${userId}/skills` + const res = await request + .get(url) + .set('Authorization', token) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + localLogger.debug({ context: 'getUserSkill', message: `response body: ${JSON.stringify(res.body)}` }) + return _.map(res.body, item => { + return { + id: item.id, + name: item.skill.name + } + }) +} + module.exports = { autoWrapExpress, setResHeaders, clearObject, isConnectMember, getESClient, - getUserId + getUserId, + postEvent, + getBusApiClient, + isDocumentMissingException, + getProjects, + getUserById, + getMembers, + getProjectById, + getSkillById, + getUserSkill } diff --git a/src/common/logger.js b/src/common/logger.js index cd2f2d01..b2190695 100644 --- a/src/common/logger.js +++ b/src/common/logger.js @@ -10,10 +10,13 @@ const getParams = require('get-parameter-names') const winston = require('winston') const { - combine, timestamp, colorize, align, printf + combine, timestamp, colorize, printf } = winston.format -const basicFormat = printf(info => `${info.timestamp} ${info.level}: ${info.message}`) +const basicFormat = printf(info => { + const location = `${info.component}${info.context ? ` ${info.context}` : ''}` + return `[${info.timestamp}] ${location} ${info.level} : ${info.message}` +}) const transports = [] if (!config.DISABLE_LOGGING) { @@ -23,8 +26,11 @@ if (!config.DISABLE_LOGGING) { const logger = winston.createLogger({ transports, format: combine( + winston.format(info => { + info.level = info.level.toUpperCase() + return info + })(), colorize(), - align(), timestamp(), basicFormat ) @@ -33,15 +39,25 @@ const logger = winston.createLogger({ logger.config = config /** - * Log error details with signature - * @param err the error - * @param signature the signature + * Log error details + * @param {Object} err the error + * @param {Object} context contains extra info about errors */ -logger.logFullError = (err, signature) => { +logger.logFullError = (err, context = {}) => { if (!err) { return } - logger.error((signature ? (`${signature} : `) : '') + util.inspect(err)) + if (err.logged) { + return + } + const signature = context.signature ? `${context.signature} : ` : '' + let errMessage + if (err.response && err.response.error) { + errMessage = err.response.error.message + } else { + errMessage = err.message || util.inspect(err).split('\n')[0] + } + logger.error({ ..._.pick(context, ['component', 'context']), message: `${signature}${errMessage}` }) err.logged = true } @@ -79,29 +95,37 @@ const _combineObject = (params, arr) => { /** * Decorate all functions of a service and log debug information if DEBUG is enabled * @param {Object} service the service + * @param {String} serviceName the service name */ -logger.decorateWithLogging = (service) => { +logger.decorateWithLogging = (service, serviceName) => { if (logger.config.LOG_LEVEL !== 'debug') { return } _.each(service, (method, name) => { const params = method.params || getParams(method) service[name] = async function () { - logger.debug(`ENTER ${name}`) - logger.debug('input arguments') const args = Array.prototype.slice.call(arguments) - logger.debug(util.inspect(_sanitizeObject(_combineObject(params, args)))) + logger.debug({ + component: serviceName, + context: name, + message: `input arguments: ${util.inspect(_sanitizeObject(_combineObject(params, args)), { compact: true, breakLength: Infinity })}` + }) try { const result = await method.apply(this, arguments) - logger.debug(`EXIT ${name}`) - logger.debug('output arguments') - if (result !== null && result !== undefined) { - logger.debug(util.inspect(_sanitizeObject(result))) - } + logger.debug({ + component: serviceName, + context: name, + message: `output arguments: ${result !== null && result !== undefined + ? util.inspect(_sanitizeObject(result), { compact: true, breakLength: Infinity, depth: null }) + : undefined}` + }) return result - } catch (e) { - logger.logFullError(e, name) - throw e + } catch (err) { + logger.logFullError(err, { + component: serviceName, + context: name + }) + throw err } } }) @@ -140,10 +164,11 @@ logger.decorateWithValidators = function (service) { /** * Apply logger and validation decorators * @param {Object} service the service to wrap + * @param {String} serviceName the service name */ -logger.buildService = (service) => { +logger.buildService = (service, serviceName) => { logger.decorateWithValidators(service) - logger.decorateWithLogging(service) + logger.decorateWithLogging(service, serviceName) } module.exports = logger diff --git a/src/controllers/HealthCheckController.js b/src/controllers/HealthCheckController.js new file mode 100644 index 00000000..9ac62334 --- /dev/null +++ b/src/controllers/HealthCheckController.js @@ -0,0 +1,37 @@ +/** + * Controller for health check endpoint + */ +const models = require('../models') +const config = require('config') +const logger = require('../common/logger') + +// the topcoder-healthcheck-dropin library returns checksRun count, +// here it follows that to return such count +let checksRun = 0 + +/** + * Check health of the DB + * @param {Object} req the request + * @param {Object} res the response + */ +async function checkHealth (req, res) { + checksRun += 1 + const conn = new models.Sequelize(config.get('DATABASE_URL'), { + logging: false + }) + await conn + .authenticate() + .then(() => { + logger.info({ component: 'HealthCheckController', context: 'checkHealth', message: 'Connection has been established successfully.' }) + }) + .catch(err => { + logger.logFullError(err, { component: 'HealthCheckController', context: 'checkHealth' }) + res.status(503) + }) + await conn.close() + res.send({ checksRun }) +} + +module.exports = { + checkHealth +} diff --git a/src/controllers/JobCandidateController.js b/src/controllers/JobCandidateController.js index 86b38be6..45810f39 100644 --- a/src/controllers/JobCandidateController.js +++ b/src/controllers/JobCandidateController.js @@ -11,7 +11,7 @@ const helper = require('../common/helper') * @param res the response */ async function getJobCandidate (req, res) { - res.send(await service.getJobCandidate(req.params.id)) + res.send(await service.getJobCandidate(req.params.id, req.query.fromDb)) } /** diff --git a/src/controllers/JobController.js b/src/controllers/JobController.js index dfb319ed..6e71b108 100644 --- a/src/controllers/JobController.js +++ b/src/controllers/JobController.js @@ -11,7 +11,7 @@ const helper = require('../common/helper') * @param res the response */ async function getJob (req, res) { - res.send(await service.getJob(req.params.id)) + res.send(await service.getJob(req.params.id, req.query.fromDb)) } /** diff --git a/src/controllers/ResourceBookingController.js b/src/controllers/ResourceBookingController.js index 06c57ae3..ed846a91 100644 --- a/src/controllers/ResourceBookingController.js +++ b/src/controllers/ResourceBookingController.js @@ -11,7 +11,7 @@ const helper = require('../common/helper') * @param res the response */ async function getResourceBooking (req, res) { - res.send(await service.getResourceBooking(req.authUser, req.params.id)) + res.send(await service.getResourceBooking(req.authUser, req.params.id, req.query.fromDb)) } /** diff --git a/src/controllers/TeamController.js b/src/controllers/TeamController.js new file mode 100644 index 00000000..f29e11e1 --- /dev/null +++ b/src/controllers/TeamController.js @@ -0,0 +1,37 @@ +/** + * Controller for TaaS teams endpoints + */ +const service = require('../services/TeamService') + +/** + * Search teams + * @param req the request + * @param res the response + */ +async function searchTeams (req, res) { + res.send(await service.searchTeams(req.authUser)) +} + +/** + * Get team + * @param req the request + * @param res the response + */ +async function getTeam (req, res) { + res.send(await service.getTeam(req.authUser, req.params.id)) +} + +/** + * Get team job + * @param req the request + * @param res the response + */ +async function getTeamJob (req, res) { + res.send(await service.getTeamJob(req.authUser, req.params.id, req.params.jobId)) +} + +module.exports = { + searchTeams, + getTeam, + getTeamJob +} diff --git a/src/init-db.js b/src/init-db.js index 3b838bda..95b608a8 100644 --- a/src/init-db.js +++ b/src/init-db.js @@ -6,17 +6,19 @@ const models = require('./models') const logger = require('./common/logger') const initDB = async () => { - // await models.sequelize.dropSchema(config.DB_SCHEMA_NAME) + if (process.argv[2] === 'force') { + await models.sequelize.dropSchema(config.DB_SCHEMA_NAME) + } await models.sequelize.createSchema(config.DB_SCHEMA_NAME) await models.sequelize.sync({ force: true }) } if (!module.parent) { initDB().then(() => { - logger.info('Database synced successfully') + logger.info({ component: 'init-db', message: 'Database synced successfully' }) process.exit() }).catch((e) => { - logger.logFullError(e) + logger.logFullError(e, { component: 'init-db' }) process.exit(1) }) } diff --git a/src/models/Job.js b/src/models/Job.js index dc012350..dcb11a3d 100644 --- a/src/models/Job.js +++ b/src/models/Job.js @@ -46,7 +46,7 @@ module.exports = (sequelize) => { } const job = await Job.findOne(criteria) if (!job) { - throw new errors.NotFoundError(`Job with id: ${id} doesn't exists.`) + throw new errors.NotFoundError(`id: ${id} "Job" doesn't exists.`) } return job } diff --git a/src/models/JobCandidate.js b/src/models/JobCandidate.js index 74a7cdbd..e6761eaa 100644 --- a/src/models/JobCandidate.js +++ b/src/models/JobCandidate.js @@ -29,7 +29,7 @@ module.exports = (sequelize) => { } }) if (!jobCandidate) { - throw new errors.NotFoundError(`JobCandidate with id: ${id} doesn't exists.`) + throw new errors.NotFoundError(`id: ${id} "JobCandidate" doesn't exists.`) } return jobCandidate } diff --git a/src/models/ResourceBooking.js b/src/models/ResourceBooking.js index e0a0b0c8..37ff8b6c 100644 --- a/src/models/ResourceBooking.js +++ b/src/models/ResourceBooking.js @@ -28,7 +28,7 @@ module.exports = (sequelize) => { } }) if (!resourceBooking) { - throw new errors.NotFoundError(`ResourceBooking with id: ${id} doesn't exists.`) + throw new errors.NotFoundError(`id: ${id} "ResourceBooking" doesn't exists.`) } return resourceBooking } diff --git a/src/routes/HealthCheckRoutes.js b/src/routes/HealthCheckRoutes.js new file mode 100644 index 00000000..9e0f4f55 --- /dev/null +++ b/src/routes/HealthCheckRoutes.js @@ -0,0 +1,12 @@ +/** + * Contains healthcheck routes + */ + +module.exports = { + '/health': { + get: { + controller: 'HealthCheckController', + method: 'checkHealth' + } + } +} diff --git a/src/routes/TeamRoutes.js b/src/routes/TeamRoutes.js new file mode 100644 index 00000000..30dd5bc1 --- /dev/null +++ b/src/routes/TeamRoutes.js @@ -0,0 +1,27 @@ +/** + * Contains taas team routes + */ + +module.exports = { + '/taas-teams': { + get: { + controller: 'TeamController', + method: 'searchTeams', + auth: 'jwt' + } + }, + '/taas-teams/:id': { + get: { + controller: 'TeamController', + method: 'getTeam', + auth: 'jwt' + } + }, + '/taas-teams/:id/jobs/:jobId': { + get: { + controller: 'TeamController', + method: 'getTeamJob', + auth: 'jwt' + } + } +} diff --git a/src/routes/index.js b/src/routes/index.js index dccb8b74..0c0f5054 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -2,13 +2,17 @@ * Defines the API routes */ -const _ = require('lodash') -const JobRoutes = require('./JobRoutes') -const JobCandidateRoutes = require('./JobCandidateRoutes') -const ResourceBookingRoutes = require('./ResourceBookingRoutes') - -module.exports = _.extend({}, - JobRoutes, - JobCandidateRoutes, - ResourceBookingRoutes -) +const fs = require('fs') +const path = require('path') + +const modules = {} + +fs + .readdirSync(__dirname) + .filter(file => (file.indexOf('.') !== 0) && (file !== path.basename(module.filename)) && (file.slice(-3) === '.js')) + .forEach((file) => { + const moduleName = file.slice(0, -3) + modules[moduleName] = require(path.join(__dirname, file)) + }) + +module.exports = Object.assign({}, ...Object.values(modules)) diff --git a/src/services/JobCandidateService.js b/src/services/JobCandidateService.js index 1dabf5af..d768cf84 100644 --- a/src/services/JobCandidateService.js +++ b/src/services/JobCandidateService.js @@ -5,6 +5,7 @@ const _ = require('lodash') const Joi = require('joi') const config = require('config') +const { Op } = require('sequelize') const { v4: uuid } = require('uuid') const helper = require('../common/helper') const logger = require('../common/logger') @@ -12,19 +13,38 @@ const errors = require('../common/errors') const models = require('../models') const JobCandidate = models.JobCandidate +const esClient = helper.getESClient() /** * Get jobCandidate by id * @param {String} id the jobCandidate id + * @param {Boolean} fromDb flag if query db for data or not * @returns {Object} the jobCandidate */ -async function getJobCandidate (id) { +async function getJobCandidate (id, fromDb = false) { + if (!fromDb) { + try { + const jobCandidate = await esClient.get({ + index: config.esConfig.ES_INDEX_JOB_CANDIDATE, + id + }) + const jobCandidateRecord = { id: jobCandidate.body._id, ...jobCandidate.body._source } + return jobCandidateRecord + } catch (err) { + if (helper.isDocumentMissingException(err)) { + throw new errors.NotFoundError(`id: ${id} "JobCandidate" not found`) + } + logger.logFullError(err, { component: 'JobCandidateService', context: 'getJobCandidate' }) + } + } + logger.info({ component: 'JobCandidateService', context: 'getJobCandidate', message: 'try to query db for data' }) const jobCandidate = await JobCandidate.findById(id) return helper.clearObject(jobCandidate.dataValues) } getJobCandidate.schema = Joi.object().keys({ - id: Joi.string().guid().required() + id: Joi.string().guid().required(), + fromDb: Joi.boolean() }).required() /** @@ -39,15 +59,8 @@ async function createJobCandidate (currentUser, jobCandidate) { jobCandidate.createdBy = await helper.getUserId(currentUser.userId) jobCandidate.status = 'open' - const esClient = helper.getESClient() - await esClient.create({ - index: config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), - id: jobCandidate.id, - body: _.omit(jobCandidate, 'id'), - refresh: 'true' // refresh ES so that it is visible for read operations instantly - }) - const created = await JobCandidate.create(jobCandidate) + await helper.postEvent(config.TAAS_JOB_CANDIDATE_CREATE_TOPIC, jobCandidate) return helper.clearObject(created.dataValues) } @@ -80,17 +93,8 @@ async function updateJobCandidate (currentUser, id, data) { data.updatedAt = new Date() data.updatedBy = await helper.getUserId(currentUser.userId) - const esClient = helper.getESClient() - await esClient.update({ - index: config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), - id, - body: { - doc: data - }, - refresh: 'true' // refresh ES so that it is visible for read operations instantly - }) - await jobCandidate.update(data) + await helper.postEvent(config.TAAS_JOB_CANDIDATE_UPDATE_TOPIC, { id, ...data }) const result = helper.clearObject(_.assign(jobCandidate.dataValues, data)) return result } @@ -145,17 +149,9 @@ async function deleteJobCandidate (currentUser, id) { throw new errors.ForbiddenError('You are not allowed to perform this action!') } - const esClient = helper.getESClient() - await esClient.delete({ - index: config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), - id, - refresh: 'true' // refresh ES so that it is visible for read operations instantly - }, { - ignore: [404] - }) - const jobCandidate = await JobCandidate.findById(id) await jobCandidate.update({ deletedAt: new Date() }) + await helper.postEvent(config.TAAS_JOB_CANDIDATE_DELETE_TOPIC, { id }) } deleteJobCandidate.schema = Joi.object().keys({ @@ -172,53 +168,76 @@ async function searchJobCandidates (criteria) { const page = criteria.page > 0 ? criteria.page : 1 const perPage = criteria.perPage > 0 ? criteria.perPage : 20 if (!criteria.sortBy) { - criteria.sortBy = '_id' - } - if (criteria.sortBy === 'id') { - criteria.sortBy = '_id' + criteria.sortBy = 'id' } if (!criteria.sortOrder) { criteria.sortOrder = 'desc' } - const sort = [{ [criteria.sortBy]: { order: criteria.sortOrder } }] - - const esQuery = { - index: config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), - body: { - query: { - bool: { - must: [] - } - }, - from: (page - 1) * perPage, - size: perPage, - sort + try { + const sort = [{ [criteria.sortBy === 'id' ? '_id' : criteria.sortBy]: { order: criteria.sortOrder } }] + + const esQuery = { + index: config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), + body: { + query: { + bool: { + must: [] + } + }, + from: (page - 1) * perPage, + size: perPage, + sort + } } - } - _.each(_.pick(criteria, ['jobId', 'userId', 'status']), (value, key) => { - esQuery.body.query.bool.must.push({ - term: { - [key]: { - value + _.each(_.pick(criteria, ['jobId', 'userId', 'status']), (value, key) => { + esQuery.body.query.bool.must.push({ + term: { + [key]: { + value + } } - } + }) }) - }) - logger.debug(`Query: ${JSON.stringify(esQuery)}`) + logger.debug({ component: 'JobCandidateService', context: 'searchJobCandidates', message: `Query: ${JSON.stringify(esQuery)}` }) - const esClient = helper.getESClient() - const { body } = await esClient.search(esQuery) + const { body } = await esClient.search(esQuery) + return { + total: body.hits.total.value, + page, + perPage, + result: _.map(body.hits.hits, (hit) => { + const obj = _.cloneDeep(hit._source) + obj.id = hit._id + return obj + }) + } + } catch (err) { + logger.logFullError(err, { component: 'JobCandidateService', context: 'searchJobCandidates' }) + } + logger.info({ component: 'JobCandidateService', context: 'searchJobCandidates', message: 'fallback to DB query' }) + const filter = { + [Op.and]: [{ deletedAt: null }] + } + _.each(_.pick(criteria, ['jobId', 'userId', 'status']), (value, key) => { + filter[Op.and].push({ [key]: value }) + }) + const jobCandidates = await JobCandidate.findAll({ + where: filter, + attributes: { + exclude: ['deletedAt'] + }, + offset: ((page - 1) * perPage), + limit: perPage, + order: [[criteria.sortBy, criteria.sortOrder]] + }) return { - total: body.hits.total.value, + fromDb: true, + total: jobCandidates.length, page, perPage, - result: _.map(body.hits.hits, (hit) => { - const obj = _.cloneDeep(hit._source) - obj.id = hit._id - return obj - }) + result: _.map(jobCandidates, jobCandidate => helper.clearObject(jobCandidate.dataValues)) } } diff --git a/src/services/JobService.js b/src/services/JobService.js index 4ac6d80b..f4953782 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -5,6 +5,7 @@ const _ = require('lodash') const Joi = require('joi') const config = require('config') +const { Op } = require('sequelize') const { v4: uuid } = require('uuid') const helper = require('../common/helper') const logger = require('../common/logger') @@ -12,20 +13,75 @@ const errors = require('../common/errors') const models = require('../models') const Job = models.Job +const esClient = helper.getESClient() + +/** + * populate candidates for a job. + * + * @param {String} jobId the job id + * @returns {Array} the list of candidates + */ +async function _getJobCandidates (jobId) { + const { body } = await esClient.search({ + index: config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), + body: { + query: { + term: { + jobId: { + value: jobId + } + } + } + } + }) + + if (body.hits.total.value === 0) { + return [] + } + const candidates = _.map(body.hits.hits, (hit) => { + const candidateRecord = _.cloneDeep(hit._source) + candidateRecord.id = hit._id + return candidateRecord + }) + return candidates +} /** * Get job by id * @param {String} id the job id + * @param {Boolean} fromDb flag if query db for data or not * @returns {Object} the job */ -async function getJob (id) { +async function getJob (id, fromDb = false) { + if (!fromDb) { + try { + const job = await esClient.get({ + index: config.esConfig.ES_INDEX_JOB, + id + }) + const jobId = job.body._id + const jobRecord = { id: jobId, ...job.body._source } + const candidates = await _getJobCandidates(jobId) + if (candidates.length) { + jobRecord.candidates = candidates + } + return jobRecord + } catch (err) { + if (helper.isDocumentMissingException(err)) { + throw new errors.NotFoundError(`id: ${id} "Job" not found`) + } + logger.logFullError(err, { component: 'JobService', context: 'getJob' }) + } + } + logger.info({ component: 'JobService', context: 'getJob', message: 'try to query db for data' }) const job = await Job.findById(id, true) job.dataValues.candidates = _.map(job.dataValues.candidates, (c) => helper.clearObject(c.dataValues)) return helper.clearObject(job.dataValues) } getJob.schema = Joi.object().keys({ - id: Joi.string().guid().required() + id: Joi.string().guid().required(), + fromDb: Joi.boolean() }).required() /** @@ -46,15 +102,8 @@ async function createJob (currentUser, job) { job.createdBy = await helper.getUserId(currentUser.userId) job.status = 'sourcing' - const esClient = helper.getESClient() - await esClient.create({ - index: config.get('esConfig.ES_INDEX_JOB'), - id: job.id, - body: _.omit(job, 'id'), - refresh: 'true' // refresh ES so that it is visible for read operations instantly - }) - const created = await Job.create(job) + await helper.postEvent(config.TAAS_JOB_CREATE_TOPIC, job) return helper.clearObject(created.dataValues) } @@ -92,17 +141,8 @@ async function updateJob (currentUser, id, data) { data.updatedAt = new Date() data.updatedBy = await helper.getUserId(currentUser.userId) - const esClient = helper.getESClient() - await esClient.update({ - index: config.get('esConfig.ES_INDEX_JOB'), - id, - body: { - doc: data - }, - refresh: 'true' // refresh ES so that it is visible for read operations instantly - }) - await job.update(data) + await helper.postEvent(config.TAAS_JOB_UPDATE_TOPIC, { id, ...data }) job = await Job.findById(id, true) job.dataValues.candidates = _.map(job.dataValues.candidates, (c) => helper.clearObject(c.dataValues)) return helper.clearObject(job.dataValues) @@ -172,16 +212,9 @@ async function deleteJob (currentUser, id) { throw new errors.ForbiddenError('You are not allowed to perform this action!') } - const esClient = helper.getESClient() - await esClient.delete({ - index: config.get('esConfig.ES_INDEX_JOB'), - id, - refresh: 'true' // refresh ES so that it is visible for read operations instantly - }, { - ignore: [404] - }) const job = await Job.findById(id) await job.update({ deletedAt: new Date() }) + await helper.postEvent(config.TAAS_JOB_DELETE_TOPIC, { id }) } deleteJob.schema = Joi.object().keys({ @@ -198,94 +231,128 @@ async function searchJobs (criteria) { const page = criteria.page > 0 ? criteria.page : 1 const perPage = criteria.perPage > 0 ? criteria.perPage : 20 if (!criteria.sortBy) { - criteria.sortBy = '_id' - } - if (criteria.sortBy === 'id') { - criteria.sortBy = '_id' + criteria.sortBy = 'id' } if (!criteria.sortOrder) { criteria.sortOrder = 'desc' } - const sort = [{ [criteria.sortBy]: { order: criteria.sortOrder } }] + try { + const sort = [{ [criteria.sortBy === 'id' ? '_id' : criteria.sortBy]: { order: criteria.sortOrder } }] - const esQuery = { - index: config.get('esConfig.ES_INDEX_JOB'), - body: { - query: { - bool: { - must: [] - } - }, - from: (page - 1) * perPage, - size: perPage, - sort + const esQuery = { + index: config.get('esConfig.ES_INDEX_JOB'), + body: { + query: { + bool: { + must: [] + } + }, + from: (page - 1) * perPage, + size: perPage, + sort + } } - } - _.each(_.pick(criteria, ['projectId', 'externalId', 'description', 'startDate', 'endDate', 'resourceType', 'skill', 'rateType', 'status']), (value, key) => { - let must - if (key === 'description') { - must = { - match: { - [key]: { - query: value + _.each(_.pick(criteria, ['projectId', 'externalId', 'description', 'startDate', 'endDate', 'resourceType', 'skill', 'rateType', 'status']), (value, key) => { + let must + if (key === 'description') { + must = { + match: { + [key]: { + query: value + } } } - } - } else if (key === 'skill') { - must = { - terms: { - skills: [value] - } - } - } else { - must = { - term: { - [key]: { - value + } else if (key === 'skill') { + must = { + terms: { + skills: [value] } } - } - } - esQuery.body.query.bool.must.push(must) - }) - logger.debug(`Query: ${JSON.stringify(esQuery)}`) - - const esClient = helper.getESClient() - const { body } = await esClient.search(esQuery) - const result = await Promise.all(_.map(body.hits.hits, async (hit) => { - const jobRecord = _.cloneDeep(hit._source) - jobRecord.id = hit._id - - const { body } = await esClient.search({ - index: config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), - body: { - query: { + } else { + must = { term: { - jobId: { - value: jobRecord.id + [key]: { + value } } } } + esQuery.body.query.bool.must.push(must) }) - - if (body.hits.total.value > 0) { - const candidates = _.map(body.hits.hits, (hit) => { - const candidateRecord = _.cloneDeep(hit._source) - candidateRecord.id = hit._id - return candidateRecord - }) - jobRecord.candidates = candidates + // If criteria contains projectIds, filter projectId with this value + if (criteria.projectIds) { + esQuery.body.query.bool.filter = [{ + terms: { + projectId: criteria.projectIds + } + }] } - return jobRecord - })) + logger.debug({ component: 'JobService', context: 'searchJobs', message: `Query: ${JSON.stringify(esQuery)}` }) + const { body } = await esClient.search(esQuery) + const result = await Promise.all(_.map(body.hits.hits, async (hit) => { + const jobRecord = _.cloneDeep(hit._source) + jobRecord.id = hit._id + const candidates = await _getJobCandidates(jobRecord.id) + if (candidates.length) { + jobRecord.candidates = candidates + } + return jobRecord + })) + + return { + total: body.hits.total.value, + page, + perPage, + result + } + } catch (err) { + logger.logFullError(err, { component: 'JobService', context: 'searchJobs' }) + } + logger.info({ component: 'JobService', context: 'searchJobs', message: 'fallback to DB query' }) + const filter = { + [Op.and]: [{ deletedAt: null }] + } + _.each(_.pick(criteria, ['projectId', 'externalId', 'startDate', 'endDate', 'resourceType', 'rateType', 'status']), (value, key) => { + filter[Op.and].push({ [key]: value }) + }) + if (criteria.description) { + filter.description = { + [Op.like]: `%${criteria.description}%` + } + } + if (criteria.skills) { + filter.skills = { + [Op.contains]: [criteria.skills] + } + } + const jobs = await Job.findAll({ + where: filter, + attributes: { + exclude: ['deletedAt'] + }, + offset: ((page - 1) * perPage), + limit: perPage, + order: [[criteria.sortBy, criteria.sortOrder]], + include: [{ + model: models.JobCandidate, + as: 'candidates', + where: { + deletedAt: null + }, + required: false, + attributes: { + exclude: ['deletedAt'] + } + }] + }) return { - total: body.hits.total.value, + fromDb: true, + total: jobs.length, page, perPage, - result + result: _.map(jobs, job => helper.clearObject(job.dataValues)) } } @@ -303,7 +370,8 @@ searchJobs.schema = Joi.object().keys({ resourceType: Joi.string(), skill: Joi.string().uuid(), rateType: Joi.rateType(), - status: Joi.jobStatus() + status: Joi.jobStatus(), + projectIds: Joi.array().items(Joi.number().integer()).single() }).required() }).required() diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index 9e502600..fc234402 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -5,6 +5,7 @@ const _ = require('lodash') const Joi = require('joi') const config = require('config') +const { Op } = require('sequelize') const { v4: uuid } = require('uuid') const helper = require('../common/helper') const logger = require('../common/logger') @@ -12,27 +13,56 @@ const errors = require('../common/errors') const models = require('../models') const ResourceBooking = models.ResourceBooking +const esClient = helper.getESClient() /** - * Get resourceBooking by id + * filter fields of resource booking by user role. * @param {Object} currentUser the user who perform this operation. - * @param {String} id the resourceBooking id + * @param {Object} resourceBooking the resourceBooking with all fields * @returns {Object} the resourceBooking */ -async function getResourceBooking (currentUser, id) { - const resourceBooking = await ResourceBooking.findById(id) +async function _getResourceBookingFilteringFields (currentUser, resourceBooking) { if (currentUser.isBookingManager) { - return helper.clearObject(resourceBooking.dataValues) - } else if (await helper.isConnectMember(resourceBooking.dataValues.projectId, currentUser.jwtToken)) { - return _.omit(helper.clearObject(resourceBooking.dataValues), 'memberRate') + return helper.clearObject(resourceBooking) + } else if (await helper.isConnectMember(resourceBooking.projectId, currentUser.jwtToken)) { + return _.omit(helper.clearObject(resourceBooking), 'memberRate') } else { - return _.omit(helper.clearObject(resourceBooking.dataValues), 'customerRate') + return _.omit(helper.clearObject(resourceBooking), 'customerRate') + } +} + +/** + * Get resourceBooking by id + * @param {Object} currentUser the user who perform this operation. + * @param {String} id the resourceBooking id + * @param {Boolean} fromDb flag if query db for data or not + * @returns {Object} the resourceBooking + */ +async function getResourceBooking (currentUser, id, fromDb = false) { + if (!fromDb) { + try { + const resourceBooking = await esClient.get({ + index: config.esConfig.ES_INDEX_RESOURCE_BOOKING, + id + }) + const resourceBookingRecord = { id: resourceBooking.body._id, ...resourceBooking.body._source } + return _getResourceBookingFilteringFields(currentUser, resourceBookingRecord) + } catch (err) { + if (helper.isDocumentMissingException(err)) { + throw new errors.NotFoundError(`id: ${id} "ResourceBooking" not found`) + } + logger.logFullError(err, { component: 'ResourceBookingService', context: 'getResourceBooking' }) + } } + logger.info({ component: 'ResourceBookingService', context: 'getResourceBooking', message: 'try to query db for data' }) + const resourceBooking = await ResourceBooking.findById(id) + return _getResourceBookingFilteringFields(currentUser, resourceBooking.dataValues) } getResourceBooking.schema = Joi.object().keys({ currentUser: Joi.object().required(), - id: Joi.string().guid().required() + id: Joi.string().guid().required(), + fromDb: Joi.boolean() }).required() /** @@ -53,15 +83,8 @@ async function createResourceBooking (currentUser, resourceBooking) { resourceBooking.createdBy = await helper.getUserId(currentUser.userId) resourceBooking.status = 'sourcing' - const esClient = helper.getESClient() - await esClient.create({ - index: config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), - id: resourceBooking.id, - body: _.omit(resourceBooking, 'id'), - refresh: 'true' // refresh ES so that it is visible for read operations instantly - }) - const created = await ResourceBooking.create(resourceBooking) + await helper.postEvent(config.TAAS_RESOURCE_BOOKING_CREATE_TOPIC, resourceBooking) return helper.clearObject(created.dataValues) } @@ -97,17 +120,8 @@ async function updateResourceBooking (currentUser, id, data) { data.updatedAt = new Date() data.updatedBy = await helper.getUserId(currentUser.userId) - const esClient = helper.getESClient() - await esClient.update({ - index: config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), - id, - body: { - doc: data - }, - refresh: 'true' // refresh ES so that it is visible for read operations instantly - }) - await resourceBooking.update(data) + await helper.postEvent(config.TAAS_RESOURCE_BOOKING_UPDATE_TOPIC, { id, ...data }) const result = helper.clearObject(_.assign(resourceBooking.dataValues, data)) return result } @@ -173,17 +187,9 @@ async function deleteResourceBooking (currentUser, id) { throw new errors.ForbiddenError('You are not allowed to perform this action!') } - const esClient = helper.getESClient() - await esClient.delete({ - index: config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), - id, - refresh: 'true' // refresh ES so that it is visible for read operations instantly - }, { - ignore: [404] - }) - const resourceBooking = await ResourceBooking.findById(id) await resourceBooking.update({ deletedAt: new Date() }) + await helper.postEvent(config.TAAS_RESOURCE_BOOKING_DELETE_TOPIC, { id }) } deleteResourceBooking.schema = Joi.object().keys({ @@ -201,54 +207,76 @@ async function searchResourceBookings (criteria) { const perPage = criteria.perPage > 0 ? criteria.perPage : 20 if (!criteria.sortBy) { - criteria.sortBy = '_id' + criteria.sortBy = 'id' } - if (criteria.sortBy === 'id') { - criteria.sortBy = '_id' - } - if (!criteria.sortOrder) { criteria.sortOrder = 'desc' } - const sort = [{ [criteria.sortBy]: { order: criteria.sortOrder } }] + try { + const sort = [{ [criteria.sortBy === 'id' ? '_id' : criteria.sortBy]: { order: criteria.sortOrder } }] - const esQuery = { - index: config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), - body: { - query: { - bool: { - must: [] - } - }, - from: (page - 1) * perPage, - size: perPage, - sort + const esQuery = { + index: config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), + body: { + query: { + bool: { + must: [] + } + }, + from: (page - 1) * perPage, + size: perPage, + sort + } } - } - _.each(_.pick(criteria, ['status', 'startDate', 'endDate', 'rateType']), (value, key) => { - esQuery.body.query.bool.must.push({ - term: { - [key]: { - value + _.each(_.pick(criteria, ['status', 'startDate', 'endDate', 'rateType', 'projectId']), (value, key) => { + esQuery.body.query.bool.must.push({ + term: { + [key]: { + value + } } - } + }) }) - }) - logger.debug(`Query: ${JSON.stringify(esQuery)}`) + logger.debug({ component: 'ResourceBookingService', context: 'searchResourceBookings', message: `Query: ${JSON.stringify(esQuery)}` }) - const esClient = helper.getESClient() - const { body } = await esClient.search(esQuery) + const { body } = await esClient.search(esQuery) + return { + total: body.hits.total.value, + page, + perPage, + result: _.map(body.hits.hits, (hit) => { + const obj = _.cloneDeep(hit._source) + obj.id = hit._id + return obj + }) + } + } catch (err) { + logger.logFullError(err, { component: 'ResourceBookingService', context: 'searchResourceBookings' }) + } + logger.info({ component: 'ResourceBookingService', context: 'searchResourceBookings', message: 'fallback to DB query' }) + const filter = { + [Op.and]: [{ deletedAt: null }] + } + _.each(_.pick(criteria, ['status', 'startDate', 'endDate', 'rateType']), (value, key) => { + filter[Op.and].push({ [key]: value }) + }) + const resourceBookings = await ResourceBooking.findAll({ + where: filter, + attributes: { + exclude: ['deletedAt'] + }, + offset: ((page - 1) * perPage), + limit: perPage, + order: [[criteria.sortBy, criteria.sortOrder]] + }) return { - total: body.hits.total.value, + fromDb: true, + total: resourceBookings.length, page, perPage, - result: _.map(body.hits.hits, (hit) => { - const obj = _.cloneDeep(hit._source) - obj.id = hit._id - return obj - }) + result: _.map(resourceBookings, resourceBooking => helper.clearObject(resourceBooking.dataValues)) } } @@ -261,7 +289,8 @@ searchResourceBookings.schema = Joi.object().keys({ status: Joi.jobStatus(), startDate: Joi.date(), endDate: Joi.date(), - rateType: Joi.rateType() + rateType: Joi.rateType(), + projectId: Joi.number().integer() }).required() }).required() diff --git a/src/services/TeamService.js b/src/services/TeamService.js new file mode 100644 index 00000000..0f5c2b89 --- /dev/null +++ b/src/services/TeamService.js @@ -0,0 +1,298 @@ +/** + * This service provides operations of Job. + */ + +const _ = require('lodash') +const Joi = require('joi') +const helper = require('../common/helper') +const logger = require('../common/logger') +const JobService = require('./JobService') +const ResourceBookingService = require('./ResourceBookingService') + +/** + * Function to get assigned resource booking + * @param {String} projectId project id + * @returns the request result + */ +async function _getAssignedResourceBooking (projectId) { + const criteria = { status: 'assigned' } + if (projectId) { + criteria.projectId = projectId + } + const { result } = await ResourceBookingService.searchResourceBookings(criteria) + return result +} + +/** + * Function to get jobs by projectIds + * @param {Array} projectIds project ids + * @returns the request result + */ +async function _getJobsByProjectIds (projectIds) { + const { result } = await JobService.searchJobs({ projectIds }) + return result +} + +/** + * List teams + * @param {Object} currentUser the user who perform this operation + * @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) +} + +searchTeams.schema = Joi.object().keys({ + currentUser: Joi.object().required() +}).required() + +/** + * Get team details + * @param {Object} currentUser the user who perform this operation + * @param {Object} projects the projects + * @param {Object} isSearch the flag whether for search function + * @returns {Object} the search result + */ +async function getTeamDetail (currentUser, projects, isSearch = true) { + const projectIds = _.map(projects, 'id') + // Get resourceBookings from taas api + const resourceBookings = await _getAssignedResourceBooking() + // Get jobs from taas api + const jobs = await _getJobsByProjectIds(projectIds) + + // Get first week day and last week day + const curr = new Date() + curr.setHours(0, 0, 0, 0) + const first = curr.getDate() - curr.getDay() + const last = first + 6 + + const firstDay = new Date(curr.setDate(first)) + const lastDay = new Date(curr.setDate(last)) + + const result = [] + for (const project of projects) { + const rbs = _.filter(resourceBookings, { projectId: project.id }) + const res = _.clone(project) + res.weeklyCount = 0 + res.resources = [] + + if (rbs && rbs.length > 0) { + // Get minimal start date and maximal end date + const startDates = [] + const endDates = [] + for (const rbsItem of rbs) { + if (rbsItem.startDate) { + startDates.push(new Date(rbsItem.startDate)) + } + if (rbsItem.endDate) { + endDates.push(new Date(rbsItem.endDate)) + } + } + + if (startDates && startDates.length > 0) { + res.startDate = _.min(startDates) + } + if (endDates && endDates.length > 0) { + res.endDate = _.max(endDates) + } + + // Count weekly rate + for (const item of rbs) { + const startDate = new Date(item.startDate) + const endDate = new Date(item.endDate) + + if ((!item.startDate || (startDate <= firstDay && startDate < lastDay)) && + (!item.endDate || (endDate >= lastDay && endDate > firstDay))) { + res.weeklyCount += item.customerRate + } + } + + const usersPromises = [] + _.map(rbs, (rb) => { usersPromises.push(helper.getUserById(currentUser.jwtToken, rb.userId)) }) + const userInfos = await Promise.all(usersPromises) + if (userInfos && userInfos.length > 0) { + res.resources = userInfos + + const userHandles = _.map(userInfos, 'handle') + // Get user photo from /v5/members + const members = await helper.getMembers(currentUser.jwtToken, userHandles) + + for (const item of res.resources) { + const findMember = _.find(members, { handleLower: item.handle.toLowerCase() }) + if (findMember && findMember.photoURL) { + item.photo_url = findMember.photoURL + } + + if (!isSearch) { + // If call function is not search, add job field + const findRbs = _.find(rbs, { userId: item.id }) + if (findRbs) { + item.customerRate = findRbs.customerRate + const job = _.find(jobs, { id: findRbs.jobId }) + if (job) { + item.job = { + id: job.id, + name: job.description + } + } + } + } + } + } + } + + const jobsTmp = _.filter(jobs, { projectId: project.id }) + if (jobsTmp && jobsTmp.length > 0) { + if (isSearch) { + // Count total positions + res.totalPositions = 0 + for (const item of jobsTmp) { + res.totalPositions += item.numPositions + } + } else { + res.jobs = _.map(jobsTmp, job => { + return _.pick(job, ['id', 'description', 'startDate', 'endDate', 'numPositions', 'rateType', 'skills', 'customerRate', 'status']) + }) + } + } + result.push(res) + } + + return result +} + +/** + * Get team by id + * @param {Object} currentUser the user who perform this operation + * @param {String} id the job id + * @returns {Object} the team + */ +async function getTeam (currentUser, id) { + // Get users from /v5/projects + const project = await helper.getProjectById(currentUser.jwtToken, id) + + const result = await getTeamDetail(currentUser, [project], false) + + const teamDetail = result[0] + + // add job skills for result + let jobSkills = [] + if (teamDetail && teamDetail.jobs) { + for (const job of teamDetail.jobs) { + if (job.skills) { + const usersPromises = [] + _.map(job.skills, (skillId) => { usersPromises.push(helper.getSkillById(currentUser.jwtToken, skillId)) }) + jobSkills = await Promise.all(usersPromises) + job.skills = jobSkills + } + } + } + + // add resources skills for result + if (teamDetail && teamDetail.resources) { + for (const user of teamDetail.resources) { + const userSkills = await helper.getUserSkill(currentUser.jwtToken, user.id) + user.skills = userSkills + + user.skillMatched = 0 + if (userSkills && userSkills.length > 0) { + for (const jobSkill of jobSkills) { + if (_.find(userSkills, userSkill => { + return userSkill.id === jobSkill.id + })) { + user.skillMatched += 1 + } + } + } + } + } + + return teamDetail +} + +getTeam.schema = Joi.object().keys({ + currentUser: Joi.object().required(), + id: Joi.number().integer().required() +}).required() + +/** + * Get team job with id + * @param {Object} currentUser the user who perform this operation + * @param {String} id the team id + * @param {String} jobId the job id + * @returns the team job + */ +async function getTeamJob (currentUser, id, jobId) { + // Get jobs from taas api + const jobs = await _getJobsByProjectIds([id]) + const job = _.find(jobs, { id: jobId }) + + if (!job) { + logger.debug({ component: 'TeamService', context: 'getTeamJob', message: `id ${jobId}: "Job" with Team id ${id} is not exist` }) + return {} + } + const result = { + id: job.id, + description: job.description + } + + const jobSkills = job.skills + + if (job && job.candidates && job.candidates.length > 0) { + const usersPromises = [] + _.map(job.candidates, (candidate) => { usersPromises.push(helper.getUserById(currentUser.jwtToken, candidate.userId)) }) + const candidates = await Promise.all(usersPromises) + + const userHandles = _.map(candidates, 'handle') + if (userHandles && userHandles.length > 0) { + // Get user photo from /v5/members + const members = await helper.getMembers(currentUser.jwtToken, userHandles) + + for (const item of candidates) { + item.resumeLink = null + const candidate = _.find(job.candidates, { userId: item.id }) + if (candidate) { + item.status = candidate.status + } + const findMember = _.find(members, { handleLower: item.handle.toLowerCase() }) + if (findMember && findMember.photoURL) { + item.photo_url = findMember.photoURL + } + + // Get user skill details from /v5/user/:id/skills + const userSkills = await helper.getUserSkill(currentUser.jwtToken, item.id) + item.skills = userSkills + + item.skillMatched = 0 + if (userSkills && userSkills.length > 0) { + for (const jobSkillId of jobSkills) { + if (_.find(userSkills, userSkill => { + return userSkill.id === jobSkillId + })) { + item.skillMatched += 1 + } + } + } + } + } + + result.candidates = candidates + } + + return result +} + +getTeamJob.schema = Joi.object().keys({ + currentUser: Joi.object().required(), + id: Joi.number().integer().required(), + jobId: Joi.string().guid().required() +}).required() + +module.exports = { + searchTeams, + getTeam, + getTeamJob +} diff --git a/test/unit/JobCandidateService.test.js b/test/unit/JobCandidateService.test.js index c2860fe8..bfa7b9c2 100644 --- a/test/unit/JobCandidateService.test.js +++ b/test/unit/JobCandidateService.test.js @@ -14,6 +14,7 @@ const { const helper = require('../../src/common/helper') const esClient = helper.getESClient() +const busApiClient = helper.getBusApiClient() const JobCandidate = models.JobCandidate const Job = models.Job @@ -23,6 +24,7 @@ describe('jobCandidate service test', () => { let userId let stubIsConnectMember let stubGetUserId + let stubPostEvent beforeEach(() => { isConnectMember = true stubIsConnectMember = sinon.stub(helper, 'isConnectMember').callsFake(() => { @@ -33,6 +35,7 @@ describe('jobCandidate service test', () => { stubGetUserId = sinon.stub(helper, 'getUserId').callsFake(() => { return userId }) + stubPostEvent = sinon.stub(busApiClient, 'postEvent').callsFake(async () => {}) }) afterEach(() => { @@ -40,11 +43,6 @@ describe('jobCandidate service test', () => { }) describe('create job candidate test', () => { - let stubESCreate - beforeEach(() => { - stubESCreate = sinon.stub(esClient, 'create').callsFake(async () => {}) - }) - it('create job candidate with booking manager success ', async () => { const jobCandidateRes = _.cloneDeep(jobCandidateResponseBody) const stubDBCreate = sinon.stub(JobCandidate, 'create').callsFake(() => { @@ -54,7 +52,7 @@ describe('jobCandidate service test', () => { const entity = await service.createJobCandidate(bookingManagerUser, jobCandidateRequestBody) expect(entity).to.deep.eql(jobCandidateRes.dataValues) expect(stubDBCreate.calledOnce).to.be.true - expect(stubESCreate.calledOnce).to.be.true + expect(stubPostEvent.calledOnce).to.be.true expect(stubGetUserId.calledOnce).to.be.true }) @@ -66,7 +64,7 @@ describe('jobCandidate service test', () => { const entity = await service.createJobCandidate(connectUser, jobCandidateRequestBody) expect(entity).to.deep.eql(jobCandidateRes.dataValues) expect(stubDBCreate.calledOnce).to.be.true - expect(stubESCreate.calledOnce).to.be.true + expect(stubPostEvent.calledOnce).to.be.true expect(stubGetUserId.calledOnce).to.be.true }) @@ -84,35 +82,60 @@ describe('jobCandidate service test', () => { describe('get job candidate test', () => { it('get job candidate success', async () => { + const jobCandidateRes = _.cloneDeep(jobCandidateResponseBody) + const stub = sinon.stub(esClient, 'get').callsFake(async () => { + return { + body: { + _id: jobCandidateRes.dataValues.id, + _source: _.omit(jobCandidateRes.dataValues, ['id']) + } + } + }) + const entity = await service.getJobCandidate(jobCandidateResponseBody.dataValues.id) + expect(entity).to.deep.eql(jobCandidateRes.dataValues) + expect(stub.calledOnce).to.be.true + }) + + it('get job candidate with candidate not exist success', async () => { + const stub = sinon.stub(esClient, 'get').callsFake(async () => { + const err = new Error() + err.statusCode = 404 + throw err + }) + try { + await service.getJobCandidate(jobCandidateResponseBody.dataValues.id) + unexpected() + } catch (error) { + expect(error.message).to.equal(`id: ${jobCandidateResponseBody.dataValues.id} "JobCandidate" not found`) + expect(stub.calledOnce).to.be.true + } + }) + + it('get job candidate from db success', async () => { const jobCandidateRes = _.cloneDeep(jobCandidateResponseBody) const stubJobCandidateFindOne = sinon.stub(JobCandidate, 'findOne').callsFake(() => { return jobCandidateRes }) - const entity = await service.getJobCandidate(jobCandidateResponseBody.dataValues.id) + const entity = await service.getJobCandidate(jobCandidateResponseBody.dataValues.id, true) expect(entity).to.deep.eql(jobCandidateRes.dataValues) expect(stubJobCandidateFindOne.calledOnce).to.be.true }) - it('get job candidate with candidate not exist success', async () => { + it('get job candidate from db with candidate not exist success', async () => { const stubJobCandidateFindOne = sinon.stub(JobCandidate, 'findOne').callsFake(() => { return null }) try { - await service.getJobCandidate(jobCandidateResponseBody.dataValues.id) + await service.getJobCandidate(jobCandidateResponseBody.dataValues.id, true) unexpected() } catch (error) { - expect(error.message).to.equal(`JobCandidate with id: ${jobCandidateResponseBody.dataValues.id} doesn't exists.`) + expect(error.message).to.equal(`id: ${jobCandidateResponseBody.dataValues.id} "JobCandidate" doesn't exists.`) expect(stubJobCandidateFindOne.calledOnce).to.be.true } }) }) describe('fully update job candidate test', () => { - let stubESUpdate - beforeEach(() => { - stubESUpdate = sinon.stub(esClient, 'update').callsFake(() => {}) - }) - it('fully update job candidate test with booking manager success', async () => { const jobCandidateRes = _.cloneDeep(jobCandidateResponseBody) const stubJobCandidateFindOne = sinon.stub(JobCandidate, 'findOne').callsFake(() => { @@ -129,7 +152,7 @@ describe('jobCandidate service test', () => { expect(entity).to.deep.eql(jobCandidateRes.dataValues) expect(stubJobCandidateFindOne.calledOnce).to.be.true expect(stubJobFindOne.calledOnce).to.be.true - expect(stubESUpdate.calledOnce).to.be.true + expect(stubPostEvent.calledOnce).to.be.true }) it('fully update job candidate test with connect user success', async () => { @@ -148,7 +171,7 @@ describe('jobCandidate service test', () => { expect(entity).to.deep.eql(jobCandidateRes.dataValues) expect(stubJobCandidateFindOne.calledOnce).to.be.true expect(stubJobFindOne.calledOnce).to.be.true - expect(stubESUpdate.calledOnce).to.be.true + expect(stubPostEvent.calledOnce).to.be.true expect(stubIsConnectMember.calledOnce).to.be.true }) @@ -169,7 +192,7 @@ describe('jobCandidate service test', () => { expect(entity).to.deep.eql(jobCandidateRes.dataValues) expect(stubJobCandidateFindOne.calledOnce).to.be.true expect(stubJobFindOne.calledOnce).to.be.true - expect(stubESUpdate.calledOnce).to.be.true + expect(stubPostEvent.calledOnce).to.be.true }) it('fully update job candidate test with topcoder user failed', async () => { @@ -197,11 +220,6 @@ describe('jobCandidate service test', () => { }) describe('partially update job candidate test', () => { - let stubESUpdate - beforeEach(() => { - stubESUpdate = sinon.stub(esClient, 'update').callsFake(() => {}) - }) - it('partially update job candidate test with booking manager success', async () => { const jobCandidateRes = _.cloneDeep(jobCandidateResponseBody) const stubJobCandidateFindOne = sinon.stub(JobCandidate, 'findOne').callsFake(() => { @@ -218,7 +236,7 @@ describe('jobCandidate service test', () => { expect(entity).to.deep.eql(jobCandidateRes.dataValues) expect(stubJobCandidateFindOne.calledOnce).to.be.true expect(stubJobFindOne.calledOnce).to.be.true - expect(stubESUpdate.calledOnce).to.be.true + expect(stubPostEvent.calledOnce).to.be.true }) it('partially update job candidate test with connect user success', async () => { @@ -237,7 +255,7 @@ describe('jobCandidate service test', () => { expect(entity).to.deep.eql(jobCandidateRes.dataValues) expect(stubJobCandidateFindOne.calledOnce).to.be.true expect(stubJobFindOne.calledOnce).to.be.true - expect(stubESUpdate.calledOnce).to.be.true + expect(stubPostEvent.calledOnce).to.be.true expect(stubIsConnectMember.calledOnce).to.be.true }) @@ -278,7 +296,7 @@ describe('jobCandidate service test', () => { await service.partiallyUpdateJobCandidate(bookingManagerUser, jobCandidateResponseBody.dataValues.id, partiallyUpdateJobCandidateRequestBody) expect(stubJobCandidateFindOne.calledOnce).to.be.true expect(stubJobFindOne.calledOnce).to.be.true - expect(stubESUpdate.calledOnce).to.be.true + expect(stubPostEvent.calledOnce).to.be.true }) }) @@ -290,10 +308,9 @@ describe('jobCandidate service test', () => { update: () => { return null } } }) - const stubESDelete = sinon.stub(esClient, 'delete').callsFake(() => {}) await service.deleteJobCandidate(bookingManagerUser, jobCandidateResponseBody.dataValues.id) expect(stubJobCandidateFindOne.calledOnce).to.be.true - expect(stubESDelete.calledOnce).to.be.true + expect(stubPostEvent.calledOnce).to.be.true }) it('delete job candidate test with connect user success', async () => { @@ -357,5 +374,19 @@ describe('jobCandidate service test', () => { expect(entity.result[0]).to.deep.eql(jobCandidateResponseBody.dataValues) expect(stub.calledOnce).to.be.true }) + + it('search job candidates success when es search fails', async () => { + const stubESSearch = sinon.stub(esClient, 'search').callsFake(() => { + throw new Error('dedicated es failure') + }) + + const stubDBSearch = sinon.stub(JobCandidate, 'findAll').callsFake(() => { + return [jobCandidateResponseBody] + }) + const entity = await service.searchJobCandidates({ sortBy: 'id', sortOrder: 'asc', page: 1, perPage: 1, jobId: '36762910-4efa-4db4-9b2a-c9ab54c232ed' }) + expect(entity.result[0]).to.deep.eql(jobCandidateResponseBody.dataValues) + expect(stubESSearch.calledOnce).to.be.true + expect(stubDBSearch.calledOnce).to.be.true + }) }) }) diff --git a/test/unit/JobService.test.js b/test/unit/JobService.test.js index 06bf1932..0f0d7ef2 100644 --- a/test/unit/JobService.test.js +++ b/test/unit/JobService.test.js @@ -15,6 +15,7 @@ const helper = require('../../src/common/helper') const errors = require('../../src/common/errors') const esClient = helper.getESClient() +const busApiClient = helper.getBusApiClient() const Job = models.Job @@ -23,6 +24,7 @@ describe('job service test', () => { let userId let stubIsConnectMember let stubGetUserId + let stubPostEvent beforeEach(() => { isConnectMember = true stubIsConnectMember = sinon.stub(helper, 'isConnectMember').callsFake(() => { @@ -33,6 +35,7 @@ describe('job service test', () => { stubGetUserId = sinon.stub(helper, 'getUserId').callsFake(() => { return userId }) + stubPostEvent = sinon.stub(busApiClient, 'postEvent').callsFake(async () => {}) }) afterEach(() => { @@ -40,11 +43,6 @@ describe('job service test', () => { }) describe('create job test', () => { - let stubESCreate - beforeEach(() => { - stubESCreate = sinon.stub(esClient, 'create').callsFake(async () => {}) - }) - it('create job with booking manager user success ', async () => { const stubDBCreate = sinon.stub(Job, 'create').callsFake(() => { return _.cloneDeep(jobResponseBody) @@ -53,7 +51,7 @@ describe('job service test', () => { const entity = await service.createJob(bookingManagerUser, jobRequestBody) expect(entity).to.deep.eql(jobResponseBody.dataValues) expect(stubDBCreate.calledOnce).to.be.true - expect(stubESCreate.calledOnce).to.be.true + expect(stubPostEvent.calledOnce).to.be.true expect(stubGetUserId.calledOnce).to.be.true }) @@ -64,7 +62,7 @@ describe('job service test', () => { const entity = await service.createJob(connectUser, jobRequestBody) expect(entity).to.deep.eql(jobResponseBody.dataValues) expect(stubDBCreate.calledOnce).to.be.true - expect(stubESCreate.calledOnce).to.be.true + expect(stubPostEvent.calledOnce).to.be.true expect(stubIsConnectMember.calledOnce).to.be.true expect(stubGetUserId.calledOnce).to.be.true }) @@ -99,34 +97,75 @@ describe('job service test', () => { describe('get job test', () => { it('get job success', async () => { const jobResBody = _.cloneDeep(jobResponseBody) - const stub = sinon.stub(Job, 'findOne').callsFake(() => { - return jobResBody + const stub = sinon.stub(esClient, 'get').callsFake(async () => { + return { + body: { + _id: jobResponseBody.dataValues.id, + _source: _.omit(jobResponseBody.dataValues, ['id']) + } + } + }) + const stubSearchCandidates = sinon.stub(esClient, 'search').callsFake(() => { + return { + body: { + hits: { + total: { + value: 1 + }, + hits: [{ + _id: jobResponseBody.dataValues.candidates[0].id, + _source: _.omit(jobResponseBody.dataValues.candidates[0], ['id']) + }] + } + } + } }) const entity = await service.getJob(jobResponseBody.dataValues.id) expect(entity).to.deep.eql(jobResBody.dataValues) expect(stub.calledOnce).to.be.true + expect(stubSearchCandidates.calledOnce).to.be.true }) it('get job with job not exist failed', async () => { + const stub = sinon.stub(esClient, 'get').callsFake(async () => { + const err = new Error() + err.statusCode = 404 + throw err + }) + try { + await service.getJob(jobResponseBody.dataValues.id) + unexpected() + } catch (error) { + expect(error.message).to.equal(`id: ${jobResponseBody.dataValues.id} "Job" not found`) + expect(stub.calledOnce).to.be.true + } + }) + + it('get job from db success', async () => { + const jobResBody = _.cloneDeep(jobResponseBody) + const stub = sinon.stub(Job, 'findOne').callsFake(() => { + return jobResBody + }) + const entity = await service.getJob(jobResponseBody.dataValues.id, true) + expect(entity).to.deep.eql(jobResBody.dataValues) + expect(stub.calledOnce).to.be.true + }) + + it('get job from db with job not exist failed', async () => { const stub = sinon.stub(Job, 'findOne').callsFake(() => { return null }) try { - await service.getJob(jobResponseBody.dataValues.id) + await service.getJob(jobResponseBody.dataValues.id, true) unexpected() } catch (error) { - expect(error.message).to.equal(`Job with id: ${jobResponseBody.dataValues.id} doesn't exists.`) + expect(error.message).to.equal(`id: ${jobResponseBody.dataValues.id} "Job" doesn't exists.`) expect(stub.calledOnce).to.be.true } }) }) describe('fully update job test', () => { - let stubESUpdate - beforeEach(() => { - stubESUpdate = sinon.stub(esClient, 'update').callsFake(() => {}) - }) - it('fully update job test with booking manager success', async () => { const jobResBody = _.cloneDeep(jobResponseBody) const stub = sinon.stub(Job, 'findOne').onFirstCall().callsFake(() => { @@ -145,7 +184,7 @@ describe('job service test', () => { const entity = await service.fullyUpdateJob(bookingManagerUser, jobResponseBody.dataValues.id, fullyUpdateJobRequestBody) expect(entity).to.deep.eql(jobResBody.dataValues) expect(stub.calledTwice).to.be.true - expect(stubESUpdate.calledOnce).to.be.true + expect(stubPostEvent.calledOnce).to.be.true expect(stubGetUserId.calledOnce).to.be.true }) @@ -167,7 +206,7 @@ describe('job service test', () => { const entity = await service.fullyUpdateJob(connectUser, jobResponseBody.dataValues.id, fullyUpdateJobRequestBody) expect(entity).to.deep.eql(jobResBody.dataValues) expect(stub.calledTwice).to.be.true - expect(stubESUpdate.calledOnce).to.be.true + expect(stubPostEvent.calledOnce).to.be.true expect(stubIsConnectMember.calledOnce).to.be.true expect(stubGetUserId.calledOnce).to.be.true }) @@ -192,11 +231,6 @@ describe('job service test', () => { }) describe('partially update job test', () => { - let stubESUpdate - beforeEach(() => { - stubESUpdate = sinon.stub(esClient, 'update').callsFake(() => {}) - }) - it('partially update job with booking manager success', async () => { const jobResBody = _.cloneDeep(jobResponseBody) const stub = sinon.stub(Job, 'findOne').onFirstCall().callsFake(() => { @@ -215,7 +249,7 @@ describe('job service test', () => { const entity = await service.partiallyUpdateJob(bookingManagerUser, jobResponseBody.dataValues.id, partiallyUpdateJobRequestBody) expect(entity).to.deep.eql(jobResBody.dataValues) expect(stub.calledTwice).to.be.true - expect(stubESUpdate.calledOnce).to.be.true + expect(stubPostEvent.calledOnce).to.be.true expect(stubGetUserId.calledOnce).to.be.true }) @@ -236,7 +270,7 @@ describe('job service test', () => { const entity = await service.partiallyUpdateJob(connectUser, jobResponseBody.dataValues.id, partiallyUpdateJobRequestBody) expect(entity).to.deep.eql(jobResBody.dataValues) expect(stub.calledTwice).to.be.true - expect(stubESUpdate.calledOnce).to.be.true + expect(stubPostEvent.calledOnce).to.be.true expect(stubIsConnectMember.calledOnce).to.be.true expect(stubGetUserId.calledOnce).to.be.true }) @@ -271,10 +305,9 @@ describe('job service test', () => { } } }) - const stubDelete = sinon.stub(esClient, 'delete').callsFake(async () => {}) await service.deleteJob(bookingManagerUser, jobResponseBody.dataValues.id) expect(stub.calledOnce).to.be.true - expect(stubDelete.calledOnce).to.be.true + expect(stubPostEvent.calledOnce).to.be.true }) it('delete job test with connect user failed', async () => { @@ -363,5 +396,20 @@ describe('job service test', () => { expect(entity.result[0]).to.deep.eql(jobResponseBody.dataValues) expect(stub.calledTwice).to.be.true }) + + it('search jobs success when es search fails', async () => { + const stubESSearch = sinon.stub(esClient, 'search').callsFake(() => { + throw new Error('dedicated es failure') + }) + + const stubDBSearch = sinon.stub(Job, 'findAll').callsFake(() => { + return [jobResponseBody] + }) + + const entity = await service.searchJobs({ sortBy: 'id', sortOrder: 'asc', page: 1, perPage: 1, skill: '56fdc405-eccc-4189-9e83-c78abf844f50', description: 'description 1', rateType: 'hourly' }) + expect(entity.result[0]).to.deep.eql(jobResponseBody.dataValues) + expect(stubESSearch.calledOnce).to.be.true + expect(stubDBSearch.calledOnce).to.be.true + }) }) }) diff --git a/test/unit/ResourceBookingService.test.js b/test/unit/ResourceBookingService.test.js index c662d26b..fba2415a 100644 --- a/test/unit/ResourceBookingService.test.js +++ b/test/unit/ResourceBookingService.test.js @@ -17,6 +17,7 @@ const { const helper = require('../../src/common/helper') const esClient = helper.getESClient() +const busApiClient = helper.getBusApiClient() const ResourceBooking = models.ResourceBooking @@ -25,6 +26,7 @@ describe('resourceBooking service test', () => { let userId let stubIsConnectMember let stubGetUserId + let stubPostEvent beforeEach(() => { isConnectMember = true stubIsConnectMember = sinon.stub(helper, 'isConnectMember').callsFake(() => { @@ -35,6 +37,7 @@ describe('resourceBooking service test', () => { stubGetUserId = sinon.stub(helper, 'getUserId').callsFake(() => { return userId }) + stubPostEvent = sinon.stub(busApiClient, 'postEvent').callsFake(async () => {}) }) afterEach(() => { @@ -42,11 +45,6 @@ describe('resourceBooking service test', () => { }) describe('create resource booking test', () => { - let stubESCreate - beforeEach(() => { - stubESCreate = sinon.stub(esClient, 'create').callsFake(async () => {}) - }) - it('create resource booking with booking manager success ', async () => { const stubDBCreate = sinon.stub(ResourceBooking, 'create').callsFake(() => { return resourceBookingResponseBody @@ -54,7 +52,7 @@ describe('resourceBooking service test', () => { const entity = await service.createResourceBooking(bookingManagerUser, resourceBookingRequestBody) expect(entity).to.deep.eql(resourceBookingResponseBody.dataValues) expect(stubDBCreate.calledOnce).to.be.true - expect(stubESCreate.calledOnce).to.be.true + expect(stubPostEvent.calledOnce).to.be.true }) it('create resource booking with connect user success ', async () => { @@ -65,7 +63,7 @@ describe('resourceBooking service test', () => { const entity = await service.createResourceBooking(connectUser, resourceBookingRequestBody) expect(entity).to.deep.eql(resourceBookingResponseBody.dataValues) expect(stubDBCreate.calledOnce).to.be.true - expect(stubESCreate.calledOnce).to.be.true + expect(stubPostEvent.calledOnce).to.be.true expect(stubIsConnectMember.calledOnce).to.be.true expect(stubGetUserId.calledOnce).to.be.true }) @@ -83,8 +81,13 @@ describe('resourceBooking service test', () => { describe('get resource booking test', () => { it('get resource booking with booking manager success', async () => { const resourceBookingRes = _.cloneDeep(resourceBookingResponseBody) - const stub = sinon.stub(ResourceBooking, 'findOne').callsFake(() => { - return resourceBookingRes + const stub = sinon.stub(esClient, 'get').callsFake(async () => { + return { + body: { + _id: resourceBookingRes.dataValues.id, + _source: _.omit(resourceBookingRes.dataValues, ['id']) + } + } }) const entity = await service.getResourceBooking(bookingManagerUser, resourceBookingResponseBody.dataValues.id) expect(entity).to.deep.eql(resourceBookingRes.dataValues) @@ -93,8 +96,13 @@ describe('resourceBooking service test', () => { it('get resource booking with connect user success', async () => { const resourceBookingRes = _.cloneDeep(resourceBookingResponseBody) - const stub = sinon.stub(ResourceBooking, 'findOne').callsFake(() => { - return resourceBookingRes + const stub = sinon.stub(esClient, 'get').callsFake(async () => { + return { + body: { + _id: resourceBookingRes.dataValues.id, + _source: _.omit(resourceBookingRes.dataValues, ['id']) + } + } }) const entity = await service.getResourceBooking(connectUser, resourceBookingResponseBody.dataValues.id) @@ -106,8 +114,13 @@ describe('resourceBooking service test', () => { it('get resource booking with topcoder user success', async () => { isConnectMember = false const resourceBookingRes = _.cloneDeep(resourceBookingResponseBody) - const stub = sinon.stub(ResourceBooking, 'findOne').callsFake(() => { - return resourceBookingRes + const stub = sinon.stub(esClient, 'get').callsFake(async () => { + return { + body: { + _id: resourceBookingRes.dataValues.id, + _source: _.omit(resourceBookingRes.dataValues, ['id']) + } + } }) const entity = await service.getResourceBooking(topCoderUser, resourceBookingResponseBody.dataValues.id) expect(entity).to.deep.eql(_.omit(resourceBookingRes.dataValues, ['customerRate'])) @@ -115,24 +128,66 @@ describe('resourceBooking service test', () => { }) it('get resource booking with resource booking not exist success', async () => { + const stub = sinon.stub(esClient, 'get').callsFake(async () => { + const err = new Error() + err.statusCode = 404 + throw err + }) + try { + await service.getResourceBooking(bookingManagerUser, resourceBookingResponseBody.dataValues.id) + } catch (error) { + expect(error.message).to.equal(`id: ${resourceBookingResponseBody.dataValues.id} "ResourceBooking" not found`) + expect(stub.calledOnce).to.be.true + } + }) + + it('get resource booking from db with booking manager success', async () => { + const resourceBookingRes = _.cloneDeep(resourceBookingResponseBody) + const stub = sinon.stub(ResourceBooking, 'findOne').callsFake(() => { + return resourceBookingRes + }) + const entity = await service.getResourceBooking(bookingManagerUser, resourceBookingResponseBody.dataValues.id, true) + expect(entity).to.deep.eql(resourceBookingRes.dataValues) + expect(stub.calledOnce).to.be.true + }) + + it('get resource booking from db with connect user success', async () => { + const resourceBookingRes = _.cloneDeep(resourceBookingResponseBody) + const stub = sinon.stub(ResourceBooking, 'findOne').callsFake(() => { + return resourceBookingRes + }) + + const entity = await service.getResourceBooking(connectUser, resourceBookingResponseBody.dataValues.id, true) + expect(entity).to.deep.eql(_.omit(resourceBookingRes.dataValues, ['memberRate'])) + expect(stub.calledOnce).to.be.true + expect(stubIsConnectMember.calledOnce).to.be.true + }) + + it('get resource booking from db with topcoder user success', async () => { + isConnectMember = false + const resourceBookingRes = _.cloneDeep(resourceBookingResponseBody) + const stub = sinon.stub(ResourceBooking, 'findOne').callsFake(() => { + return resourceBookingRes + }) + const entity = await service.getResourceBooking(topCoderUser, resourceBookingResponseBody.dataValues.id, true) + expect(entity).to.deep.eql(_.omit(resourceBookingRes.dataValues, ['customerRate'])) + expect(stub.calledOnce).to.be.true + }) + + it('get resource booking from db with resource booking not exist success', async () => { const stub = sinon.stub(ResourceBooking, 'findOne').callsFake(() => { return null }) try { - await service.getResourceBooking(bookingManagerUser, resourceBookingResponseBody.dataValues.id) + await service.getResourceBooking(bookingManagerUser, resourceBookingResponseBody.dataValues.id, true) } catch (error) { - expect(error.message).to.equal(`ResourceBooking with id: ${resourceBookingResponseBody.dataValues.id} doesn't exists.`) + expect(error.message).to.equal(`id: ${resourceBookingResponseBody.dataValues.id} "ResourceBooking" doesn't exists.`) expect(stub.calledOnce).to.be.true } }) }) describe('fully update resource booking test', () => { - let stubESUpdate - beforeEach(() => { - stubESUpdate = sinon.stub(esClient, 'update').callsFake(() => {}) - }) - it('fully update resource booking test with booking manager success', async () => { const resourceBookingRes = _.cloneDeep(resourceBookingResponseBody) const stubResourceBookingFindOne = sinon.stub(ResourceBooking, 'findOne').callsFake(() => { @@ -144,7 +199,7 @@ describe('resourceBooking service test', () => { const entity = await service.fullyUpdateResourceBooking(bookingManagerUser, resourceBookingResponseBody.dataValues.id, fullyUpdateResourceBookingRequestBody) expect(entity).to.deep.eql(resourceBookingRes.dataValues) expect(stubResourceBookingFindOne.calledOnce).to.be.true - expect(stubESUpdate.calledOnce).to.be.true + expect(stubPostEvent.calledOnce).to.be.true }) it('fully update resource booking test with connect user success', async () => { @@ -159,7 +214,7 @@ describe('resourceBooking service test', () => { const entity = await service.fullyUpdateResourceBooking(connectUser, resourceBookingResponseBody.dataValues.id, fullyUpdateResourceBookingRequestBody) expect(entity).to.deep.eql(resourceBookingRes.dataValues) expect(stubResourceBookingFindOne.calledOnce).to.be.true - expect(stubESUpdate.calledOnce).to.be.true + expect(stubPostEvent.calledOnce).to.be.true expect(stubIsConnectMember.calledOnce).to.be.true }) @@ -183,11 +238,6 @@ describe('resourceBooking service test', () => { }) describe('partially update resource booking test', () => { - let stubESUpdate - beforeEach(() => { - stubESUpdate = sinon.stub(esClient, 'update').callsFake(() => {}) - }) - it('partially update resource booking test with booking manager success', async () => { const resourceBookingRes = _.cloneDeep(resourceBookingResponseBody) const stubResourceBookingFindOne = sinon.stub(ResourceBooking, 'findOne').callsFake(() => { @@ -199,7 +249,7 @@ describe('resourceBooking service test', () => { const entity = await service.partiallyUpdateResourceBooking(bookingManagerUser, resourceBookingResponseBody.dataValues.id, partiallyUpdateResourceBookingRequestBody) expect(entity).to.deep.eql(resourceBookingRes.dataValues) expect(stubResourceBookingFindOne.calledOnce).to.be.true - expect(stubESUpdate.calledOnce).to.be.true + expect(stubPostEvent.calledOnce).to.be.true }) it('partially update resource booking test with connect user success', async () => { @@ -214,7 +264,7 @@ describe('resourceBooking service test', () => { const entity = await service.partiallyUpdateResourceBooking(connectUser, resourceBookingResponseBody.dataValues.id, partiallyUpdateResourceBookingRequestBody) expect(entity).to.deep.eql(resourceBookingRes.dataValues) expect(stubResourceBookingFindOne.calledOnce).to.be.true - expect(stubESUpdate.calledOnce).to.be.true + expect(stubPostEvent.calledOnce).to.be.true expect(stubIsConnectMember.calledOnce).to.be.true }) @@ -245,10 +295,9 @@ describe('resourceBooking service test', () => { update: () => { return null } } }) - const stubES = sinon.stub(esClient, 'delete').callsFake(() => {}) await service.deleteResourceBooking(bookingManagerUser, resourceBookingResponseBody.dataValues.id) expect(stubResourceBookingFindOne.calledOnce).to.be.true - expect(stubES.calledOnce).to.be.true + expect(stubPostEvent.calledOnce).to.be.true }) it('delete resource booking test with connect user failed', async () => { @@ -312,5 +361,19 @@ describe('resourceBooking service test', () => { expect(entity.result[0]).to.deep.eql(resourceBookingResponseBody.dataValues) expect(stub.calledOnce).to.be.true }) + + it('search resource booking success when es search fails', async () => { + const stubESSearch = sinon.stub(esClient, 'search').callsFake(() => { + throw new Error('dedicated es failure') + }) + + const stubDBSearch = sinon.stub(ResourceBooking, 'findAll').callsFake(() => { + return [resourceBookingResponseBody] + }) + const entity = await service.searchResourceBookings({ sortBy: 'id', sortOrder: 'asc', page: 1, perPage: 1, status: 'sourcing' }) + expect(entity.result[0]).to.deep.eql(resourceBookingResponseBody.dataValues) + expect(stubESSearch.calledOnce).to.be.true + expect(stubDBSearch.calledOnce).to.be.true + }) }) }) diff --git a/test/unit/TeamService.test.js b/test/unit/TeamService.test.js new file mode 100644 index 00000000..bbe81a00 --- /dev/null +++ b/test/unit/TeamService.test.js @@ -0,0 +1,293 @@ +/* eslint-disable no-unused-expressions */ +process.env.NODE_ENV = 'test' +require('../../src/bootstrap') + +const expect = require('chai').expect +const sinon = require('sinon') + +const service = require('../../src/services/TeamService') +const JobService = require('../../src/services/JobService') +const ResourceBookingService = require('../../src/services/ResourceBookingService') +const { + bookingManagerUser, + unexpected, + projectRequestBody, + userRequestBody, + memberRequestBody, + skillsRequestBody, + userSkillsRequestBody, + resourceBookingsRequestBody, + jobsRequestBody, + taasTeamItem0ResponseBody, + taasTeam9050ResponseBody, + jobDetailResponseBody +} = require('./common/testData') +const helper = require('../../src/common/helper') + +describe('Team service test', () => { + let stubGetProjects + let stubGetProjectById + let stubGetUsers + let stubGetMembers + let stubGetSkills + let stubGetUserSkill + let stubGetAssignedResourceBookings + let stubGetJobs + beforeEach(() => { + stubGetProjects = sinon.stub(helper, 'getProjects').callsFake(() => { + return projectRequestBody + }) + + stubGetProjectById = sinon.stub(helper, 'getProjectById').callsFake(() => { + return projectRequestBody[0] + }) + + stubGetUsers = sinon.stub(helper, 'getUsers').callsFake(() => { + return userRequestBody + }) + + stubGetMembers = sinon.stub(helper, 'getMembers').callsFake(() => { + return memberRequestBody + }) + + stubGetSkills = sinon.stub(helper, 'getSkills').callsFake(() => { + return skillsRequestBody + }) + + stubGetUserSkill = sinon.stub(helper, 'getUserSkill').callsFake(() => { + return userSkillsRequestBody + }) + + stubGetAssignedResourceBookings = sinon.stub(ResourceBookingService, 'searchResourceBookings').callsFake(() => { + return { result: resourceBookingsRequestBody } + }) + + stubGetJobs = sinon.stub(JobService, 'searchJobs').callsFake(() => { + return { result: jobsRequestBody } + }) + }) + + afterEach(() => { + sinon.restore() + }) + + describe('search teams test', () => { + beforeEach(() => { + stubGetAssignedResourceBookings.restore() + stubGetAssignedResourceBookings = sinon.stub(ResourceBookingService, 'searchResourceBookings').callsFake(() => { + return { result: resourceBookingsRequestBody } + }) + stubGetJobs.restore() + stubGetJobs = sinon.stub(JobService, 'searchJobs').callsFake(() => { + return { result: jobsRequestBody } + }) + }) + + it('search teams success ', async () => { + const entity = await service.searchTeams(bookingManagerUser) + expect(entity.length).to.equal(20) + expect(entity[0]).to.deep.eql(taasTeamItem0ResponseBody) + expect(stubGetProjects.calledOnce).to.be.true + expect(stubGetUsers.calledOnce).to.be.true + expect(stubGetMembers.called).to.be.true + expect(stubGetAssignedResourceBookings.calledOnce).to.be.true + expect(stubGetJobs.calledOnce).to.be.true + }) + + it('search teams success with no resourceBooking ', async () => { + stubGetAssignedResourceBookings.restore() + stubGetAssignedResourceBookings = sinon.stub(ResourceBookingService, 'searchResourceBookings').callsFake(() => { + return { result: [] } + }) + stubGetJobs.restore() + stubGetJobs = sinon.stub(JobService, 'searchJobs').callsFake(() => { + return { result: [] } + }) + + const entity = await service.searchTeams(bookingManagerUser) + expect(entity.length).to.equal(20) + expect(entity[0]).to.deep.eql({ + id: 9050, + name: 'sample', + weeklyCount: 0, + resources: [] + }) + expect(stubGetProjects.calledOnce).to.be.true + expect(stubGetAssignedResourceBookings.calledOnce).to.be.true + expect(stubGetJobs.calledOnce).to.be.true + }) + + it('search teams success with no null start date', async () => { + stubGetAssignedResourceBookings.restore() + stubGetAssignedResourceBookings = sinon.stub(ResourceBookingService, 'searchResourceBookings').callsFake(() => { + return { + result: [{ + projectId: 9050, + userId: '1b88e433-828b-4e0d-9fb5-ef75b9dcca6e', + jobId: '1d9e8c1a-e653-4d31-a799-2685e41da212', + startDate: null, + endDate: null, + memberRate: 13.23, + customerRate: 13, + rateType: 'hourly', + status: 'assigned', + createdAt: '2020-11-11T04:17:23.131Z', + createdBy: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + id: '3d9e8c1a-e653-4d31-a799-2685e41da212' + }] + } + }) + + const entity = await service.searchTeams(bookingManagerUser) + expect(entity.length).to.equal(20) + expect(stubGetProjects.calledOnce).to.be.true + expect(stubGetAssignedResourceBookings.calledOnce).to.be.true + expect(stubGetJobs.calledOnce).to.be.true + }) + + it('search teams success with no empty user infos', async () => { + stubGetAssignedResourceBookings.restore() + stubGetAssignedResourceBookings = sinon.stub(ResourceBookingService, 'searchResourceBookings').callsFake(() => { + return { + result: [{ + projectId: 9050, + userId: 'not exist', + jobId: '1d9e8c1a-e653-4d31-a799-2685e41da212', + startDate: null, + endDate: null, + memberRate: 13.23, + customerRate: 13, + rateType: 'hourly', + status: 'assigned', + createdAt: '2020-11-11T04:17:23.131Z', + createdBy: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + id: '3d9e8c1a-e653-4d31-a799-2685e41da212' + }] + } + }) + + const entity = await service.searchTeams(bookingManagerUser) + expect(entity.length).to.equal(20) + expect(stubGetProjects.calledOnce).to.be.true + expect(stubGetAssignedResourceBookings.calledOnce).to.be.true + expect(stubGetJobs.calledOnce).to.be.true + }) + }) + + describe('get team by id test', () => { + beforeEach(() => { + stubGetUserSkill.restore() + stubGetUserSkill = sinon.stub(helper, 'getUserSkill').callsFake(() => { + return userSkillsRequestBody + }) + }) + + it('get team by id success ', async () => { + const entity = await service.getTeam(bookingManagerUser, 9050) + expect(entity).to.deep.eql(taasTeam9050ResponseBody) + expect(stubGetProjectById.calledOnce).to.be.true + expect(stubGetUsers.calledOnce).to.be.true + expect(stubGetMembers.called).to.be.true + expect(stubGetAssignedResourceBookings.calledOnce).to.be.true + expect(stubGetJobs.calledOnce).to.be.true + expect(stubGetSkills.calledOnce).to.be.true + expect(stubGetUserSkill.called).to.be.true + }) + + it('get team by id with no user skills', async () => { + stubGetUserSkill.restore() + stubGetUserSkill = sinon.stub(helper, 'getUserSkill').callsFake(() => { + return [] + }) + + const entity = await service.getTeam(bookingManagerUser, 9050) + expect(entity.resources[0].skillMatched).to.equal(0) + expect(stubGetProjectById.calledOnce).to.be.true + expect(stubGetAssignedResourceBookings.calledOnce).to.be.true + expect(stubGetJobs.calledOnce).to.be.true + }) + }) + + describe('get job by team id and job id test', () => { + beforeEach(() => { + stubGetUserSkill.restore() + stubGetUserSkill = sinon.stub(helper, 'getUserSkill').callsFake(() => { + return userSkillsRequestBody + }) + stubGetMembers.restore() + stubGetMembers = sinon.stub(helper, 'getMembers').callsFake(() => { + return memberRequestBody + }) + stubGetUsers.restore() + stubGetUsers = sinon.stub(helper, 'getUsers').callsFake(() => { + return userRequestBody + }) + }) + + it('get job detail by id success ', async () => { + const entity = await service.getTeamJob(bookingManagerUser, 9050, '1d9e8c1a-e653-4d31-a799-2685e41da212') + expect(entity).to.deep.eql(jobDetailResponseBody) + expect(stubGetJobs.calledOnce).to.be.true + expect(stubGetUsers.called).to.be.true + expect(stubGetMembers.called).to.be.true + expect(stubGetUserSkill.called).to.be.true + }) + + it('get job detail by not exist id success ', async () => { + const entity = await service.getTeamJob(bookingManagerUser, 9050, '1d9e8c1a-e653-4d31-a799-2685e41da999') + expect(entity).to.be.a('object') + expect(stubGetJobs.calledOnce).to.be.true + }) + + it('get job detail by invalid id ', async () => { + try { + await service.getTeamJob(bookingManagerUser, 9050, 'invalid') + unexpected() + } catch (error) { + expect(error.message).to.equal('"jobId" must be a valid GUID') + } + }) + + it('get team by id with no user skills', async () => { + stubGetUserSkill.restore() + stubGetUserSkill = sinon.stub(helper, 'getUserSkill').callsFake(() => { + return [] + }) + + const entity = await service.getTeamJob(bookingManagerUser, 9050, '1d9e8c1a-e653-4d31-a799-2685e41da212') + expect(entity.candidates[0].skillMatched).to.equal(0) + expect(stubGetJobs.calledOnce).to.be.true + expect(stubGetUsers.called).to.be.true + expect(stubGetMembers.called).to.be.true + expect(stubGetUserSkill.called).to.be.true + }) + + it('get job detail by id with photoURL ', async () => { + stubGetMembers.restore() + stubGetMembers = sinon.stub(helper, 'getMembers').callsFake(() => { + return [{ + userId: 8547893, + handleLower: 'sandrine_kuvalis98', + photoURL: 'https://topcoder-dev-media.s3.amazonaws.com/member/profile/TonyJ-1604301092491.jpeg' + }] + }) + + const entity = await service.getTeamJob(bookingManagerUser, 9050, '1d9e8c1a-e653-4d31-a799-2685e41da212') + expect(entity.candidates[0].photo_url).to.be.a('string') + expect(stubGetJobs.calledOnce).to.be.true + expect(stubGetUsers.called).to.be.true + expect(stubGetMembers.called).to.be.true + expect(stubGetUserSkill.called).to.be.true + }) + + it('get job detail by id empty users ', async () => { + stubGetUsers.restore() + stubGetUsers = sinon.stub(helper, 'getUsers').callsFake(() => { + return [] + }) + + const entity = await service.getTeamJob(bookingManagerUser, 9050, '1d9e8c1a-e653-4d31-a799-2685e41da212') + expect(entity.candidates.length).to.equal(0) + }) + }) +}) diff --git a/test/unit/common/testData.js b/test/unit/common/testData.js index 60d8a131..c7040d0b 100644 --- a/test/unit/common/testData.js +++ b/test/unit/common/testData.js @@ -209,6 +209,556 @@ const partiallyUpdateResourceBookingRequestBody = { const unexpected = () => { throw new Error('should not reach here') } +const projectRequestBody = [ + { id: 9050, name: 'sample' }, + { id: 9056, name: 'Invitation Test Max' }, + { id: 9063, name: 'Project001{codejam}' }, + { id: 9072, name: 'Project Name Edited' }, + { id: 9080, name: 'sdfdf' }, + { id: 9081, name: 'Test' }, + { id: 9091, name: 'Test 1 Max' }, + { id: 9096, name: 'test 1' }, + { id: 9097, name: 'test 2' }, + { id: 9099, name: 'Test' }, + { id: 9101, name: 'testin vikasverma' }, + { id: 9102, name: 'Test' }, + { id: 9132, name: 'Copilot Invite Test Max' }, + { id: 9133, name: 'Test' }, + { id: 9138, name: 'Update node versions 5 {codejam}' }, + { id: 9153, name: 'y' }, + { id: 9155, name: 'error' }, + { id: 9157, name: '3950-vikasverma' }, + { id: 9175, name: 'project' }, + { id: 9176, name: 'Testing 4-24-20 at 4-20' } +] + +const userRequestBody = [ + { + id: '1b88e433-828b-4e0d-9fb5-ef75b9dcca6e', + handle: 'PE335869', + firstName: ' Peddaram', + lastName: ' Varalakshmi' + }, + { + id: '10803918-ded0-4906-9442-65dc8819de91', + handle: 'Gayathri_1994', + firstName: 'Gayathri', + lastName: 'Sekar' + }, + { + id: '5bd69a82-c2cb-476f-9462-0883d3b28b90', + handle: 'Sandrine_Kuvalis98', + firstName: 'Mekhi', + lastName: 'Tremblay' + }, + { + id: '460bddcd-3580-4f2a-bfe8-5ba6d8f6f6af', + handle: 'Lourdes0', + firstName: 'Larissa', + lastName: 'Sporer' + }, + { + id: '4f2dc463-e24b-4b4a-8cde-c0122fbfb8ac', + handle: 'Demond39', + firstName: 'Domenic', + lastName: 'Casper' + }, + { + id: '39203872-707a-41b8-a587-18cab2557632', + handle: 'testkeychng204', + firstName: 'Testing', + lastName: 'Mithun' + }, + { + id: 'b074236c-bb33-449f-9320-72437a064c38', + handle: 'MA40018690', + firstName: 'Mangasamudram', + lastName: 'Teja' + }, + { + id: 'cdaeb417-e400-4df1-b484-f99ae10b4800', + handle: 'Leilani_Fahey35', + firstName: 'Godfrey', + lastName: 'Morar' + }, + { + id: 'ecec4ad8-3a1d-4646-8641-25054e8f2d33', + handle: 'Bernadine17', + firstName: 'Elmore', + lastName: 'Sanford' + }, + { + id: '28df7acf-d7b1-467c-8ee5-594c7bace8dc', + handle: 'Sylvan_Gorczany', + firstName: 'Samara', + lastName: 'Schultz' + }, + { + id: '6910d2f4-a50a-4494-8f46-6de1f3d032c2', + handle: 'Aditya65', + firstName: 'Clemens', + lastName: 'Rodriguez' + }, + { + id: '9bf08a13-29b6-4ef9-a2b6-d967c1c50fb4', + handle: 'Abirami_S', + firstName: 'Abirami', + lastName: 'SenthilNathan' + }, + { + id: '25f7b0e8-10a1-4bbc-b2f9-dacb1c72f1e9', + handle: 'Gaurav..Kumar', + firstName: 'Gaurav', + lastName: 'Kumar' + }, + { + id: '6fa6d708-68a6-47be-9591-4b5100921b3a', + handle: 'Kian.DuBuque', + firstName: 'Myles', + lastName: 'Connelly' + }, + { + id: '8edca7c4-0e71-4688-952a-42227f73ca32', + handle: 'BinoyVipin', + firstName: 'Binoy', + lastName: 'V' + }, + { + id: '247aaea8-f7e0-4ac8-b89e-4d78b76226b0', + handle: 'saikrupa87', + firstName: 'Miriyala', + lastName: 'Saikrupa reddy' + }, + { + id: 'cc7a694c-44a0-412b-9d1d-f98f7fe26a21', + handle: 'SriV_1672', + firstName: 'Srinivas', + lastName: 'Merugu' + }, + { + id: '07744775-eff1-443d-b56b-9d09ed02e599', + handle: 'Aachal', + firstName: 'Aachal ', + lastName: 'Jain' + }, + { + id: '0668fe37-b9cf-481b-8769-c3615833f80a', + handle: 'satadipa', + firstName: 'Satadipa', + lastName: 'Datta' + }, + { + id: '844fad5d-f19e-444b-be2e-ba9c36d34265', + handle: 'BA249730', + firstName: 'Balamurali', + lastName: 'B' + } +] + +const memberRequestBody = [ + { + userId: 305384, + handleLower: 'mess', + photoURL: 'https://topcoder-dev-media.s3.us-east-1.amazonaws.com/member/profile/mess-1601487458118.jpeg' + }, + { + userId: 88773829, + handleLower: 'pe335869' + }, + { + userId: 8547899, + handleLower: 'tonyj', + photoURL: 'https://topcoder-dev-media.s3.amazonaws.com/member/profile/TonyJ-1604301092491.jpeg' + }, + { + userId: 8547893, + handleLower: 'gayathri_1994', + photoURL: 'https://topcoder-dev-media.s3.amazonaws.com/member/profile/TonyJ-1604301092491.jpeg' + } +] + +const skillsRequestBody = [ + { id: 'cb01fd31-e8d2-4e34-8bf3-b149705de3e1', name: 'ACI Concrete Strength Testing Technician' }, + { id: '59ee7b42-f3f3-48c9-bdca-e8396b241793', name: '2D Computer Graphics' }, + { id: '1b585c26-2649-4078-8369-b599fe6a9d75', name: 'ACCP Certified' }, + { id: '8b757998-ff7d-4b3a-9fee-a49d3e41da03', name: '3D Projection' }, + { id: 'bcfc8806-cae6-47ff-b0c9-f604acfc8c99', name: '35 Mm Films' }, + { id: 'db4358f3-c9a3-4afd-94a4-2d352d6bfa60', name: 'Nintex Workflow for SharePoint' }, + { id: '077533be-8029-4585-8b8e-9efc2dc43f53', name: 'CA Service Virtualization' }, + { id: 'f682838c-a9a7-4b47-8c2e-45d5132c04d7', name: 'SOAP UI Testing' }, + { id: '13dda8dc-4c34-4751-bbab-aab76d757cbb', name: 'JavaBean' }, + { id: '99b930b5-1b91-4df1-8b17-d9307107bb51', name: 'Excel' }, + { id: '513ba597-5ad1-4177-8556-35583e0cc6ac', name: 'Microsoft Dynamics 365 Portals' }, + { id: 'ee4c50c1-c8c3-475e-b6b6-edbd136a19d6', name: 'SFDC Lightening Components' }, + { id: '1c67b60d-32a3-4dcd-8bc4-947bc476fcd1', name: 'Performance Point' }, + { id: '70834003-3eba-452c-9bc4-a6d9d637a10e', name: 'AI/ML App Testing' }, + { id: '89139c80-d0a2-47c2-aa16-14589d5afd10', name: 'User Testing' }, + { id: '866ee344-5328-4c1d-b9ae-1c003f8fef16', name: 'DB2 Testing' }, + { id: '9f2d9127-6a2e-4506-ad76-c4ab63577b09', name: 'IndexedDB' }, + { id: 'c854ab55-5922-4be1-8ecc-b3bc1f8629af', name: 'InVision' }, + { id: '9515e7ee-83b6-49d1-ba5c-6c59c5a8ef1b', name: 'List' }, + { id: '57ef43c2-4227-4ea1-bc5a-287321f3f8b2', name: 'Microsoft Dynamics AX 2012 - Retail' } +] + +const userSkillsRequestBody = [ + { id: '5d313a7b-795b-42a2-9e7e-dc5e81c2f2b5', name: 'Java' }, + { id: 'cb01fd31-e8d2-4e34-8bf3-b149705de3e1', name: 'ACI Concrete Strength Testing Technician' } +] + +const resourceBookingsRequestBody = [ + { + projectId: 9063, + userId: '4f2dc463-e24b-4b4a-8cde-c0122fbfb8ac', + jobId: '1d9e8c1a-e653-4d31-a799-2685e41da216', + startDate: '2020-09-28T04:17:23.131Z', + endDate: '2020-12-30T04:17:23.131Z', + memberRate: 13.23, + customerRate: 11.11, + rateType: 'hourly', + status: 'assigned', + createdAt: '2020-11-11T04:17:23.131Z', + createdBy: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + id: '3d9e8c1a-e653-4d31-a799-2685e41da216' + }, + { + projectId: 9056, + userId: '460bddcd-3580-4f2a-bfe8-5ba6d8f6f6af', + jobId: '1d9e8c1a-e653-4d31-a799-2685e41da215', + startDate: '2020-09-28T04:17:23.131Z', + endDate: '2020-12-30T04:17:23.131Z', + memberRate: 13.23, + customerRate: 156.7, + rateType: 'hourly', + status: 'assigned', + createdAt: '2020-11-11T04:17:23.131Z', + createdBy: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + id: '3d9e8c1a-e653-4d31-a799-2685e41da215' + }, + { + projectId: 9050, + userId: '5bd69a82-c2cb-476f-9462-0883d3b28b90', + jobId: '1d9e8c1a-e653-4d31-a799-2685e41da214', + startDate: '2020-09-28T04:17:23.131Z', + endDate: '2020-12-30T04:17:23.131Z', + memberRate: 13.23, + customerRate: 16.7, + rateType: 'hourly', + status: 'assigned', + createdAt: '2020-11-11T04:17:23.131Z', + createdBy: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + id: '3d9e8c1a-e653-4d31-a799-2685e41da214' + }, + { + projectId: 9050, + userId: '10803918-ded0-4906-9442-65dc8819de91', + jobId: '1d9e8c1a-e653-4d31-a799-2685e41da213', + startDate: '2020-09-22T04:17:23.131Z', + endDate: '2020-10-27T04:17:23.131Z', + memberRate: 13.23, + customerRate: 14.5, + rateType: 'hourly', + status: 'assigned', + createdAt: '2020-11-11T04:17:23.131Z', + createdBy: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + id: '3d9e8c1a-e653-4d31-a799-2685e41da213' + }, + { + projectId: 9050, + userId: '1b88e433-828b-4e0d-9fb5-ef75b9dcca6e', + jobId: '1d9e8c1a-e653-4d31-a799-2685e41da212', + startDate: '2020-09-27T04:17:23.131Z', + endDate: '2021-11-11T04:17:23.131Z', + memberRate: 13.23, + customerRate: 13, + rateType: 'hourly', + status: 'assigned', + createdAt: '2020-11-11T04:17:23.131Z', + createdBy: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + id: '3d9e8c1a-e653-4d31-a799-2685e41da212' + } +] + +const jobsRequestBody = [ + { + projectId: 9063, + externalId: '1212', + description: 'Dummy Description', + startDate: '2020-09-27T04:17:23.131Z', + endDate: '2020-10-09T04:17:23.131Z', + numPositions: 10, + resourceType: 'Dummy Resource Type', + rateType: 'hourly', + skills: [ + '1b585c26-2649-4078-8369-b599fe6a9d75', + 'bcfc8806-cae6-47ff-b0c9-f604acfc8c99' + ], + status: 'sourcing', + createdAt: '2020-11-11T04:17:23.131Z', + createdBy: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + id: '1d9e8c1a-e653-4d31-a799-2685e41da215' + }, + { + projectId: 9063, + externalId: '1212', + description: 'Dummy Description', + startDate: '2020-09-17T04:17:23.131Z', + endDate: '2020-10-19T04:17:23.131Z', + numPositions: 20, + resourceType: 'Dummy Resource Type', + rateType: 'hourly', + skills: [ + '8b757998-ff7d-4b3a-9fee-a49d3e41da03', + 'bcfc8806-cae6-47ff-b0c9-f604acfc8c99' + ], + status: 'sourcing', + createdAt: '2020-11-11T04:17:23.131Z', + createdBy: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + id: '1d9e8c1a-e653-4d31-a799-2685e41da214', + candidates: [ + { + jobId: '1d9e8c1a-e653-4d31-a799-2685e41da214', + userId: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + status: 'open', + createdAt: '2020-11-11T04:17:23.131Z', + createdBy: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + id: '2d9e8c1a-e653-4d31-a799-2685e41da214' + } + ] + }, + { + projectId: 9056, + externalId: '1212', + description: 'Dummy Description', + startDate: '2020-09-29T04:17:23.131Z', + endDate: '2020-10-17T04:17:23.131Z', + numPositions: 11, + resourceType: 'Dummy Resource Type', + rateType: 'hourly', + skills: [ + 'cb01fd31-e8d2-4e34-8bf3-b149705de3e1', + '59ee7b42-f3f3-48c9-bdca-e8396b241793', + 'bcfc8806-cae6-47ff-b0c9-f604acfc8c99' + ], + status: 'sourcing', + createdAt: '2020-11-11T04:17:23.131Z', + createdBy: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + id: '1d9e8c1a-e653-4d31-a799-2685e41da213', + candidates: [ + { + jobId: '1d9e8c1a-e653-4d31-a799-2685e41da213', + userId: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + status: 'open', + createdAt: '2020-11-11T04:17:23.131Z', + createdBy: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + id: '2d9e8c1a-e653-4d31-a799-2685e41da213' + } + ] + }, + { + projectId: 9050, + externalId: '1212', + description: 'Dummy Description', + startDate: '2020-09-27T04:17:23.131Z', + endDate: '2020-10-17T04:17:23.131Z', + numPositions: 13, + resourceType: 'Dummy Resource Type', + rateType: 'hourly', + skills: [ + 'cb01fd31-e8d2-4e34-8bf3-b149705de3e1', + '59ee7b42-f3f3-48c9-bdca-e8396b241793', + '1b585c26-2649-4078-8369-b599fe6a9d75', + '8b757998-ff7d-4b3a-9fee-a49d3e41da03', + 'bcfc8806-cae6-47ff-b0c9-f604acfc8c99' + ], + status: 'sourcing', + createdAt: '2020-11-11T04:17:23.131Z', + createdBy: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + id: '1d9e8c1a-e653-4d31-a799-2685e41da212', + candidates: [ + { + jobId: '1d9e8c1a-e653-4d31-a799-2685e41da212', + userId: '5bd69a82-c2cb-476f-9462-0883d3b28b90', + status: 'open', + createdAt: '2020-11-11T04:17:23.131Z', + createdBy: 'a55fe1bc-1754-45fa-9adc-cf3d6d7c377a', + id: '2d9e8c1a-e653-4d31-a799-2685e41da212' + } + ] + } +] + +const taasTeamItem0ResponseBody = { + id: 9050, + name: 'sample', + weeklyCount: 29.7, + resources: [ + { + id: '1b88e433-828b-4e0d-9fb5-ef75b9dcca6e', + handle: 'PE335869', + firstName: ' Peddaram', + lastName: ' Varalakshmi' + }, + { + id: '10803918-ded0-4906-9442-65dc8819de91', + handle: 'Gayathri_1994', + firstName: 'Gayathri', + lastName: 'Sekar', + photo_url: 'https://topcoder-dev-media.s3.amazonaws.com/member/profile/TonyJ-1604301092491.jpeg' + }, + { + id: '5bd69a82-c2cb-476f-9462-0883d3b28b90', + handle: 'Sandrine_Kuvalis98', + firstName: 'Mekhi', + lastName: 'Tremblay' + } + ], + startDate: new Date('2020-09-22T04:17:23.131Z'), + endDate: new Date('2021-11-11T04:17:23.131Z'), + totalPositions: 13 +} + +const taasTeam9050ResponseBody = { + id: 9050, + name: 'sample', + weeklyCount: 29.7, + resources: [ + { + id: '1b88e433-828b-4e0d-9fb5-ef75b9dcca6e', + handle: 'PE335869', + firstName: ' Peddaram', + lastName: ' Varalakshmi', + customerRate: 13, + job: { + id: '1d9e8c1a-e653-4d31-a799-2685e41da212', + name: 'Dummy Description' + }, + skills: [ + { + id: '5d313a7b-795b-42a2-9e7e-dc5e81c2f2b5', + name: 'Java' + }, + { + id: 'cb01fd31-e8d2-4e34-8bf3-b149705de3e1', + name: 'ACI Concrete Strength Testing Technician' + } + + ], + skillMatched: 1 + }, + { + id: '10803918-ded0-4906-9442-65dc8819de91', + handle: 'Gayathri_1994', + firstName: 'Gayathri', + lastName: 'Sekar', + customerRate: 14.5, + photo_url: 'https://topcoder-dev-media.s3.amazonaws.com/member/profile/TonyJ-1604301092491.jpeg', + job: { + id: '1d9e8c1a-e653-4d31-a799-2685e41da213', + name: 'Dummy Description' + }, + skills: [ + { + id: '5d313a7b-795b-42a2-9e7e-dc5e81c2f2b5', + name: 'Java' + }, + { + id: 'cb01fd31-e8d2-4e34-8bf3-b149705de3e1', + name: 'ACI Concrete Strength Testing Technician' + } + ], + skillMatched: 1 + }, + { + id: '5bd69a82-c2cb-476f-9462-0883d3b28b90', + handle: 'Sandrine_Kuvalis98', + firstName: 'Mekhi', + lastName: 'Tremblay', + customerRate: 16.7, + job: { + id: '1d9e8c1a-e653-4d31-a799-2685e41da214', + name: 'Dummy Description' + }, + skills: [ + { + id: '5d313a7b-795b-42a2-9e7e-dc5e81c2f2b5', + name: 'Java' + }, + { + id: 'cb01fd31-e8d2-4e34-8bf3-b149705de3e1', + name: 'ACI Concrete Strength Testing Technician' + } + ], + skillMatched: 1 + } + ], + startDate: new Date('2020-09-22T04:17:23.131Z'), + endDate: new Date('2021-11-11T04:17:23.131Z'), + jobs: [ + { + id: '1d9e8c1a-e653-4d31-a799-2685e41da212', + description: 'Dummy Description', + startDate: '2020-09-27T04:17:23.131Z', + endDate: '2020-10-17T04:17:23.131Z', + numPositions: 13, + rateType: 'hourly', + skills: [ + { + id: 'cb01fd31-e8d2-4e34-8bf3-b149705de3e1', + name: 'ACI Concrete Strength Testing Technician' + }, + { + id: '59ee7b42-f3f3-48c9-bdca-e8396b241793', + name: '2D Computer Graphics' + }, + { + id: '1b585c26-2649-4078-8369-b599fe6a9d75', + name: 'ACCP Certified' + }, + { + id: '8b757998-ff7d-4b3a-9fee-a49d3e41da03', + name: '3D Projection' + }, + { + id: 'bcfc8806-cae6-47ff-b0c9-f604acfc8c99', + name: '35 Mm Films' + } + ], + status: 'sourcing' + } + ] +} + +const jobDetailResponseBody = { + id: '1d9e8c1a-e653-4d31-a799-2685e41da212', + description: 'Dummy Description', + candidates: [ + { + id: '5bd69a82-c2cb-476f-9462-0883d3b28b90', + handle: 'Sandrine_Kuvalis98', + firstName: 'Mekhi', + lastName: 'Tremblay', + resumeLink: null, + status: 'open', + skills: [ + { + id: '5d313a7b-795b-42a2-9e7e-dc5e81c2f2b5', + name: 'Java' + }, + { + id: 'cb01fd31-e8d2-4e34-8bf3-b149705de3e1', + name: 'ACI Concrete Strength Testing Technician' + } + ], + job: { + id: '1d9e8c1a-e653-4d31-a799-2685e41da214', + name: 'Dummy Description' + }, + skillMatched: 1, + customerRate: 16.7 + } + ] +} + module.exports = { bookingManagerUser, connectUser, @@ -225,5 +775,15 @@ module.exports = { resourceBookingResponseBody, fullyUpdateResourceBookingRequestBody, partiallyUpdateResourceBookingRequestBody, - unexpected + unexpected, + projectRequestBody, + userRequestBody, + memberRequestBody, + skillsRequestBody, + userSkillsRequestBody, + resourceBookingsRequestBody, + jobsRequestBody, + taasTeamItem0ResponseBody, + taasTeam9050ResponseBody, + jobDetailResponseBody } diff --git a/test/unit/helper.test.js b/test/unit/helper.test.js index 7f0fb777..5c4b3141 100644 --- a/test/unit/helper.test.js +++ b/test/unit/helper.test.js @@ -142,7 +142,7 @@ describe('helper test', () => { }) }) - describe('getUserId test', () => { + describe('isConnectMember test', () => { it('isConnectMember return true', async () => { sinon.stub(request, 'get').callsFake(() => { return { @@ -199,7 +199,7 @@ describe('helper test', () => { expect(res).to.equal(id) }) - it('getUserId return id', async () => { + it('getUserId catch not found', async () => { let i = 0 sinon.stub(request, 'get').callsFake(() => { return { @@ -222,8 +222,137 @@ describe('helper test', () => { try { await helper.getUserId(44532) } catch (err) { - expect(err.message).to.equal('user id not found') + expect(err.message).to.equal('userId: 44532 "user" not found') } }) }) + + describe('getProjects test', () => { + it('getProjects return entity result', async () => { + let i = 0 + sinon.stub(request, 'get').callsFake(() => { + return { + set: function () { + i++ + if (i === 3) { + return { + body: [{ id: 1001, name: 'name' }] + } + } + return this + } + } + }) + const res = await helper.getProjects('token') + expect(res).to.be.a('array') + }) + }) + + describe('getProjectById test', () => { + it('getProjectById return entity result', async () => { + let i = 0 + sinon.stub(request, 'get').callsFake(() => { + return { + set: function () { + i++ + if (i === 3) { + return { + body: { id: 1001, name: 'name' } + } + } + return this + } + } + }) + + const res = await helper.getProjectById('token', 1001) + expect(res).to.be.a('object') + }) + }) + + describe('getUsers test', () => { + it('getUsers return entity result', async () => { + let i = 0 + sinon.stub(request, 'get').callsFake(() => { + return { + set: function () { + i++ + if (i === 3) { + return { + body: [{ id: '1001', handle: 'handle', firstName: 'Bill', lastName: 'Gate' }] + } + } + return this + } + } + }) + + const res = await helper.getUsers('token') + expect(res).to.be.a('array') + }) + }) + + describe('getMembers test', () => { + it('getMembers return entity result', async () => { + let i = 0 + sinon.stub(request, 'get').callsFake(() => { + return { + set: function () { + i++ + if (i === 3) { + return { + body: { id: '1001', name: 'name' } + } + } + return this + } + } + }) + + const res = await helper.getMembers('token', ['name1', 'name2']) + expect(res).to.be.a('object') + }) + }) + + describe('getSkills test', () => { + it('getSkills return entity result', async () => { + let i = 0 + sinon.stub(request, 'get').callsFake(() => { + return { + set: function () { + i++ + if (i === 3) { + return { + body: [{ id: 1001, name: 'name' }] + } + } + return this + } + } + }) + const res = await helper.getSkills('token') + expect(res).to.be.a('array') + }) + }) + + describe('getUserSkill test', () => { + it('getUserSkill return entity result', async () => { + let i = 0 + sinon.stub(request, 'get').callsFake(() => { + return { + set: function () { + i++ + if (i === 3) { + return { + body: [{ id: 1001, skill: { name: 'name' } }] + } + } + return this + } + } + }) + const res = await helper.getUserSkill('token', '1001') + expect(res).to.be.a('array') + }) + }) })