From ffd1a2ff4b295300f618eeb79b76a377709155ee Mon Sep 17 00:00:00 2001 From: imcaizheng Date: Wed, 27 Jan 2021 06:20:38 +0800 Subject: [PATCH 01/46] allow setting status during Job/JobCandidate/ResourceBooking creation --- docs/swagger.yaml | 15 +++++++++++++++ .../ResourceBookingEventHandler.js | 17 +++++++++++++++-- src/eventHandlers/index.js | 1 + src/services/JobCandidateService.js | 2 +- src/services/JobService.js | 2 +- src/services/ResourceBookingService.js | 2 +- 6 files changed, 34 insertions(+), 5 deletions(-) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 3cd72f7b..a4397b30 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1734,6 +1734,11 @@ components: example: "Dummy title" description: "The title." maxLength: 64 + status: + type: string + enum: ['sourcing', 'in-review', 'assigned', 'closed', 'cancelled'] + description: "The job status." + default: sourcing startDate: type: string format: date-time @@ -1829,6 +1834,11 @@ components: format: uuid example: "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a" description: "The user id." + status: + type: string + enum: ['open', 'selected', 'shortlist', 'rejected', 'cancelled'] + description: "The job candidate status." + default: open externalId: type: string example: "1212" @@ -1983,6 +1993,11 @@ components: type: string format: uuid description: "The job id." + status: + type: string + enum: ['sourcing', 'in-review', 'assigned', 'closed', 'cancelled'] + description: "The job status." + default: sourcing startDate: type: string format: date-time diff --git a/src/eventHandlers/ResourceBookingEventHandler.js b/src/eventHandlers/ResourceBookingEventHandler.js index 78cf211e..bc023e9d 100644 --- a/src/eventHandlers/ResourceBookingEventHandler.js +++ b/src/eventHandlers/ResourceBookingEventHandler.js @@ -3,6 +3,7 @@ */ const { Op } = require('sequelize') +const _ = require('lodash') const models = require('../models') const logger = require('../common/logger') const helper = require('../common/helper') @@ -18,7 +19,7 @@ const JobCandidateService = require('../services/JobCandidateService') * @returns {undefined} */ async function selectJobCandidate (payload) { - if (payload.value.status === payload.options.oldValue.status) { + if (_.get(payload, 'options.oldValue') && payload.value.status === payload.options.oldValue.status) { logger.debug({ component: 'ResourceBookingEventHandler', context: 'selectJobCandidate', @@ -73,7 +74,7 @@ async function selectJobCandidate (payload) { * @returns {undefined} */ async function assignJob (payload) { - if (payload.value.status === payload.options.oldValue.status) { + if (_.get(payload, 'options.oldValue') && payload.value.status === payload.options.oldValue.status) { logger.debug({ component: 'ResourceBookingEventHandler', context: 'assignJob', @@ -125,6 +126,17 @@ async function assignJob (payload) { } } +/** + * Process resource booking create event. + * + * @param {Object} payload the event payload + * @returns {undefined} + */ +async function processCreate (payload) { + await selectJobCandidate(payload) + await assignJob(payload) +} + /** * Process resource booking update event. * @@ -137,5 +149,6 @@ async function processUpdate (payload) { } module.exports = { + processCreate, processUpdate } diff --git a/src/eventHandlers/index.js b/src/eventHandlers/index.js index 6de47dc3..f4089ea0 100644 --- a/src/eventHandlers/index.js +++ b/src/eventHandlers/index.js @@ -12,6 +12,7 @@ const logger = require('../common/logger') const TopicOperationMapping = { [config.TAAS_JOB_UPDATE_TOPIC]: JobEventHandler.processUpdate, [config.TAAS_JOB_CANDIDATE_CREATE_TOPIC]: JobCandidateEventHandler.processCreate, + [config.TAAS_RESOURCE_BOOKING_CREATE_TOPIC]: ResourceBookingEventHandler.processCreate, [config.TAAS_RESOURCE_BOOKING_UPDATE_TOPIC]: ResourceBookingEventHandler.processUpdate } diff --git a/src/services/JobCandidateService.js b/src/services/JobCandidateService.js index 57e8a3d8..7153ebb4 100644 --- a/src/services/JobCandidateService.js +++ b/src/services/JobCandidateService.js @@ -92,7 +92,6 @@ async function createJobCandidate (currentUser, jobCandidate) { jobCandidate.id = uuid() jobCandidate.createdAt = new Date() jobCandidate.createdBy = await helper.getUserId(currentUser.userId) - jobCandidate.status = 'open' const created = await JobCandidate.create(jobCandidate) await helper.postEvent(config.TAAS_JOB_CANDIDATE_CREATE_TOPIC, jobCandidate) @@ -102,6 +101,7 @@ async function createJobCandidate (currentUser, jobCandidate) { createJobCandidate.schema = Joi.object().keys({ currentUser: Joi.object().required(), jobCandidate: Joi.object().keys({ + status: Joi.jobCandidateStatus().default('open'), jobId: Joi.string().uuid().required(), userId: Joi.string().uuid().required(), externalId: Joi.string(), diff --git a/src/services/JobService.js b/src/services/JobService.js index b298dfaf..e50e790a 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -152,7 +152,6 @@ async function createJob (currentUser, job) { job.id = uuid() job.createdAt = new Date() job.createdBy = await helper.getUserId(currentUser.userId) - job.status = 'sourcing' const created = await Job.create(job) await helper.postEvent(config.TAAS_JOB_CREATE_TOPIC, job) @@ -162,6 +161,7 @@ async function createJob (currentUser, job) { createJob.schema = Joi.object().keys({ currentUser: Joi.object().required(), job: Joi.object().keys({ + status: Joi.jobStatus().default('sourcing'), projectId: Joi.number().integer().required(), externalId: Joi.string(), description: Joi.string(), diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index 9f2a63cf..095b86b2 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -105,7 +105,6 @@ async function createResourceBooking (currentUser, resourceBooking) { resourceBooking.id = uuid() resourceBooking.createdAt = new Date() resourceBooking.createdBy = await helper.getUserId(currentUser.userId) - resourceBooking.status = 'sourcing' const created = await ResourceBooking.create(resourceBooking) await helper.postEvent(config.TAAS_RESOURCE_BOOKING_CREATE_TOPIC, resourceBooking) @@ -115,6 +114,7 @@ async function createResourceBooking (currentUser, resourceBooking) { createResourceBooking.schema = Joi.object().keys({ currentUser: Joi.object().required(), resourceBooking: Joi.object().keys({ + status: Joi.jobStatus().default('sourcing'), projectId: Joi.number().integer().required(), userId: Joi.string().uuid().required(), jobId: Joi.string().uuid(), From 10749dde1cc644db16c81f7d8c5fbc9bf61812b4 Mon Sep 17 00:00:00 2001 From: imcaizheng Date: Sat, 30 Jan 2021 10:28:07 +0800 Subject: [PATCH 02/46] Winner submission for Topcoder TaaS API - Improve Local Setup --- README.md | 252 ++++++++++++------ config/default.js | 43 ++- local/docker-compose.yaml | 99 +++++++ local/generic-tc-service/Dockerfile | 15 ++ local/generic-tc-service/docker-entrypoint.sh | 13 + local/kafka-client/Dockerfile | 5 + local/kafka-client/create-topics.sh | 9 + local/kafka-client/topics.txt | 9 + .../2021-01-13-make-some-job-fields-longer.js | 8 +- package-lock.json | 70 ++++- package.json | 24 +- src/models/Job.js | 2 +- 12 files changed, 454 insertions(+), 95 deletions(-) create mode 100644 local/docker-compose.yaml create mode 100644 local/generic-tc-service/Dockerfile create mode 100755 local/generic-tc-service/docker-entrypoint.sh create mode 100644 local/kafka-client/Dockerfile create mode 100755 local/kafka-client/create-topics.sh create mode 100644 local/kafka-client/topics.txt diff --git a/README.md b/README.md index 4c142f0a..0e2aeaaf 100644 --- a/README.md +++ b/README.md @@ -5,68 +5,11 @@ - nodejs https://nodejs.org/en/ (v12+) - PostgreSQL - ElasticSearch (7.x) -- Docker +- Zookeeper +- Kafka +- Docker(version 20.10 and above) - Docker-Compose -## Configuration - -Configuration for the application is at `config/default.js`. - -The following parameters can be set in config files or in env variables: - -- `LOG_LEVEL`: the log level, default is 'debug' -- `PORT`: the server port, default is 3000 -- `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_UBAHN`: Auth0 audience for U-Bahn -- `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 - -- `m2m.M2M_AUDIT_USER_ID`: default value is `00000000-0000-0000-0000-000000000000` -- `m2m.M2M_AUDIT_HANDLE`: default value is `TopcoderService` - -- `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 -- `TOPCODER_SKILL_PROVIDER_ID`: the referenced skill provider id - -- `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 force` to force creating table) - ## DB Migration - `npm run migrate`: run any migration files which haven't run yet. - `npm run migrate:undo`: revert most recent migration. @@ -80,27 +23,186 @@ The following parameters can be set in the config file or via env variables: - `database`: set via env `DB_NAME`; datebase name - `host`: set via env `DB_HOST`; datebase host name -## ElasticSearch Setup -- Go to https://www.elastic.co/downloads/ download and install the elasticsearch. -- Modify `esConfig` under `config/default.js` to meet your environment. -- Run `npm run create-index` to create ES index. -- Run `npm run delete-index` to delete ES index. +### Steps to run locally +1. 📦 Install npm dependencies + + ```bash + npm install + ``` + +2. ⚙ Local config + + 1. In the root directory create `.env` file with the next environment variables. Values for **Auth0 config** should be shared with you on the forum.
+ ```bash + # Auth0 config + AUTH0_URL= + AUTH0_AUDIENCE= + AUTH0_AUDIENCE_UBAHN= + AUTH0_CLIENT_ID= + AUTH0_CLIENT_SECRET= + AUTH0_PROXY_SERVER_URL= + + # Locally deployed services (via docker-compose) + ES_HOST=http://dockerhost:9200 + DATABASE_URL=postgres://postgres:postgres@dockerhost:5432/postgres + BUSAPI_URL=http://dockerhost:8002/v5 + ``` + + - Values from this file would be automatically used by many `npm` commands. + - ⚠️ Never commit this file or its copy to the repository! + + 1. Set `dockerhost` to point the IP address of Docker. Docker IP address depends on your system. For example if docker is run on IP `127.0.0.1` add a the next line to your `/etc/hosts` file: + ``` + 127.0.0.1 dockerhost + ``` + + Alternatively, you may update `.env` file and replace `dockerhost` with your docker IP address. + +1. 🚢 Start docker-compose with services which are required to start Taas API locally + + *(NOTE Please ensure that you have installed docker of version 20.10 or above since the docker-compose file uses new feature introduced by docker version 20.10. Run `docker --version` to check your docker version.)* -## Local Deployment + ```bash + npm run services:up + ``` -- Install dependencies `npm install` -- Run lint `npm run lint` -- Run lint fix `npm run lint:fix` -- Clear and init db `npm run init-db force` -- Clear and create es index + Wait until all containers are fully started. As a good indicator, wait until `es-processor` successfully started by viewing its logs: + + ```bash + npm run services:logs -- -f es-processor + ``` + +
🖱️ Click to see a good logs example ``` bash - npm run delete-index # run this if you already created index - npm run create-index + tc-taas-es-processor | Waiting for kafka-client to exit.... + tc-taas-es-processor | kafka-client exited! + tc-taas-es-processor | + tc-taas-es-processor | > taas-es-processor@1.0.0 start /opt/app + tc-taas-es-processor | > node src/app.js + tc-taas-es-processor | + tc-taas-es-processor | [2021-01-21T02:44:43.442Z] app INFO : Starting kafka consumer + tc-taas-es-processor | 2021-01-21T02:44:44.534Z INFO no-kafka-client Joined group taas-es-processor generationId 1 as no-kafka-client-70c25a43-af93-495e-a123-0c4f4ea389eb + tc-taas-es-processor | 2021-01-21T02:44:44.534Z INFO no-kafka-client Elected as group leader + tc-taas-es-processor | 2021-01-21T02:44:44.614Z DEBUG no-kafka-client Subscribed to taas.jobcandidate.create:0 offset 0 leader kafka:9093 + tc-taas-es-processor | 2021-01-21T02:44:44.615Z DEBUG no-kafka-client Subscribed to taas.job.create:0 offset 0 leader kafka:9093 + tc-taas-es-processor | 2021-01-21T02:44:44.615Z DEBUG no-kafka-client Subscribed to taas.resourcebooking.delete:0 offset 0 leader kafka:9093 + tc-taas-es-processor | 2021-01-21T02:44:44.616Z DEBUG no-kafka-client Subscribed to taas.jobcandidate.delete:0 offset 0 leader kafka:9093 + tc-taas-es-processor | 2021-01-21T02:44:44.616Z DEBUG no-kafka-client Subscribed to taas.jobcandidate.update:0 offset 0 leader kafka:9093 + tc-taas-es-processor | 2021-01-21T02:44:44.617Z DEBUG no-kafka-client Subscribed to taas.resourcebooking.create:0 offset 0 leader kafka:9093 + tc-taas-es-processor | 2021-01-21T02:44:44.617Z DEBUG no-kafka-client Subscribed to taas.job.delete:0 offset 0 leader kafka:9093 + tc-taas-es-processor | 2021-01-21T02:44:44.618Z DEBUG no-kafka-client Subscribed to taas.job.update:0 offset 0 leader kafka:9093 + tc-taas-es-processor | 2021-01-21T02:44:44.618Z DEBUG no-kafka-client Subscribed to taas.resourcebooking.update:0 offset 0 leader kafka:9093 + tc-taas-es-processor | [2021-01-21T02:44:44.619Z] app INFO : Initialized....... + tc-taas-es-processor | [2021-01-21T02:44:44.623Z] app INFO : taas.job.create,taas.job.update,taas.job.delete,taas.jobcandidate.create,taas.jobcandidate.update,taas.jobcandidate.delete,taas.resourcebooking.create,taas.resourcebooking.update,taas.resourcebooking.delete + tc-taas-es-processor | [2021-01-21T02:44:44.623Z] app INFO : Kick Start....... + tc-taas-es-processor | ********** Topcoder Health Check DropIn listening on port 3001 + tc-taas-es-processor | Topcoder Health Check DropIn started and ready to roll ``` -- Start app `npm start` -- App is running at `http://localhost:3000` +
+ + If you want to learn more about docker-compose configuration +
🖱️ Click to see more details here +
+ + This docker-compose file starts the next services: + | Service | Name | Port | + |----------|:-----:|:----:| + | PostgreSQL | db | 5432 | + | Elasticsearch | esearch | 9200 | + | Zookeeper | zookeeper | 2181 | + | Kafka | kafka | 9092 | + | [tc-bus-api](https://github.com/topcoder-platform/tc-bus-api) | bus-api | 8002 | + | [taas-es-processor](https://github.com/topcoder-platform/taas-es-processor) | es-processor | 5000 | + + - as many of the Topcoder services in this docker-compose require Auth0 configuration for M2M calls, our docker-compose file passes environment variables `AUTH0_CLIENT_ID`, `AUTH0_CLIENT_SECRET`, `AUTH0_URL`, `AUTH0_AUDIENCE`, `AUTH0_PROXY_SERVER_URL` to its containers. docker-compose takes them from `.env` file if provided. + + - `docker-compose` automatically would create Kafka topics which are used by `taas-apis` listed in `./local/kafka-client/topics.txt`. + + - To view the logs from any container inside docker-compose use the following command, replacing `SERVICE_NAME` with the corresponding value under the **Name** column in the above table: + + ```bash + npm run services:logs -- -f SERVICE_NAME + ``` + + - If you want to modify the code of any of the services which are run inside this docker-compose file, you can stop such service inside docker-compose by command `docker-compose -f local/docker-compose.yaml stop ` and run the service separately, following its README file.

+ *NOTE: If kafka(along with zookeeper) is stopped and brings up in the host machine you will need to restart the `es-processor` service by running `docker-compose -f local/docker-compose.yaml restart es-processor` so the processor will connect with the new zookeeper.* + + *NOTE: In production these dependencies / services are hosted & managed outside Taas API.* + +2. ♻ Init DB and ES + + ```bash + npm run local:init + ``` + + This command will do 2 things: + - create Database tables + - create Elasticsearch indexes + +3. 🚀 Start Taas API + + ```bash + npm run dev + ``` + + Runs the Taas API using nodemon, so it would be restarted after any of the files is updated. + The API will be served on `http://localhost:3000`. + +## NPM Commands + +| Command | Description | +| -- | -- | +| `npm start` | Start app. | +| `npm run dev` | Start app using `nodemon`. | +| `npm run lint` | Check for for lint errors. | +| `npm run lint:fix` | Check for for lint errors and fix error automatically when possible. | +| `npm run services:up` | Start services via docker-compose for local development. | +| `npm run services:down` | Stop services via docker-compose for local development. | +| `npm run services:logs -- -f ` | View logs of some service inside docker-compose. | +| `npm run local:init` | Create Database and Elasticsearch indexes. | +| `npm run init-db` | Create database. | +| `npm run init-db force` | Force re-creating database. | +| `npm run create-index` | Create Elasticsearch indexes. | +| `npm run delete-index` | Delete Elasticsearch indexes. | +| `npm run migrate` | Run DB migration. | +| `npm run migrate:undo` | Undo DB migration executed previously | +| `npm run test-data` | Insert test data. | +| `npm run test` | Run tests. | +| `npm run cov` | Run test with coverage. | + +## Kafka Commands + +You can use the following commands to manipulate kafka topics and messages: + +(Replace `TOPIC_NAME` with the name of the desired topic) + +### Create Topic + +```bash +docker exec tc-taas-kafka /opt/kafka/bin/kafka-topics.sh --create --zookeeper zookeeper:2181 --partitions 1 --replication-factor 1 --topic TOPIC_NAME +``` + +### List Topics + +```bash +docker exec tc-taas-kafka /opt/kafka/bin/kafka-topics.sh --list --zookeeper zookeeper:2181 +``` + +### Watch Topic + +```bash +docker exec tc-taas-kafka /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic TOPIC_NAME +``` + +### Post Message to Topic (from stdin) + +```bash +docker exec -it tc-taas-kafka /opt/kafka/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic TOPIC_NAME +``` + +- Enter or copy/paste the message into the console after starting this command. ## Local Deployment with Docker diff --git a/config/default.js b/config/default.js index d726ca58..578c1484 100644 --- a/config/default.js +++ b/config/default.js @@ -1,63 +1,104 @@ -require('dotenv').config() module.exports = { + // the log level LOG_LEVEL: process.env.LOG_LEVEL || 'debug', + // the server port PORT: process.env.PORT || 3000, + // the server api base path BASE_PATH: process.env.BASE_PATH || '/api/v5', + // The authorization secret used during token verification. AUTH_SECRET: process.env.AUTH_SECRET || 'mysecret', + // The valid issuer of tokens, a json array contains valid issuer. 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, used to get TC M2M token AUTH0_URL: process.env.AUTH0_URL, + // Auth0 audience, used to get TC M2M token AUTH0_AUDIENCE: process.env.AUTH0_AUDIENCE, + // Auth0 audience for U-Bahn AUTH0_AUDIENCE_UBAHN: process.env.AUTH0_AUDIENCE_UBAHN, + // Auth0 token cache time, used to get TC M2M token TOKEN_CACHE_TIME: process.env.TOKEN_CACHE_TIME, + // Auth0 client id, used to get TC M2M token AUTH0_CLIENT_ID: process.env.AUTH0_CLIENT_ID, + // Auth0 client secret, used to get TC M2M token AUTH0_CLIENT_SECRET: process.env.AUTH0_CLIENT_SECRET, + // Proxy Auth0 URL, used to get TC M2M token AUTH0_PROXY_SERVER_URL: process.env.AUTH0_PROXY_SERVER_URL, m2m: { + // default user ID for m2m user M2M_AUDIT_USER_ID: process.env.M2M_AUDIT_USER_ID || '00000000-0000-0000-0000-000000000000', + // default handle name for m2m user M2M_AUDIT_HANDLE: process.env.M2M_AUDIT_HANDLE || 'TopcoderService' }, + // the Topcoder v5 url TC_API: process.env.TC_API || 'https://api.topcoder-dev.com/v5', + // the organization id ORG_ID: process.env.ORG_ID || '36ed815b-3da1-49f1-a043-aaed0a4e81ad', + // the referenced skill provider id TOPCODER_SKILL_PROVIDER_ID: process.env.TOPCODER_SKILL_PROVIDER_ID || '9cc0795a-6e12-4c84-9744-15858dba1861', + // the TC API for v3 users TOPCODER_USERS_API: process.env.TOPCODER_USERS_API || 'https://api.topcoder-dev.com/v3/users', + // PostgreSQL database url. DATABASE_URL: process.env.DATABASE_URL || 'postgres://postgres:postgres@localhost:5432/postgres', + // string - PostgreSQL database target schema DB_SCHEMA_NAME: process.env.DB_SCHEMA_NAME || 'bookings', + // the project service url PROJECT_API_URL: process.env.PROJECT_API_URL || 'https://api.topcoder-dev.com', esConfig: { + // the elasticsearch host HOST: process.env.ES_HOST || 'http://localhost:9200', ELASTICCLOUD: { + // 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 id: process.env.ELASTICCLOUD_ID, + // The elastic cloud username for basic authentication. Provide this only if your elasticsearch instance is hosted on elastic cloud username: process.env.ELASTICCLOUD_USERNAME, + // The elastic cloud password for basic authentication. Provide this only if your elasticsearch instance is hosted on elastic cloud password: process.env.ELASTICCLOUD_PASSWORD }, + // The Amazon region to use when using AWS Elasticsearch service AWS_REGION: process.env.AWS_REGION || 'us-east-1', // AWS Region to be used if we use AWS ES + // the job index ES_INDEX_JOB: process.env.ES_INDEX_JOB || 'job', + // // 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 + // the job candidate index ES_INDEX_JOB_CANDIDATE: process.env.ES_INDEX_JOB_CANDIDATE || 'job_candidate', + // the resource booking index ES_INDEX_RESOURCE_BOOKING: process.env.ES_INDEX_RESOURCE_BOOKING || 'resource_booking' }, + // Topcoder Bus API URL BUSAPI_URL: process.env.BUSAPI_URL || 'https://api.topcoder-dev.com/v5', + // The error topic at which bus api will publish any errors KAFKA_ERROR_TOPIC: process.env.KAFKA_ERROR_TOPIC || 'common.error.reporting', + // The originator value for the kafka messages KAFKA_MESSAGE_ORIGINATOR: process.env.KAFKA_MESSAGE_ORIGINATOR || 'taas-api', // topics for job service + // the create job entity Kafka message topic TAAS_JOB_CREATE_TOPIC: process.env.TAAS_JOB_CREATE_TOPIC || 'taas.job.create', + // the update job entity Kafka message topic TAAS_JOB_UPDATE_TOPIC: process.env.TAAS_JOB_UPDATE_TOPIC || 'taas.job.update', + // the delete job entity Kafka message topic TAAS_JOB_DELETE_TOPIC: process.env.TAAS_JOB_DELETE_TOPIC || 'taas.job.delete', // topics for jobcandidate service + // the create job candidate entity Kafka message topic TAAS_JOB_CANDIDATE_CREATE_TOPIC: process.env.TAAS_JOB_CANDIDATE_CREATE_TOPIC || 'taas.jobcandidate.create', + // the update job candidate entity Kafka message topic TAAS_JOB_CANDIDATE_UPDATE_TOPIC: process.env.TAAS_JOB_CANDIDATE_UPDATE_TOPIC || 'taas.jobcandidate.update', + // the delete job candidate entity Kafka message topic TAAS_JOB_CANDIDATE_DELETE_TOPIC: process.env.TAAS_JOB_CANDIDATE_DELETE_TOPIC || 'taas.jobcandidate.delete', // topics for job service + // the create resource booking entity Kafka message topic TAAS_RESOURCE_BOOKING_CREATE_TOPIC: process.env.TAAS_RESOURCE_BOOKING_CREATE_TOPIC || 'taas.resourcebooking.create', + // the update resource booking entity Kafka message topic TAAS_RESOURCE_BOOKING_UPDATE_TOPIC: process.env.TAAS_RESOURCE_BOOKING_UPDATE_TOPIC || 'taas.resourcebooking.update', + // the delete resource booking entity Kafka message topic TAAS_RESOURCE_BOOKING_DELETE_TOPIC: process.env.TAAS_RESOURCE_BOOKING_DELETE_TOPIC || 'taas.resourcebooking.delete' } diff --git a/local/docker-compose.yaml b/local/docker-compose.yaml new file mode 100644 index 00000000..1709207e --- /dev/null +++ b/local/docker-compose.yaml @@ -0,0 +1,99 @@ +version: '2.4' +services: + zookeeper: + image: wurstmeister/zookeeper + container_name: tc-taas-zookeeper + ports: + - "2181:2181" + environment: + zk_id: "1" + kafka: + image: wurstmeister/kafka + container_name: tc-taas-kafka + ports: + - "9092:9092" + depends_on: + - zookeeper + environment: + KAFKA_ADVERTISED_LISTENERS: INSIDE://kafka:9093,OUTSIDE://localhost:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT + KAFKA_LISTENERS: INSIDE://0.0.0.0:9093,OUTSIDE://0.0.0.0:9092 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE + esearch: + image: elasticsearch:7.7.1 + container_name: tc-taas-es + ports: + - "9200:9200" + environment: + - discovery.type=single-node + db: + image: postgres + container_name: tc-taas-postgres + ports: + - "5432:5432" + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + kafka-client: + build: ./kafka-client + container_name: tc-taas-kafka-client + extra_hosts: + - "host.docker.internal:host-gateway" + bus-api: + container_name: tc-taas-bus-api + build: + context: ./generic-tc-service + args: + NODE_VERSION: 8.11.3 + GIT_URL: https://github.com/topcoder-platform/tc-bus-api + GIT_BRANCH: dev + BYPASS_TOKEN_VALIDATION: 1 + command: start kafka-client + expose: + - "3000" + ports: + - "8002:3000" + depends_on: + - kafka-client + environment: + - PORT=3000 + - KAFKA_URL=http://host.docker.internal:9092 + - JWT_TOKEN_SECRET=secret + - VALID_ISSUERS="[\"https:\/\/topcoder-newauth.auth0.com\/\",\"https:\/\/api.topcoder-dev.com\",\"https:\/\/topcoder-dev.auth0.com\/\"]" + - AUTH0_CLIENT_ID + - AUTH0_CLIENT_SECRET + - AUTH0_URL + - AUTH0_AUDIENCE + - AUTH0_PROXY_SERVER_URL + extra_hosts: + - "host.docker.internal:host-gateway" + - "localhost:host-gateway" + es-processor: + container_name: tc-taas-es-processor + build: + context: ./generic-tc-service + args: + NODE_VERSION: 12 + GIT_URL: https://github.com/topcoder-platform/taas-es-processor + GIT_BRANCH: dev + BYPASS_TOKEN_VALIDATION: 0 + command: start kafka-client + depends_on: + - kafka-client + expose: + - "3001" + ports: + - "5000:3001" + environment: + - PORT=3001 + - KAFKA_URL=http://host.docker.internal:9092 + - ES_HOST=http://host.docker.internal:9200 + - AUTH0_CLIENT_ID + - AUTH0_CLIENT_SECRET + - AUTH0_URL + - AUTH0_AUDIENCE + - AUTH0_PROXY_SERVER_URL + extra_hosts: + - "host.docker.internal:host-gateway" + - "localhost:host-gateway" diff --git a/local/generic-tc-service/Dockerfile b/local/generic-tc-service/Dockerfile new file mode 100644 index 00000000..e3113c7f --- /dev/null +++ b/local/generic-tc-service/Dockerfile @@ -0,0 +1,15 @@ +ARG NODE_VERSION=8.11.3 + +FROM node:$NODE_VERSION +ARG GIT_URL +ARG GIT_BRANCH +ARG BYPASS_TOKEN_VALIDATION + +RUN git clone $GIT_URL /opt/app +WORKDIR /opt/app +RUN git checkout -b node-branch origin/$GIT_BRANCH + +RUN npm install +RUN if [ $BYPASS_TOKEN_VALIDATION -eq 1 ]; then sed -i '/decodedToken = jwt.decode/a \ callback(undefined, decodedToken.payload); return;' node_modules/tc-core-library-js/lib/auth/verifier.js; fi +COPY docker-entrypoint.sh /opt/ +ENTRYPOINT ["/opt/docker-entrypoint.sh"] diff --git a/local/generic-tc-service/docker-entrypoint.sh b/local/generic-tc-service/docker-entrypoint.sh new file mode 100755 index 00000000..24cb1fe8 --- /dev/null +++ b/local/generic-tc-service/docker-entrypoint.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +if [ $# -eq 2 ]; then + echo "Waiting for $2 to exit...." + while ping -c1 $2 &>/dev/null + do + sleep 1 + done + echo "$2 exited!" +fi + +tail -n+3 /etc/hosts > /tmp/hosts && cp /tmp/hosts /etc/hosts # remove default localhost +cd /opt/app/ && npm run $1 diff --git a/local/kafka-client/Dockerfile b/local/kafka-client/Dockerfile new file mode 100644 index 00000000..15c20839 --- /dev/null +++ b/local/kafka-client/Dockerfile @@ -0,0 +1,5 @@ +From wurstmeister/kafka +WORKDIR /app/ +COPY topics.txt . +COPY create-topics.sh . +ENTRYPOINT ["/app/create-topics.sh"] diff --git a/local/kafka-client/create-topics.sh b/local/kafka-client/create-topics.sh new file mode 100755 index 00000000..df00f7ea --- /dev/null +++ b/local/kafka-client/create-topics.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +until /opt/kafka/bin/kafka-topics.sh --list --zookeeper host.docker.internal:2181 > exists-topics.txt + do sleep 1 +done + +while read topic; do + /opt/kafka/bin/kafka-topics.sh --create --if-not-exists --zookeeper host.docker.internal:2181 --partitions 1 --replication-factor 1 --topic $topic +done < <(sort topics.txt exists-topics.txt exists-topics.txt | uniq -u) diff --git a/local/kafka-client/topics.txt b/local/kafka-client/topics.txt new file mode 100644 index 00000000..a392a8fc --- /dev/null +++ b/local/kafka-client/topics.txt @@ -0,0 +1,9 @@ +taas.job.create +taas.jobcandidate.create +taas.resourcebooking.create +taas.job.update +taas.jobcandidate.update +taas.resourcebooking.update +taas.job.delete +taas.jobcandidate.delete +taas.resourcebooking.delete diff --git a/migrations/2021-01-13-make-some-job-fields-longer.js b/migrations/2021-01-13-make-some-job-fields-longer.js index 286fd888..7b2fc2b4 100644 --- a/migrations/2021-01-13-make-some-job-fields-longer.js +++ b/migrations/2021-01-13-make-some-job-fields-longer.js @@ -7,14 +7,14 @@ module.exports = { up: queryInterface => { return Promise.all([ - queryInterface.sequelize.query(`ALTER TABLE bookings.jobs ALTER COLUMN title TYPE VARCHAR(128)`), - queryInterface.sequelize.query(`ALTER TABLE bookings.jobs ALTER COLUMN description TYPE TEXT`) + queryInterface.sequelize.query('ALTER TABLE bookings.jobs ALTER COLUMN title TYPE VARCHAR(128)'), + queryInterface.sequelize.query('ALTER TABLE bookings.jobs ALTER COLUMN description TYPE TEXT') ]) }, down: queryInterface => { return Promise.all([ - queryInterface.sequelize.query(`ALTER TABLE bookings.jobs ALTER COLUMN title TYPE VARCHAR(64)`), - queryInterface.sequelize.query(`ALTER TABLE bookings.jobs ALTER COLUMN description TYPE VARCHAR(255)`) + queryInterface.sequelize.query('ALTER TABLE bookings.jobs ALTER COLUMN title TYPE VARCHAR(64)'), + queryInterface.sequelize.query('ALTER TABLE bookings.jobs ALTER COLUMN description TYPE VARCHAR(255)') ]) } } diff --git a/package-lock.json b/package-lock.json index 5b90459b..d904d878 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1796,7 +1796,69 @@ "dotenv": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", - "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==", + "dev": true + }, + "dotenv-cli": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-4.0.0.tgz", + "integrity": "sha512-ByKEec+ashePEXthZaA1fif9XDtcaRnkN7eGdBDx3HHRjwZ/rA1go83Cbs4yRrx3JshsCf96FjAyIA2M672+CQ==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.1", + "dotenv": "^8.1.0", + "dotenv-expand": "^5.1.0", + "minimist": "^1.1.3" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", + "dev": true }, "dottie": { "version": "2.0.2", @@ -3115,9 +3177,9 @@ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, "inquirer": { diff --git a/package.json b/package.json index 15788a91..1df401a2 100644 --- a/package.json +++ b/package.json @@ -5,17 +5,21 @@ "main": "app.js", "scripts": { "start": "node app.js", - "dev": "nodemon app.js", + "dev": "dotenv nodemon app.js", "lint": "standard", "lint:fix": "standard --fix", - "init-db": "node src/init-db.js", - "create-index": "node scripts/createIndex.js", - "delete-index": "node scripts/deleteIndex.js", - "migrate": "npx sequelize db:migrate", - "migrate:undo": "npx sequelize db:migrate:undo", - "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" + "services:up": "docker-compose -f local/docker-compose.yaml up -d", + "services:down": "docker-compose -f local/docker-compose.yaml down", + "services:logs": "docker-compose -f local/docker-compose.yaml logs", + "local:init": "npm run init-db && npm run create-index", + "init-db": "dotenv node src/init-db.js", + "create-index": "dotenv node scripts/createIndex.js", + "delete-index": "dotenv node scripts/deleteIndex.js", + "migrate": "dotenv npx sequelize db:migrate", + "migrate:undo": "dotenv npx sequelize db:migrate:undo", + "test-data": "dotenv node scripts/insert-es-data.js", + "test": "dotenv mocha test/unit/*.test.js --timeout 30000 --exit", + "cov": "dotenv nyc --reporter=html --reporter=text mocha test/unit/*.test.js --timeout 30000 --exit" }, "keywords": [], "author": "", @@ -27,7 +31,6 @@ "config": "^3.3.2", "cors": "^2.8.5", "date-fns": "^2.16.1", - "dotenv": "^8.2.0", "express": "^4.17.1", "express-interceptor": "^1.2.0", "get-parameter-names": "^0.3.0", @@ -47,6 +50,7 @@ }, "devDependencies": { "chai": "^4.2.0", + "dotenv-cli": "^4.0.0", "mocha": "^8.1.3", "nodemon": "^2.0.4", "nyc": "^15.1.0", diff --git a/src/models/Job.js b/src/models/Job.js index 14cec753..d6cc3955 100644 --- a/src/models/Job.js +++ b/src/models/Job.js @@ -69,7 +69,7 @@ module.exports = (sequelize) => { type: Sequelize.STRING(255) }, description: { - type: Sequelize.TEXT, // technically unlimited length + type: Sequelize.TEXT // technically unlimited length }, title: { type: Sequelize.STRING(128), From cc6c5482d470ba711644f7fe82c0622ec9302bce Mon Sep 17 00:00:00 2001 From: imcaizheng Date: Mon, 1 Feb 2021 17:04:55 +0800 Subject: [PATCH 03/46] Create a script to import Jobs data from Recruit CRM --- docs/swagger.yaml | 26 +++ package-lock.json | 18 ++ package.json | 2 + scripts/recruit-crm-job-import/README.md | 76 ++++++++ scripts/recruit-crm-job-import/config.js | 21 +++ scripts/recruit-crm-job-import/constants.js | 22 +++ .../recruit-crm-job-import/example_data.csv | 8 + scripts/recruit-crm-job-import/helper.js | 142 ++++++++++++++ scripts/recruit-crm-job-import/index.js | 177 ++++++++++++++++++ scripts/recruit-crm-job-import/logger.js | 10 + scripts/recruit-crm-job-import/report.js | 49 +++++ src/services/ResourceBookingService.js | 6 +- 12 files changed, 555 insertions(+), 2 deletions(-) create mode 100644 scripts/recruit-crm-job-import/README.md create mode 100644 scripts/recruit-crm-job-import/config.js create mode 100644 scripts/recruit-crm-job-import/constants.js create mode 100644 scripts/recruit-crm-job-import/example_data.csv create mode 100644 scripts/recruit-crm-job-import/helper.js create mode 100644 scripts/recruit-crm-job-import/index.js create mode 100644 scripts/recruit-crm-job-import/logger.js create mode 100644 scripts/recruit-crm-job-import/report.js diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 3cd72f7b..1a257ab2 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1002,6 +1002,32 @@ paths: type: string enum: ['hourly', 'daily', 'weekly', 'monthly'] description: The rate type. + - in: query + name: jobId + required: false + schema: + type: string + format: uuid + description: The job id. + - in: query + name: userId + required: false + schema: + type: string + format: uuid + description: The job id. + - in: query + name: projectId + required: false + schema: + type: integer + description: The project id. + - in: query + name: projectIds + required: false + schema: + type: string + description: comma separated project ids. responses: '200': diff --git a/package-lock.json b/package-lock.json index 5b90459b..26e22f89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -414,6 +414,15 @@ "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", "dev": true }, + "@joi/date": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@joi/date/-/date-2.0.1.tgz", + "integrity": "sha512-vAnBxaPmyXRmHlbr5H3zY6x8ToW1a3c3gCo91dsf/HPKP2vS4sz2xzjyCE1up0vmFmSWgfDIyJMpRWVOG2cpZQ==", + "dev": true, + "requires": { + "moment": "2.x.x" + } + }, "@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", @@ -1630,6 +1639,15 @@ "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", "dev": true }, + "csv-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.0.0.tgz", + "integrity": "sha512-s6OYSXAK3IdKqYO33y09jhypG/bSDHPuyCme/IdEHfWpLf/jKcpitVFyOC6UemgGk8v7Q5u2XE0vvwmanxhGlQ==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, "d": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", diff --git a/package.json b/package.json index 15788a91..61621f77 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,9 @@ "winston": "^3.3.3" }, "devDependencies": { + "@joi/date": "^2.0.1", "chai": "^4.2.0", + "csv-parser": "^3.0.0", "mocha": "^8.1.3", "nodemon": "^2.0.4", "nyc": "^15.1.0", diff --git a/scripts/recruit-crm-job-import/README.md b/scripts/recruit-crm-job-import/README.md new file mode 100644 index 00000000..fdd8e01a --- /dev/null +++ b/scripts/recruit-crm-job-import/README.md @@ -0,0 +1,76 @@ +Recruit CRM Data Import +=== + +# Configuration +Configuration file is at `./scripts/recruit-crm-job-import/config.js`. + + +# Usage +``` bash +node scripts/recruit-crm-job-import +``` + +By default the script creates jobs and resource bookings via `TC_API`. +# Example + +Follow the README for Taas API to deploy Taas API locally and then point the script to the local API by running: + +``` bash +export RCRM_IMPORT_TAAS_API_URL=http://localhost:3000/api/v5 +node scripts/recruit-crm-job-import scripts/recruit-crm-job-import/example_data.csv | tee /tmp/report.txt +``` + +The example output is: + +``` bash +DEBUG: processing line #1 - {"directProjectId":"24568","projectId":"(dynamic load)","externalId":"","title":"taas-demo-job5","startDate":"10/26/2020","endDate":"01/29/2021","numPositions":"2","userHandle":"nkumartest","jobid":"(dynamic load)","customerRate":"20","memberRate":"10","_lnum":1} +WARN: #1 - externalId is missing +DEBUG: processed line #1 +DEBUG: processing line #2 - {"directProjectId":"24568","projectId":"(dynamic load)","externalId":"0","title":"taas-demo-job5","startDate":"10/26/2020","endDate":"01/29/2021","numPositions":"2","userHandle":"not_found_handle","jobid":"(dynamic load)","customerRate":"20","memberRate":"10","_lnum":2} +ERROR: #2 - handle: not_found_handle user not found +DEBUG: processed line #2 +DEBUG: processing line #3 - {"directProjectId":"24568","projectId":"(dynamic load)","externalId":"0","title":"taas-demo-job5","startDate":"10/26/2020","endDate":"01/29/2021","numPositions":"2","userHandle":"nkumartest","jobid":"(dynamic load)","customerRate":"20","memberRate":"10","_lnum":3} +DEBUG: userHandle: nkumartest userId: 57646ff9-1cd3-4d3c-88ba-eb09a395366c +DEBUG: resourceBookingId: dc8b23d4-9987-4a7d-a587-2056283223de status: assigned +INFO: #3 - id: 7c8ed989-35bf-4899-9c93-708630a7c63b job already exists; id: dc8b23d4-9987-4a7d-a587-2056283223de resource booking created; id: dc8b23d4-9987-4a7d-a587-2056283223de status: assigned resource booking updated +DEBUG: processed line #3 +DEBUG: processing line #4 - {"directProjectId":"24567","projectId":"(dynamic load)","externalId":"1212","title":"Dummy Description","startDate":"10/20/2020","endDate":"01/29/2021","numPositions":"2","userHandle":"pshah_manager","jobid":"(dynamic load)","customerRate":"150","memberRate":"100","_lnum":4} +DEBUG: userHandle: pshah_manager userId: a55fe1bc-1754-45fa-9adc-cf3d6d7c377a +DEBUG: resourceBookingId: 708469fb-ead0-4fc3-bef7-1ef4dd041428 status: assigned +INFO: #4 - id: f61da880-5295-40c2-b6db-21e6cdef93f9 job created; id: 708469fb-ead0-4fc3-bef7-1ef4dd041428 resource booking created; id: 708469fb-ead0-4fc3-bef7-1ef4dd041428 status: assigned resource booking updated +DEBUG: processed line #4 +DEBUG: processing line #5 - {"directProjectId":"24566","projectId":"(dynamic load)","externalId":"23850272","title":"33fromzaps330","startDate":"02/21/2021","endDate":"03/15/2021","numPositions":"7","userHandle":"nkumar2","jobid":"(dynamic load)","customerRate":"50","memberRate":"30","_lnum":5} +DEBUG: userHandle: nkumar2 userId: 4b00d029-c87b-47b2-bfe2-0ab80d8b5774 +DEBUG: resourceBookingId: 7870c30b-e511-48f2-8687-499ab116174f status: assigned +INFO: #5 - id: 72dc0399-5e4b-4783-9a27-ea07a4ce99a7 job created; id: 7870c30b-e511-48f2-8687-499ab116174f resource booking created; id: 7870c30b-e511-48f2-8687-499ab116174f status: assigned resource booking updated +DEBUG: processed line #5 +DEBUG: processing line #6 - {"directProjectId":"24565","projectId":"(dynamic load)","externalId":"23843365","title":"Designer","startDate":"02/24/2021","endDate":"03/30/2021","numPositions":"1","userHandle":"GunaK-TopCoder","jobid":"(dynamic load)","customerRate":"70","memberRate":"70","_lnum":6} +DEBUG: userHandle: GunaK-TopCoder userId: 2bba34d5-20e4-46d6-bfc1-05736b17afbb +DEBUG: resourceBookingId: b2e705d3-6864-4697-96bb-dc2a288755bc status: assigned +INFO: #6 - id: 7ff0737e-958c-494e-8a5a-592ac1c5d4ff job created; id: b2e705d3-6864-4697-96bb-dc2a288755bc resource booking created; id: b2e705d3-6864-4697-96bb-dc2a288755bc status: assigned resource booking updated +DEBUG: processed line #6 +DEBUG: processing line #7 - {"directProjectId":"24564","projectId":"(dynamic load)","externalId":"23836459","title":"demo-dev-19janV4","startDate":"01/20/2021","endDate":"01/30/2021","numPositions":"1","userHandle":"nkumar1","jobid":"(dynamic load)","customerRate":"400","memberRate":"200","_lnum":7} +DEBUG: userHandle: nkumar1 userId: ab19a53b-0607-4a99-8bdd-f3b0cb552293 +DEBUG: resourceBookingId: 04299b4c-3f6e-4b3e-ae57-bf8232408cf9 status: assigned +INFO: #7 - id: 73301ade-40ff-4103-bd50-37b8d2a98183 job created; id: 04299b4c-3f6e-4b3e-ae57-bf8232408cf9 resource booking created; id: 04299b4c-3f6e-4b3e-ae57-bf8232408cf9 status: assigned resource booking updated +DEBUG: processed line #7 +INFO: === summary === +INFO: total: 7 +INFO: success: 5 +INFO: failure: 1 +INFO: skips: 1 +INFO: === summary === +INFO: done! +``` + +To list all skipped lines: + +``` bash +cat /tmp/report.txt | grep 'WARN' +``` + +To find out whether there are some users not found by user handles, run the following command: + +``` bash +cat /tmp/report.txt | grep 'ERROR' | grep 'user not found' +``` diff --git a/scripts/recruit-crm-job-import/config.js b/scripts/recruit-crm-job-import/config.js new file mode 100644 index 00000000..58a96031 --- /dev/null +++ b/scripts/recruit-crm-job-import/config.js @@ -0,0 +1,21 @@ +/* + * Configuration for the RCRM import script. + * Namespace is created to allow to configure the env variables for this script independently. + */ + +const config = require('config') + +const namespace = process.env.RCRM_IMPORT_CONFIG_NAMESAPCE || 'RCRM_IMPORT_' + +module.exports = { + SLEEP_TIME: process.env[`${namespace}SLEEP_TIME`] || 500, + TAAS_API_URL: process.env[`${namespace}TAAS_API_URL`] || config.TC_API, + + TC_API: process.env[`${namespace}TC_API`] || config.TC_API, + AUTH0_URL: process.env[`${namespace}AUTH0_URL`] || config.AUTH0_URL, + AUTH0_AUDIENCE: process.env[`${namespace}AUTH0_AUDIENCE`] || config.AUTH0_AUDIENCE, + TOKEN_CACHE_TIME: process.env[`${namespace}TOKEN_CACHE_TIME`] || config.TOKEN_CACHE_TIME, + AUTH0_CLIENT_ID: process.env[`${namespace}AUTH0_CLIENT_ID`] || config.AUTH0_CLIENT_ID, + AUTH0_CLIENT_SECRET: process.env[`${namespace}AUTH0_CLIENT_SECRET`] || config.AUTH0_CLIENT_SECRET, + AUTH0_PROXY_SERVER_URL: process.env[`${namespace}AUTH0_PROXY_SERVER_URL`] || config.AUTH0_PROXY_SERVER_URL +} diff --git a/scripts/recruit-crm-job-import/constants.js b/scripts/recruit-crm-job-import/constants.js new file mode 100644 index 00000000..7dbe4743 --- /dev/null +++ b/scripts/recruit-crm-job-import/constants.js @@ -0,0 +1,22 @@ +/* + * Constants for the RCRM import script. + */ + +module.exports = { + ProcessingStatus: { + Successful: 'successful', + Failed: 'failed', + Skipped: 'skipped' + }, + fieldNameMap: { + DirectprojectId: 'directProjectId', + externalId: 'externalId', + title: 'title', + startDate: 'startDate', + endDate: 'endDate', + numPositions: 'numPositions', + userHandle: 'userHandle', + customerRate: 'customerRate', + memberRate: 'memberRate' + } +} diff --git a/scripts/recruit-crm-job-import/example_data.csv b/scripts/recruit-crm-job-import/example_data.csv new file mode 100644 index 00000000..1a52211d --- /dev/null +++ b/scripts/recruit-crm-job-import/example_data.csv @@ -0,0 +1,8 @@ +DirectprojectId,projectId,externalId,title,startDate,endDate,numPositions,userHandle,jobid,customerRate,memberRate +24568,(dynamic load),,taas-demo-job5,10/26/2020,01/29/2021,2,nkumartest,(dynamic load),20,10 +24568,(dynamic load),0,taas-demo-job5,10/26/2020,01/29/2021,2,not_found_handle,(dynamic load),20,10 +24568,(dynamic load),0,taas-demo-job5,10/26/2020,01/29/2021,2,nkumartest,(dynamic load),20,10 +24567,(dynamic load),1212,Dummy Description,10/20/2020,01/29/2021,2,pshah_manager,(dynamic load),150,100 +24566,(dynamic load),23850272,33fromzaps330,02/21/2021,03/15/2021,7,nkumar2,(dynamic load),50,30 +24565,(dynamic load),23843365,Designer,02/24/2021,03/30/2021,1,GunaK-TopCoder,(dynamic load),70,70 +24564,(dynamic load),23836459,demo-dev-19janV4,01/20/2021,01/30/2021,1,nkumar1,(dynamic load),400,200 diff --git a/scripts/recruit-crm-job-import/helper.js b/scripts/recruit-crm-job-import/helper.js new file mode 100644 index 00000000..5cdf10ff --- /dev/null +++ b/scripts/recruit-crm-job-import/helper.js @@ -0,0 +1,142 @@ +/* + * Provide some commonly used functions for the RCRM import script. + */ +const config = require('./config') +const request = require('superagent') +const { getM2MToken } = require('../../src/common/helper') + +/** + * Sleep for a given number of milliseconds. + * + * @param {Number} milliseconds the sleep time + * @returns {undefined} + */ +async function sleep (milliseconds) { + return new Promise((resolve) => setTimeout(resolve, milliseconds)) +} + +/** + * Create a new job via taas api. + * + * @param {Object} data the job data + * @returns {Object} the result + */ +async function createJob (data) { + const token = await getM2MToken() + const { body: job } = await request.post(`${config.TAAS_API_URL}/jobs`) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + .send(data) + return job +} + +/** + * Find taas job by external id. + * + * @param {String} externalId the external id + * @returns {Object} the result + */ +async function getJobByExternalId (externalId) { + const token = await getM2MToken() + const { body: jobs } = await request.get(`${config.TAAS_API_URL}/jobs`) + .query({ externalId }) + .set('Authorization', `Bearer ${token}`) + if (!jobs.length) { + throw new Error(`externalId: ${externalId} job not found`) + } + return jobs[0] +} + +/** + * Update the status of a resource booking. + * + * @param {String} resourceBookingId the resource booking id + * @param {String} status the status for the resource booking + * @returns {Object} the result + */ +async function updateResourceBookingStatus (resourceBookingId, status) { + const token = await getM2MToken() + const { body: resourceBooking } = await request.patch(`${config.TAAS_API_URL}/resourceBookings/${resourceBookingId}`) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + .send({ status }) + return resourceBooking +} + +/** + * Find taas resource booking by job id and user id. + * + * @param {String} jobId the job id + * @param {String} userId the user id + * @returns {Object} the result + */ +async function getResourceBookingByJobIdAndUserId (jobId, userId) { + const token = await getM2MToken() + const { body: resourceBookings } = await request.get(`${config.TAAS_API_URL}/resourceBookings`) + .query({ jobId, userId }) + .set('Authorization', `Bearer ${token}`) + if (!resourceBookings.length) { + throw new Error(`jobId: ${jobId} userId: ${userId} resource booking not found`) + } + return resourceBookings[0] +} + +/** + * Create a new resource booking via taas api. + * + * @param {Object} data the resource booking data + * @returns {Object} the result + */ +async function createResourceBooking (data) { + const token = await getM2MToken() + const { body: resourceBooking } = await request.post(`${config.TAAS_API_URL}/resourceBookings`) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + .send(data) + return resourceBooking +} + +/** + * Find user via /v5/users by user handle. + * + * @param {String} handle the user handle + * @returns {Object} the result + */ +async function getUserByHandle (handle) { + const token = await getM2MToken() + const { body: users } = await request.get(`${config.TC_API}/users`) + .query({ handle }) + .set('Authorization', `Bearer ${token}`) + if (!users.length) { + throw new Error(`handle: ${handle} user not found`) + } + return users[0] +} + +/** + * Find project via /v5/projects by Direct project id. + * + * @param {Number} directProjectId the Direct project id + * @returns {Object} the result + */ +async function getProjectByDirectProjectId (directProjectId) { + const token = await getM2MToken() + const { body: projects } = await request.get(`${config.TC_API}/projects`) + .query({ directProjectId }) + .set('Authorization', `Bearer ${token}`) + if (!projects.length) { + throw new Error(`directProjectId: ${directProjectId} project not found`) + } + return projects[0] +} + +module.exports = { + sleep, + createJob, + getJobByExternalId, + updateResourceBookingStatus, + getResourceBookingByJobIdAndUserId, + createResourceBooking, + getUserByHandle, + getProjectByDirectProjectId +} diff --git a/scripts/recruit-crm-job-import/index.js b/scripts/recruit-crm-job-import/index.js new file mode 100644 index 00000000..e807e7f9 --- /dev/null +++ b/scripts/recruit-crm-job-import/index.js @@ -0,0 +1,177 @@ +/* + * Script to import Jobs data from Recruit CRM to Taas API. + */ + +const csv = require('csv-parser') +const fs = require('fs') +const Joi = require('joi') + .extend(require('@joi/date')) +const _ = require('lodash') +const dateFNS = require('date-fns') +const Report = require('./report') +const config = require('./config') +const helper = require('./helper') +const constants = require('./constants') +const logger = require('./logger') + +const jobSchema = Joi.object({ + directProjectId: Joi.number().integer().required(), + externalId: Joi.string().allow(''), + title: Joi.string().required(), + startDate: Joi.date().format('MM/DD/YYYY').required(), + endDate: Joi.date().format('MM/DD/YYYY').required(), + numPositions: Joi.number().integer().min(1), + userHandle: Joi.string(), + customerRate: Joi.number(), + memberRate: Joi.number(), + skills: Joi.array().default([]), + rateType: Joi.string().default('weekly') +}).unknown(true) + +/** + * Validate job data. + * + * @param {Object} job the job data + * @returns {Object} the validation result + */ +function validateJob (job) { + return jobSchema.validate(job) +} + +/** + * Load Recruit CRM jobs data from file. + * + * @param {String} pathname the pathname for the file + * @returns {Array} the result jobs data + */ +async function loadRcrmJobsFromFile (pathname) { + let lnum = 1 + const result = [] + return new Promise((resolve, reject) => { + fs.createReadStream(pathname) + .pipe(csv({ + mapHeaders: ({ header }) => constants.fieldNameMap[header] || header + })) + .on('data', (data) => { + result.push({ ...data, _lnum: lnum }) + lnum += 1 + }) + .on('error', err => reject(err)) + .on('end', () => resolve(result)) + }) +} + +/** + * Get pathname for a csv file from command line arguments. + * + * @returns {undefined} + */ +function getPathname () { + if (process.argv.length < 3) { + throw new Error('pathname for the csv file is required') + } + const pathname = process.argv[2] + if (!fs.existsSync(pathname)) { + throw new Error(`pathname: ${pathname} path not exist`) + } + if (!fs.lstatSync(pathname).isFile()) { + throw new Error(`pathname: ${pathname} path is not a regular file`) + } + return pathname +} + +/** + * Process single job data. The processing consists of: + * - Validate the data. + * - Skip processing if externalId is missing. + * - Create a job if it is not already exists. + * - Create a resource booking if it is not already exists. + * - Update the resourceBooking based on startDate and endDate. + * + * @param {Object} job the job data + * @param {Array} info contains processing details + * @returns {Object} + */ +async function processJob (job, info = []) { + // validate the data + const { value: data, error } = validateJob(job) + if (error) { + info.push(error.details[0].message) + return { status: constants.ProcessingStatus.Failed, info } + } + if (!data.externalId) { + info.push('externalId is missing') + return { status: constants.ProcessingStatus.Skipped, info } + } + data.projectId = (await helper.getProjectByDirectProjectId(data.directProjectId)).id + // create a job if it is not already exists + try { + const result = await helper.getJobByExternalId(data.externalId) + info.push(`id: ${result.id} externalId: ${data.externalId} job already exists`) + data.jobId = result.id + } catch (err) { + if (!(err.message && err.message.includes('job not found'))) { + throw err + } + const result = await helper.createJob(_.pick(data, ['projectId', 'externalId', 'title', 'numPositions', 'skills'])) + info.push(`id: ${result.id} job created`) + data.jobId = result.id + } + data.userId = (await helper.getUserByHandle(data.userHandle)).id + logger.debug(`userHandle: ${data.userHandle} userId: ${data.userId}`) + // create a resource booking if it is not already exists + try { + const result = await helper.getResourceBookingByJobIdAndUserId(data.jobId, data.userId) + info.push(`id: ${result.id} resource booking already exists`) + return { status: constants.ProcessingStatus.Successful, info } + } catch (err) { + if (!(err.message && err.message.includes('resource booking not found'))) { + throw err + } + const result = await helper.createResourceBooking(_.pick(data, ['projectId', 'jobId', 'userId', 'startDate', 'endDate', 'memberRate', 'customerRate', 'rateType'])) + info.push(`id: ${result.id} resource booking created`) + data.resourceBookingId = result.id + } + // update the resourceBooking based on startDate and endDate + const resourceBookingStatus = dateFNS.compareAsc(new Date(data.startDate), new Date(data.endDate)) === 1 ? 'closed' : 'assigned' + logger.debug(`resourceBookingId: ${data.resourceBookingId} status: ${resourceBookingStatus}`) + await helper.updateResourceBookingStatus(data.resourceBookingId, resourceBookingStatus) + info.push(`id: ${data.resourceBookingId} status: ${resourceBookingStatus} resource booking updated`) + return { status: constants.ProcessingStatus.Successful, info } +} + +/** + * The entry of the script. + * + * @returns {undefined} + */ +async function main () { + const pathname = getPathname() + const jobs = await loadRcrmJobsFromFile(pathname) + const report = new Report() + for (const job of jobs) { + logger.debug(`processing line #${job._lnum} - ${JSON.stringify(job)}`) + try { + const result = await processJob(job) + report.add({ lnum: job._lnum, ...result }) + } catch (err) { + if (err.response) { + report.add({ lnum: job._lnum, status: constants.ProcessingStatus.Failed, info: [err.response.error.toString().split('\n')[0]] }) + } else { + report.add({ lnum: job._lnum, status: constants.ProcessingStatus.Failed, info: [err.message] }) + } + } + report.print() + logger.debug(`processed line #${job._lnum}`) + await helper.sleep(config.SLEEP_TIME) + } + report.printSummary() +} + +main().then(() => { + logger.info('done!') + process.exit() +}).catch(err => { + logger.error(err.message) + process.exit(1) +}) diff --git a/scripts/recruit-crm-job-import/logger.js b/scripts/recruit-crm-job-import/logger.js new file mode 100644 index 00000000..ccb00102 --- /dev/null +++ b/scripts/recruit-crm-job-import/logger.js @@ -0,0 +1,10 @@ +/* + * Logger for the RCRM import script. + */ + +module.exports = { + info: (message) => console.log(`INFO: ${message}`), + debug: (message) => console.log(`DEBUG: ${message}`), + warn: (message) => console.log(`WARN: ${message}`), + error: (message) => console.log(`ERROR: ${message}`) +} diff --git a/scripts/recruit-crm-job-import/report.js b/scripts/recruit-crm-job-import/report.js new file mode 100644 index 00000000..574dd8f3 --- /dev/null +++ b/scripts/recruit-crm-job-import/report.js @@ -0,0 +1,49 @@ +/* + * The Report class. + */ + +const logger = require('./logger') +const constants = require('./constants') +const _ = require('lodash') + +class Report { + constructor () { + this.messages = [] + } + + // append a message to the report + add (message) { + this.messages.push(message) + } + + // print the last message to the console + print () { + const lastMessage = this.messages[this.messages.length - 1] + const output = `#${lastMessage.lnum} - ${lastMessage.info.join('; ')}` + if (lastMessage.status === constants.ProcessingStatus.Skipped) { + logger.warn(output) + } + if (lastMessage.status === constants.ProcessingStatus.Successful) { + logger.info(output) + } + if (lastMessage.status === constants.ProcessingStatus.Failed) { + logger.error(output) + } + } + + // print a summary to the console + printSummary () { + const groups = _.groupBy(this.messages, 'status') + const sucesss = groups[constants.ProcessingStatus.Successful] || [] + const failure = groups[constants.ProcessingStatus.Failed] || [] + const skips = groups[constants.ProcessingStatus.Skipped] || [] + logger.info('=== summary ===') + logger.info(`total: ${this.messages.length}`) + logger.info(`success: ${sucesss.length}`) + logger.info(`failure: ${failure.length}`) + logger.info(`skips: ${skips.length}`) + logger.info('=== summary ===') + } +} + +module.exports = Report diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index 9f2a63cf..59c446d3 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -290,7 +290,7 @@ async function searchResourceBookings (currentUser, criteria, options = { return } } - _.each(_.pick(criteria, ['status', 'startDate', 'endDate', 'rateType', 'projectId']), (value, key) => { + _.each(_.pick(criteria, ['status', 'startDate', 'endDate', 'rateType', 'projectId', 'jobId', 'userId']), (value, key) => { esQuery.body.query.bool.must.push({ term: { [key]: { @@ -328,7 +328,7 @@ async function searchResourceBookings (currentUser, criteria, options = { return const filter = { [Op.and]: [{ deletedAt: null }] } - _.each(_.pick(criteria, ['status', 'startDate', 'endDate', 'rateType']), (value, key) => { + _.each(_.pick(criteria, ['status', 'startDate', 'endDate', 'rateType', 'projectId', 'jobId', 'userId']), (value, key) => { filter[Op.and].push({ [key]: value }) }) if (criteria.projectIds) { @@ -363,6 +363,8 @@ searchResourceBookings.schema = Joi.object().keys({ startDate: Joi.date(), endDate: Joi.date(), rateType: Joi.rateType(), + jobId: Joi.string().uuid(), + userId: Joi.string().uuid(), projectId: Joi.number().integer(), projectIds: Joi.alternatives( Joi.string(), From b067dc7f54671b1b01fd560ca81a4770c65b5958 Mon Sep 17 00:00:00 2001 From: maxceem Date: Tue, 2 Feb 2021 14:54:53 +0200 Subject: [PATCH 04/46] Revert "Winner submission for Topcoder TaaS API - Improve Local Setup" --- README.md | 252 ++++++------------ config/default.js | 43 +-- local/docker-compose.yaml | 99 ------- local/generic-tc-service/Dockerfile | 15 -- local/generic-tc-service/docker-entrypoint.sh | 13 - local/kafka-client/Dockerfile | 5 - local/kafka-client/create-topics.sh | 9 - local/kafka-client/topics.txt | 9 - .../2021-01-13-make-some-job-fields-longer.js | 8 +- package-lock.json | 70 +---- package.json | 24 +- src/models/Job.js | 2 +- 12 files changed, 95 insertions(+), 454 deletions(-) delete mode 100644 local/docker-compose.yaml delete mode 100644 local/generic-tc-service/Dockerfile delete mode 100755 local/generic-tc-service/docker-entrypoint.sh delete mode 100644 local/kafka-client/Dockerfile delete mode 100755 local/kafka-client/create-topics.sh delete mode 100644 local/kafka-client/topics.txt diff --git a/README.md b/README.md index 0e2aeaaf..4c142f0a 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,68 @@ - nodejs https://nodejs.org/en/ (v12+) - PostgreSQL - ElasticSearch (7.x) -- Zookeeper -- Kafka -- Docker(version 20.10 and above) +- Docker - Docker-Compose +## Configuration + +Configuration for the application is at `config/default.js`. + +The following parameters can be set in config files or in env variables: + +- `LOG_LEVEL`: the log level, default is 'debug' +- `PORT`: the server port, default is 3000 +- `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_UBAHN`: Auth0 audience for U-Bahn +- `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 + +- `m2m.M2M_AUDIT_USER_ID`: default value is `00000000-0000-0000-0000-000000000000` +- `m2m.M2M_AUDIT_HANDLE`: default value is `TopcoderService` + +- `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 +- `TOPCODER_SKILL_PROVIDER_ID`: the referenced skill provider id + +- `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 force` to force creating table) + ## DB Migration - `npm run migrate`: run any migration files which haven't run yet. - `npm run migrate:undo`: revert most recent migration. @@ -23,186 +80,27 @@ The following parameters can be set in the config file or via env variables: - `database`: set via env `DB_NAME`; datebase name - `host`: set via env `DB_HOST`; datebase host name -### Steps to run locally -1. 📦 Install npm dependencies - - ```bash - npm install - ``` - -2. ⚙ Local config - - 1. In the root directory create `.env` file with the next environment variables. Values for **Auth0 config** should be shared with you on the forum.
- ```bash - # Auth0 config - AUTH0_URL= - AUTH0_AUDIENCE= - AUTH0_AUDIENCE_UBAHN= - AUTH0_CLIENT_ID= - AUTH0_CLIENT_SECRET= - AUTH0_PROXY_SERVER_URL= - - # Locally deployed services (via docker-compose) - ES_HOST=http://dockerhost:9200 - DATABASE_URL=postgres://postgres:postgres@dockerhost:5432/postgres - BUSAPI_URL=http://dockerhost:8002/v5 - ``` - - - Values from this file would be automatically used by many `npm` commands. - - ⚠️ Never commit this file or its copy to the repository! - - 1. Set `dockerhost` to point the IP address of Docker. Docker IP address depends on your system. For example if docker is run on IP `127.0.0.1` add a the next line to your `/etc/hosts` file: - ``` - 127.0.0.1 dockerhost - ``` - - Alternatively, you may update `.env` file and replace `dockerhost` with your docker IP address. - -1. 🚢 Start docker-compose with services which are required to start Taas API locally - - *(NOTE Please ensure that you have installed docker of version 20.10 or above since the docker-compose file uses new feature introduced by docker version 20.10. Run `docker --version` to check your docker version.)* +## ElasticSearch Setup +- Go to https://www.elastic.co/downloads/ download and install the elasticsearch. +- Modify `esConfig` under `config/default.js` to meet your environment. +- Run `npm run create-index` to create ES index. +- Run `npm run delete-index` to delete ES index. - ```bash - npm run services:up - ``` +## Local Deployment - Wait until all containers are fully started. As a good indicator, wait until `es-processor` successfully started by viewing its logs: - - ```bash - npm run services:logs -- -f es-processor - ``` - -
🖱️ Click to see a good logs example +- Install dependencies `npm install` +- Run lint `npm run lint` +- Run lint fix `npm run lint:fix` +- Clear and init db `npm run init-db force` +- Clear and create es index ``` bash - tc-taas-es-processor | Waiting for kafka-client to exit.... - tc-taas-es-processor | kafka-client exited! - tc-taas-es-processor | - tc-taas-es-processor | > taas-es-processor@1.0.0 start /opt/app - tc-taas-es-processor | > node src/app.js - tc-taas-es-processor | - tc-taas-es-processor | [2021-01-21T02:44:43.442Z] app INFO : Starting kafka consumer - tc-taas-es-processor | 2021-01-21T02:44:44.534Z INFO no-kafka-client Joined group taas-es-processor generationId 1 as no-kafka-client-70c25a43-af93-495e-a123-0c4f4ea389eb - tc-taas-es-processor | 2021-01-21T02:44:44.534Z INFO no-kafka-client Elected as group leader - tc-taas-es-processor | 2021-01-21T02:44:44.614Z DEBUG no-kafka-client Subscribed to taas.jobcandidate.create:0 offset 0 leader kafka:9093 - tc-taas-es-processor | 2021-01-21T02:44:44.615Z DEBUG no-kafka-client Subscribed to taas.job.create:0 offset 0 leader kafka:9093 - tc-taas-es-processor | 2021-01-21T02:44:44.615Z DEBUG no-kafka-client Subscribed to taas.resourcebooking.delete:0 offset 0 leader kafka:9093 - tc-taas-es-processor | 2021-01-21T02:44:44.616Z DEBUG no-kafka-client Subscribed to taas.jobcandidate.delete:0 offset 0 leader kafka:9093 - tc-taas-es-processor | 2021-01-21T02:44:44.616Z DEBUG no-kafka-client Subscribed to taas.jobcandidate.update:0 offset 0 leader kafka:9093 - tc-taas-es-processor | 2021-01-21T02:44:44.617Z DEBUG no-kafka-client Subscribed to taas.resourcebooking.create:0 offset 0 leader kafka:9093 - tc-taas-es-processor | 2021-01-21T02:44:44.617Z DEBUG no-kafka-client Subscribed to taas.job.delete:0 offset 0 leader kafka:9093 - tc-taas-es-processor | 2021-01-21T02:44:44.618Z DEBUG no-kafka-client Subscribed to taas.job.update:0 offset 0 leader kafka:9093 - tc-taas-es-processor | 2021-01-21T02:44:44.618Z DEBUG no-kafka-client Subscribed to taas.resourcebooking.update:0 offset 0 leader kafka:9093 - tc-taas-es-processor | [2021-01-21T02:44:44.619Z] app INFO : Initialized....... - tc-taas-es-processor | [2021-01-21T02:44:44.623Z] app INFO : taas.job.create,taas.job.update,taas.job.delete,taas.jobcandidate.create,taas.jobcandidate.update,taas.jobcandidate.delete,taas.resourcebooking.create,taas.resourcebooking.update,taas.resourcebooking.delete - tc-taas-es-processor | [2021-01-21T02:44:44.623Z] app INFO : Kick Start....... - tc-taas-es-processor | ********** Topcoder Health Check DropIn listening on port 3001 - tc-taas-es-processor | Topcoder Health Check DropIn started and ready to roll + npm run delete-index # run this if you already created index + npm run create-index ``` -
- - If you want to learn more about docker-compose configuration -
🖱️ Click to see more details here -
- - This docker-compose file starts the next services: - | Service | Name | Port | - |----------|:-----:|:----:| - | PostgreSQL | db | 5432 | - | Elasticsearch | esearch | 9200 | - | Zookeeper | zookeeper | 2181 | - | Kafka | kafka | 9092 | - | [tc-bus-api](https://github.com/topcoder-platform/tc-bus-api) | bus-api | 8002 | - | [taas-es-processor](https://github.com/topcoder-platform/taas-es-processor) | es-processor | 5000 | - - - as many of the Topcoder services in this docker-compose require Auth0 configuration for M2M calls, our docker-compose file passes environment variables `AUTH0_CLIENT_ID`, `AUTH0_CLIENT_SECRET`, `AUTH0_URL`, `AUTH0_AUDIENCE`, `AUTH0_PROXY_SERVER_URL` to its containers. docker-compose takes them from `.env` file if provided. - - - `docker-compose` automatically would create Kafka topics which are used by `taas-apis` listed in `./local/kafka-client/topics.txt`. - - - To view the logs from any container inside docker-compose use the following command, replacing `SERVICE_NAME` with the corresponding value under the **Name** column in the above table: - - ```bash - npm run services:logs -- -f SERVICE_NAME - ``` - - - If you want to modify the code of any of the services which are run inside this docker-compose file, you can stop such service inside docker-compose by command `docker-compose -f local/docker-compose.yaml stop ` and run the service separately, following its README file.

- *NOTE: If kafka(along with zookeeper) is stopped and brings up in the host machine you will need to restart the `es-processor` service by running `docker-compose -f local/docker-compose.yaml restart es-processor` so the processor will connect with the new zookeeper.* - - *NOTE: In production these dependencies / services are hosted & managed outside Taas API.* - -2. ♻ Init DB and ES - - ```bash - npm run local:init - ``` - - This command will do 2 things: - - create Database tables - - create Elasticsearch indexes - -3. 🚀 Start Taas API - - ```bash - npm run dev - ``` - - Runs the Taas API using nodemon, so it would be restarted after any of the files is updated. - The API will be served on `http://localhost:3000`. - -## NPM Commands - -| Command | Description | -| -- | -- | -| `npm start` | Start app. | -| `npm run dev` | Start app using `nodemon`. | -| `npm run lint` | Check for for lint errors. | -| `npm run lint:fix` | Check for for lint errors and fix error automatically when possible. | -| `npm run services:up` | Start services via docker-compose for local development. | -| `npm run services:down` | Stop services via docker-compose for local development. | -| `npm run services:logs -- -f ` | View logs of some service inside docker-compose. | -| `npm run local:init` | Create Database and Elasticsearch indexes. | -| `npm run init-db` | Create database. | -| `npm run init-db force` | Force re-creating database. | -| `npm run create-index` | Create Elasticsearch indexes. | -| `npm run delete-index` | Delete Elasticsearch indexes. | -| `npm run migrate` | Run DB migration. | -| `npm run migrate:undo` | Undo DB migration executed previously | -| `npm run test-data` | Insert test data. | -| `npm run test` | Run tests. | -| `npm run cov` | Run test with coverage. | - -## Kafka Commands - -You can use the following commands to manipulate kafka topics and messages: - -(Replace `TOPIC_NAME` with the name of the desired topic) - -### Create Topic - -```bash -docker exec tc-taas-kafka /opt/kafka/bin/kafka-topics.sh --create --zookeeper zookeeper:2181 --partitions 1 --replication-factor 1 --topic TOPIC_NAME -``` - -### List Topics - -```bash -docker exec tc-taas-kafka /opt/kafka/bin/kafka-topics.sh --list --zookeeper zookeeper:2181 -``` - -### Watch Topic - -```bash -docker exec tc-taas-kafka /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic TOPIC_NAME -``` - -### Post Message to Topic (from stdin) - -```bash -docker exec -it tc-taas-kafka /opt/kafka/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic TOPIC_NAME -``` - -- Enter or copy/paste the message into the console after starting this command. +- Start app `npm start` +- App is running at `http://localhost:3000` ## Local Deployment with Docker diff --git a/config/default.js b/config/default.js index 578c1484..d726ca58 100644 --- a/config/default.js +++ b/config/default.js @@ -1,104 +1,63 @@ +require('dotenv').config() module.exports = { - // the log level LOG_LEVEL: process.env.LOG_LEVEL || 'debug', - // the server port PORT: process.env.PORT || 3000, - // the server api base path BASE_PATH: process.env.BASE_PATH || '/api/v5', - // The authorization secret used during token verification. AUTH_SECRET: process.env.AUTH_SECRET || 'mysecret', - // The valid issuer of tokens, a json array contains valid issuer. 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, used to get TC M2M token AUTH0_URL: process.env.AUTH0_URL, - // Auth0 audience, used to get TC M2M token AUTH0_AUDIENCE: process.env.AUTH0_AUDIENCE, - // Auth0 audience for U-Bahn AUTH0_AUDIENCE_UBAHN: process.env.AUTH0_AUDIENCE_UBAHN, - // Auth0 token cache time, used to get TC M2M token TOKEN_CACHE_TIME: process.env.TOKEN_CACHE_TIME, - // Auth0 client id, used to get TC M2M token AUTH0_CLIENT_ID: process.env.AUTH0_CLIENT_ID, - // Auth0 client secret, used to get TC M2M token AUTH0_CLIENT_SECRET: process.env.AUTH0_CLIENT_SECRET, - // Proxy Auth0 URL, used to get TC M2M token AUTH0_PROXY_SERVER_URL: process.env.AUTH0_PROXY_SERVER_URL, m2m: { - // default user ID for m2m user M2M_AUDIT_USER_ID: process.env.M2M_AUDIT_USER_ID || '00000000-0000-0000-0000-000000000000', - // default handle name for m2m user M2M_AUDIT_HANDLE: process.env.M2M_AUDIT_HANDLE || 'TopcoderService' }, - // the Topcoder v5 url TC_API: process.env.TC_API || 'https://api.topcoder-dev.com/v5', - // the organization id ORG_ID: process.env.ORG_ID || '36ed815b-3da1-49f1-a043-aaed0a4e81ad', - // the referenced skill provider id TOPCODER_SKILL_PROVIDER_ID: process.env.TOPCODER_SKILL_PROVIDER_ID || '9cc0795a-6e12-4c84-9744-15858dba1861', - // the TC API for v3 users TOPCODER_USERS_API: process.env.TOPCODER_USERS_API || 'https://api.topcoder-dev.com/v3/users', - // PostgreSQL database url. DATABASE_URL: process.env.DATABASE_URL || 'postgres://postgres:postgres@localhost:5432/postgres', - // string - PostgreSQL database target schema DB_SCHEMA_NAME: process.env.DB_SCHEMA_NAME || 'bookings', - // the project service url PROJECT_API_URL: process.env.PROJECT_API_URL || 'https://api.topcoder-dev.com', esConfig: { - // the elasticsearch host HOST: process.env.ES_HOST || 'http://localhost:9200', ELASTICCLOUD: { - // 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 id: process.env.ELASTICCLOUD_ID, - // The elastic cloud username for basic authentication. Provide this only if your elasticsearch instance is hosted on elastic cloud username: process.env.ELASTICCLOUD_USERNAME, - // The elastic cloud password for basic authentication. Provide this only if your elasticsearch instance is hosted on elastic cloud password: process.env.ELASTICCLOUD_PASSWORD }, - // The Amazon region to use when using AWS Elasticsearch service AWS_REGION: process.env.AWS_REGION || 'us-east-1', // AWS Region to be used if we use AWS ES - // the job index ES_INDEX_JOB: process.env.ES_INDEX_JOB || 'job', - // // 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 - // the job candidate index ES_INDEX_JOB_CANDIDATE: process.env.ES_INDEX_JOB_CANDIDATE || 'job_candidate', - // the resource booking index ES_INDEX_RESOURCE_BOOKING: process.env.ES_INDEX_RESOURCE_BOOKING || 'resource_booking' }, - // Topcoder Bus API URL BUSAPI_URL: process.env.BUSAPI_URL || 'https://api.topcoder-dev.com/v5', - // The error topic at which bus api will publish any errors KAFKA_ERROR_TOPIC: process.env.KAFKA_ERROR_TOPIC || 'common.error.reporting', - // The originator value for the kafka messages KAFKA_MESSAGE_ORIGINATOR: process.env.KAFKA_MESSAGE_ORIGINATOR || 'taas-api', // topics for job service - // the create job entity Kafka message topic TAAS_JOB_CREATE_TOPIC: process.env.TAAS_JOB_CREATE_TOPIC || 'taas.job.create', - // the update job entity Kafka message topic TAAS_JOB_UPDATE_TOPIC: process.env.TAAS_JOB_UPDATE_TOPIC || 'taas.job.update', - // the delete job entity Kafka message topic TAAS_JOB_DELETE_TOPIC: process.env.TAAS_JOB_DELETE_TOPIC || 'taas.job.delete', // topics for jobcandidate service - // the create job candidate entity Kafka message topic TAAS_JOB_CANDIDATE_CREATE_TOPIC: process.env.TAAS_JOB_CANDIDATE_CREATE_TOPIC || 'taas.jobcandidate.create', - // the update job candidate entity Kafka message topic TAAS_JOB_CANDIDATE_UPDATE_TOPIC: process.env.TAAS_JOB_CANDIDATE_UPDATE_TOPIC || 'taas.jobcandidate.update', - // the delete job candidate entity Kafka message topic TAAS_JOB_CANDIDATE_DELETE_TOPIC: process.env.TAAS_JOB_CANDIDATE_DELETE_TOPIC || 'taas.jobcandidate.delete', // topics for job service - // the create resource booking entity Kafka message topic TAAS_RESOURCE_BOOKING_CREATE_TOPIC: process.env.TAAS_RESOURCE_BOOKING_CREATE_TOPIC || 'taas.resourcebooking.create', - // the update resource booking entity Kafka message topic TAAS_RESOURCE_BOOKING_UPDATE_TOPIC: process.env.TAAS_RESOURCE_BOOKING_UPDATE_TOPIC || 'taas.resourcebooking.update', - // the delete resource booking entity Kafka message topic TAAS_RESOURCE_BOOKING_DELETE_TOPIC: process.env.TAAS_RESOURCE_BOOKING_DELETE_TOPIC || 'taas.resourcebooking.delete' } diff --git a/local/docker-compose.yaml b/local/docker-compose.yaml deleted file mode 100644 index 1709207e..00000000 --- a/local/docker-compose.yaml +++ /dev/null @@ -1,99 +0,0 @@ -version: '2.4' -services: - zookeeper: - image: wurstmeister/zookeeper - container_name: tc-taas-zookeeper - ports: - - "2181:2181" - environment: - zk_id: "1" - kafka: - image: wurstmeister/kafka - container_name: tc-taas-kafka - ports: - - "9092:9092" - depends_on: - - zookeeper - environment: - KAFKA_ADVERTISED_LISTENERS: INSIDE://kafka:9093,OUTSIDE://localhost:9092 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT - KAFKA_LISTENERS: INSIDE://0.0.0.0:9093,OUTSIDE://0.0.0.0:9092 - KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 - KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE - esearch: - image: elasticsearch:7.7.1 - container_name: tc-taas-es - ports: - - "9200:9200" - environment: - - discovery.type=single-node - db: - image: postgres - container_name: tc-taas-postgres - ports: - - "5432:5432" - environment: - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=postgres - kafka-client: - build: ./kafka-client - container_name: tc-taas-kafka-client - extra_hosts: - - "host.docker.internal:host-gateway" - bus-api: - container_name: tc-taas-bus-api - build: - context: ./generic-tc-service - args: - NODE_VERSION: 8.11.3 - GIT_URL: https://github.com/topcoder-platform/tc-bus-api - GIT_BRANCH: dev - BYPASS_TOKEN_VALIDATION: 1 - command: start kafka-client - expose: - - "3000" - ports: - - "8002:3000" - depends_on: - - kafka-client - environment: - - PORT=3000 - - KAFKA_URL=http://host.docker.internal:9092 - - JWT_TOKEN_SECRET=secret - - VALID_ISSUERS="[\"https:\/\/topcoder-newauth.auth0.com\/\",\"https:\/\/api.topcoder-dev.com\",\"https:\/\/topcoder-dev.auth0.com\/\"]" - - AUTH0_CLIENT_ID - - AUTH0_CLIENT_SECRET - - AUTH0_URL - - AUTH0_AUDIENCE - - AUTH0_PROXY_SERVER_URL - extra_hosts: - - "host.docker.internal:host-gateway" - - "localhost:host-gateway" - es-processor: - container_name: tc-taas-es-processor - build: - context: ./generic-tc-service - args: - NODE_VERSION: 12 - GIT_URL: https://github.com/topcoder-platform/taas-es-processor - GIT_BRANCH: dev - BYPASS_TOKEN_VALIDATION: 0 - command: start kafka-client - depends_on: - - kafka-client - expose: - - "3001" - ports: - - "5000:3001" - environment: - - PORT=3001 - - KAFKA_URL=http://host.docker.internal:9092 - - ES_HOST=http://host.docker.internal:9200 - - AUTH0_CLIENT_ID - - AUTH0_CLIENT_SECRET - - AUTH0_URL - - AUTH0_AUDIENCE - - AUTH0_PROXY_SERVER_URL - extra_hosts: - - "host.docker.internal:host-gateway" - - "localhost:host-gateway" diff --git a/local/generic-tc-service/Dockerfile b/local/generic-tc-service/Dockerfile deleted file mode 100644 index e3113c7f..00000000 --- a/local/generic-tc-service/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -ARG NODE_VERSION=8.11.3 - -FROM node:$NODE_VERSION -ARG GIT_URL -ARG GIT_BRANCH -ARG BYPASS_TOKEN_VALIDATION - -RUN git clone $GIT_URL /opt/app -WORKDIR /opt/app -RUN git checkout -b node-branch origin/$GIT_BRANCH - -RUN npm install -RUN if [ $BYPASS_TOKEN_VALIDATION -eq 1 ]; then sed -i '/decodedToken = jwt.decode/a \ callback(undefined, decodedToken.payload); return;' node_modules/tc-core-library-js/lib/auth/verifier.js; fi -COPY docker-entrypoint.sh /opt/ -ENTRYPOINT ["/opt/docker-entrypoint.sh"] diff --git a/local/generic-tc-service/docker-entrypoint.sh b/local/generic-tc-service/docker-entrypoint.sh deleted file mode 100755 index 24cb1fe8..00000000 --- a/local/generic-tc-service/docker-entrypoint.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -if [ $# -eq 2 ]; then - echo "Waiting for $2 to exit...." - while ping -c1 $2 &>/dev/null - do - sleep 1 - done - echo "$2 exited!" -fi - -tail -n+3 /etc/hosts > /tmp/hosts && cp /tmp/hosts /etc/hosts # remove default localhost -cd /opt/app/ && npm run $1 diff --git a/local/kafka-client/Dockerfile b/local/kafka-client/Dockerfile deleted file mode 100644 index 15c20839..00000000 --- a/local/kafka-client/Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -From wurstmeister/kafka -WORKDIR /app/ -COPY topics.txt . -COPY create-topics.sh . -ENTRYPOINT ["/app/create-topics.sh"] diff --git a/local/kafka-client/create-topics.sh b/local/kafka-client/create-topics.sh deleted file mode 100755 index df00f7ea..00000000 --- a/local/kafka-client/create-topics.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -until /opt/kafka/bin/kafka-topics.sh --list --zookeeper host.docker.internal:2181 > exists-topics.txt - do sleep 1 -done - -while read topic; do - /opt/kafka/bin/kafka-topics.sh --create --if-not-exists --zookeeper host.docker.internal:2181 --partitions 1 --replication-factor 1 --topic $topic -done < <(sort topics.txt exists-topics.txt exists-topics.txt | uniq -u) diff --git a/local/kafka-client/topics.txt b/local/kafka-client/topics.txt deleted file mode 100644 index a392a8fc..00000000 --- a/local/kafka-client/topics.txt +++ /dev/null @@ -1,9 +0,0 @@ -taas.job.create -taas.jobcandidate.create -taas.resourcebooking.create -taas.job.update -taas.jobcandidate.update -taas.resourcebooking.update -taas.job.delete -taas.jobcandidate.delete -taas.resourcebooking.delete diff --git a/migrations/2021-01-13-make-some-job-fields-longer.js b/migrations/2021-01-13-make-some-job-fields-longer.js index 7b2fc2b4..286fd888 100644 --- a/migrations/2021-01-13-make-some-job-fields-longer.js +++ b/migrations/2021-01-13-make-some-job-fields-longer.js @@ -7,14 +7,14 @@ module.exports = { up: queryInterface => { return Promise.all([ - queryInterface.sequelize.query('ALTER TABLE bookings.jobs ALTER COLUMN title TYPE VARCHAR(128)'), - queryInterface.sequelize.query('ALTER TABLE bookings.jobs ALTER COLUMN description TYPE TEXT') + queryInterface.sequelize.query(`ALTER TABLE bookings.jobs ALTER COLUMN title TYPE VARCHAR(128)`), + queryInterface.sequelize.query(`ALTER TABLE bookings.jobs ALTER COLUMN description TYPE TEXT`) ]) }, down: queryInterface => { return Promise.all([ - queryInterface.sequelize.query('ALTER TABLE bookings.jobs ALTER COLUMN title TYPE VARCHAR(64)'), - queryInterface.sequelize.query('ALTER TABLE bookings.jobs ALTER COLUMN description TYPE VARCHAR(255)') + queryInterface.sequelize.query(`ALTER TABLE bookings.jobs ALTER COLUMN title TYPE VARCHAR(64)`), + queryInterface.sequelize.query(`ALTER TABLE bookings.jobs ALTER COLUMN description TYPE VARCHAR(255)`) ]) } } diff --git a/package-lock.json b/package-lock.json index d904d878..5b90459b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1796,69 +1796,7 @@ "dotenv": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", - "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==", - "dev": true - }, - "dotenv-cli": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-4.0.0.tgz", - "integrity": "sha512-ByKEec+ashePEXthZaA1fif9XDtcaRnkN7eGdBDx3HHRjwZ/rA1go83Cbs4yRrx3JshsCf96FjAyIA2M672+CQ==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.1", - "dotenv": "^8.1.0", - "dotenv-expand": "^5.1.0", - "minimist": "^1.1.3" - }, - "dependencies": { - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "dotenv-expand": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", - "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", - "dev": true + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" }, "dottie": { "version": "2.0.2", @@ -3177,9 +3115,9 @@ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "dev": true }, "inquirer": { diff --git a/package.json b/package.json index 1df401a2..15788a91 100644 --- a/package.json +++ b/package.json @@ -5,21 +5,17 @@ "main": "app.js", "scripts": { "start": "node app.js", - "dev": "dotenv nodemon app.js", + "dev": "nodemon app.js", "lint": "standard", "lint:fix": "standard --fix", - "services:up": "docker-compose -f local/docker-compose.yaml up -d", - "services:down": "docker-compose -f local/docker-compose.yaml down", - "services:logs": "docker-compose -f local/docker-compose.yaml logs", - "local:init": "npm run init-db && npm run create-index", - "init-db": "dotenv node src/init-db.js", - "create-index": "dotenv node scripts/createIndex.js", - "delete-index": "dotenv node scripts/deleteIndex.js", - "migrate": "dotenv npx sequelize db:migrate", - "migrate:undo": "dotenv npx sequelize db:migrate:undo", - "test-data": "dotenv node scripts/insert-es-data.js", - "test": "dotenv mocha test/unit/*.test.js --timeout 30000 --exit", - "cov": "dotenv nyc --reporter=html --reporter=text mocha test/unit/*.test.js --timeout 30000 --exit" + "init-db": "node src/init-db.js", + "create-index": "node scripts/createIndex.js", + "delete-index": "node scripts/deleteIndex.js", + "migrate": "npx sequelize db:migrate", + "migrate:undo": "npx sequelize db:migrate:undo", + "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" }, "keywords": [], "author": "", @@ -31,6 +27,7 @@ "config": "^3.3.2", "cors": "^2.8.5", "date-fns": "^2.16.1", + "dotenv": "^8.2.0", "express": "^4.17.1", "express-interceptor": "^1.2.0", "get-parameter-names": "^0.3.0", @@ -50,7 +47,6 @@ }, "devDependencies": { "chai": "^4.2.0", - "dotenv-cli": "^4.0.0", "mocha": "^8.1.3", "nodemon": "^2.0.4", "nyc": "^15.1.0", diff --git a/src/models/Job.js b/src/models/Job.js index d6cc3955..14cec753 100644 --- a/src/models/Job.js +++ b/src/models/Job.js @@ -69,7 +69,7 @@ module.exports = (sequelize) => { type: Sequelize.STRING(255) }, description: { - type: Sequelize.TEXT // technically unlimited length + type: Sequelize.TEXT, // technically unlimited length }, title: { type: Sequelize.STRING(128), From a871d2cb76d38a5906549b90eda076a933dbb918 Mon Sep 17 00:00:00 2001 From: maxceem Date: Tue, 2 Feb 2021 14:58:39 +0200 Subject: [PATCH 05/46] feat: improve local setup - docker-compose file to run all the dependant services - done by "phaniram" via challenge "30162708" (2nd place) --- README.md | 271 +++++++++++------- config/default.js | 38 +++ local/docker-compose.yml | 88 ++++++ local/generic-tc-service/Dockerfile | 15 + local/generic-tc-service/docker-entrypoint.sh | 19 ++ local/kafka-client/Dockerfile | 5 + local/kafka-client/create-topics.sh | 6 + local/kafka-client/topics.txt | 9 + package.json | 4 + 9 files changed, 359 insertions(+), 96 deletions(-) create mode 100644 local/docker-compose.yml create mode 100644 local/generic-tc-service/Dockerfile create mode 100755 local/generic-tc-service/docker-entrypoint.sh create mode 100644 local/kafka-client/Dockerfile create mode 100755 local/kafka-client/create-topics.sh create mode 100644 local/kafka-client/topics.txt diff --git a/README.md b/README.md index 4c142f0a..24ba1f7e 100644 --- a/README.md +++ b/README.md @@ -8,124 +8,203 @@ - Docker - Docker-Compose -## Configuration - -Configuration for the application is at `config/default.js`. - -The following parameters can be set in config files or in env variables: - -- `LOG_LEVEL`: the log level, default is 'debug' -- `PORT`: the server port, default is 3000 -- `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_UBAHN`: Auth0 audience for U-Bahn -- `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 - -- `m2m.M2M_AUDIT_USER_ID`: default value is `00000000-0000-0000-0000-000000000000` -- `m2m.M2M_AUDIT_HANDLE`: default value is `TopcoderService` - -- `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 -- `TOPCODER_SKILL_PROVIDER_ID`: the referenced skill provider id - -- `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 force` to force creating table) +### Steps to run locally -## DB Migration -- `npm run migrate`: run any migration files which haven't run yet. -- `npm run migrate:undo`: revert most recent migration. +1. 📦 Install npm dependencies -Configuration for migration is at `./config/config.json`. + ```bash + npm install + ``` -The following parameters can be set in the config file or via env variables: +2. ⚙ Local config -- `username`: set via env `DB_USERNAME`; datebase username -- `password`: set via env `DB_PASSWORD`; datebase password -- `database`: set via env `DB_NAME`; datebase name -- `host`: set via env `DB_HOST`; datebase host name + 1. In the `taas-apis` root directory create `.env` file with the next environment variables. Values for **Auth0 config** should be shared with you on the forum.
+ + ```bash + # Auth0 config + AUTH0_URL= + AUTH0_AUDIENCE= + AUTH0_AUDIENCE_UBAHN= + AUTH0_CLIENT_ID= + AUTH0_CLIENT_SECRET= + + # Locally deployed services (via docker-compose) + ES_HOST=dockerhost:9200 + DATABASE_URL=postgres://postgres:postgres@dockerhost:5432/postgres + BUSAPI_URL=http://dockerhost:8002/v5 + ``` + + - Values from this file would be automatically used by many `npm` commands. + - ⚠️ Never commit this file or its copy to the repository! + + 1. Set `dockerhost` to point the IP address of Docker. Docker IP address depends on your system. For example if docker is run on IP `127.0.0.1` add a the next line to your `/etc/hosts` file: + + ``` + 127.0.0.1 dockerhost + ``` + + Alternatively, you may update `.env` file and replace `dockerhost` with your docker IP address. + +3. 🚢 Start docker-compose with services which are required to start Topcoder Bookings API locally + + ```bash + npm run services:up + ``` + + Wait until all containers are fully started. As a good indicator, wait until `taas-es-processor` successfully started by viewing its logs: + + ```bash + npm run services:logs -- -f taas-es-processor + ``` + +
Click to see a good logs example +
+ + - first it would be waiting for `kafka-client` to create all the required topics and exit, you would see: + + ``` + tc-taas-es-procesor | Waiting for kafka-client to exit.... + ``` + + - after that, `taas-es-processor` would be started itself. Make sure it successfully connected to Kafka, you should see 9 lines with text `Subscribed to taas.`: + + ``` + tc-taas-es-procesor | 2021-01-22T14:27:48.971Z DEBUG no-kafka-client Subscribed to taas.jobcandidate.create:0 offset 0 leader kafka:9093 + tc-taas-es-procesor | 2021-01-22T14:27:48.972Z DEBUG no-kafka-client Subscribed to taas.job.create:0 offset 0 leader kafka:9093 + tc-taas-es-procesor | 2021-01-22T14:27:48.972Z DEBUG no-kafka-client Subscribed to taas.resourcebooking.delete:0 offset 0 leader kafka:9093 + tc-taas-es-procesor | 2021-01-22T14:27:48.973Z DEBUG no-kafka-client Subscribed to taas.jobcandidate.delete:0 offset 0 leader kafka:9093 + tc-taas-es-procesor | 2021-01-22T14:27:48.974Z DEBUG no-kafka-client Subscribed to taas.jobcandidate.update:0 offset 0 leader kafka:9093 + tc-taas-es-procesor | 2021-01-22T14:27:48.975Z DEBUG no-kafka-client Subscribed to taas.resourcebooking.create:0 offset 0 leader kafka:9093 + tc-taas-es-procesor | 2021-01-22T14:27:48.976Z DEBUG no-kafka-client Subscribed to taas.job.delete:0 offset 0 leader kafka:9093 + tc-taas-es-procesor | 2021-01-22T14:27:48.977Z DEBUG no-kafka-client Subscribed to taas.job.update:0 offset 0 leader kafka:9093 + tc-taas-es-procesor | 2021-01-22T14:27:48.978Z DEBUG no-kafka-client Subscribed to taas.resourcebooking.update:0 offset 0 leader kafka:9093 + ``` + +
+ +
+ If you want to learn more about docker-compose configuration +
see more details here +
+ + This docker-compose file starts the next services: + | Service | Name | Port | + |----------|:-----:|:----:| + | PostgreSQL | postgres | 5432 | + | Elasticsearch | elasticsearch | 9200 | + | Zookeeper | zookeeper | 2181 | + | Kafka | kafka | 9092 | + | [tc-bus-api](https://github.com/topcoder-platform/tc-bus-api) | tc-bus-api | 8002 | + | [taas-es-processor](https://github.com/topcoder-platform/taas-es-processor) | taas-es-processor | 5000 | + + - as many of the Topcoder services in this docker-compose require Auth0 configuration for M2M calls, our docker-compose file passes environment variables `AUTH0_CLIENT_ID`, `AUTH0_CLIENT_SECRET`, `AUTH0_URL`, `AUTH0_AUDIENCE`, `AUTH0_PROXY_SERVER_URL` to its containers. docker-compose takes them from `.env` file if provided. -## ElasticSearch Setup -- Go to https://www.elastic.co/downloads/ download and install the elasticsearch. -- Modify `esConfig` under `config/default.js` to meet your environment. -- Run `npm run create-index` to create ES index. -- Run `npm run delete-index` to delete ES index. + - `docker-compose` automatically would create Kafka topics which are used by `taas-es-processor` listed in `local/kafka-client/topics.txt`. -## Local Deployment + - To view the logs from any container inside docker-compose use the following command, replacing `SERVICE_NAME` with the corresponding value under the **Name** column in the above table: -- Install dependencies `npm install` -- Run lint `npm run lint` -- Run lint fix `npm run lint:fix` -- Clear and init db `npm run init-db force` -- Clear and create es index + ```bash + npm run services:log -- -f SERVICE_NAME + ``` - ``` bash - npm run delete-index # run this if you already created index - npm run create-index - ``` + - If you want to modify the code of any of the services which are run inside this docker-compose file, you can stop such service inside docker-compose by command `docker-compose -f local/docker-compose.yml stop -f ` and run the service separately, following its README file. -- Start app `npm start` -- App is running at `http://localhost:3000` +
-## Local Deployment with Docker + _NOTE: In production these dependencies / services are hosted & managed outside Topcoder Bookings API._ -Make sure all config values are right, and you can run on local successful, then run below commands +4. ♻ Init DB, ES -1. Navigate to the directory `docker` + ```bash + npm run local:init + ``` -2. Rename the file `sample.api.env` to `api.env` + This command will do 2 things: -3. Set the required AUTH0 configurations, PostgreSQL Database url and ElasticSearch host in the file `api.env` + - create Database table + - create Elasticsearch indexes - 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. +5. 🚀 Start Topcoder Bookings API -4. Once that is done, run the following command + ```bash + npm run dev + ``` - ```bash - docker-compose up - ``` + Runs the Topcoder Bookings API using nodemon, so it would be restarted after any of the files is updated. + The Topcoder Bookings API will be served on `http://localhost:3000`. -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 +## NPM Commands + +| Command                    | Description | +| ------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | +| `npm run lint` | Check for for lint errors. | +| `npm run lint:fix` | Check for for lint errors and fix error automatically when possible. | +| `npm run build` | Build source code for production run into `dist` folder. | +| `npm run start` | Start app in the production mode from prebuilt `dist` folder. | +| `npm run dev` | Start app in the development mode using `nodemon`. | +| `npm run test` | Run tests. | +| `npm run test-data` | Clears and imports Data into ES. | +| `npm run init-db` | Initializes Database. | +| `npm run create-index` | Create Elasticsearch indexes. | +| `npm run delete-index` | Delete Elasticsearch indexes. | +| `npm run services:up` | Start services via docker-compose for local development. | +| `npm run services:down` | Stop services via docker-compose for local development. | +| `npm run services:logs -- -f ` | View logs of some service inside docker-compose. | +| `npm run local:init` | Creates Elasticsearch indexes and initializes Database. | +| `npm run cov` | Code Coverage Report. | +| `npm run migrate` | Run any migration files which haven't run yet. | +| `npm run migrate:undo` | Revert most recent migration. | + +## Kafka commands + +If you've used `docker-compose` with the file `local/docker-compose.yml` during local setup to spawn kafka & zookeeper, you can use the following commands to manipulate kafka topics and messages: +(Replace `TOPIC_NAME` with the name of the desired topic) + +### Create Topic + +```bash +docker exec tc-taas-kafka /opt/kafka/bin/kafka-topics.sh --create --zookeeper zookeeper:2181 --partitions 1 --replication-factor 1 --topic TOPIC_NAME +``` + +### List Topics + +```bash +docker exec tc-taas-kafka /opt/kafka/bin/kafka-topics.sh --list --zookeeper zookeeper:2181 +``` + +### Watch Topic + +```bash +docker exec tc-taas-kafka /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic TOPIC_NAME +``` + +### Post Message to Topic (from stdin) + +```bash +docker exec -it tc-taas-kafka /opt/kafka/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic TOPIC_NAME +``` + +- Enter or copy/paste the message into the console after starting this command. + +## DB Migration + +- `npm run migrate`: run any migration files which haven't run yet. +- `npm run migrate:undo`: revert most recent migration. + +Configuration for migration is at `./config/config.json`. + +The following parameters can be set in the config file or via env variables: + +- `username`: set via env `DB_USERNAME`; datebase username +- `password`: set via env `DB_PASSWORD`; datebase password +- `database`: set via env `DB_NAME`; datebase name +- `host`: set via env `DB_HOST`; datebase host name ## 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) diff --git a/config/default.js b/config/default.js index d726ca58..e729d62b 100644 --- a/config/default.js +++ b/config/default.js @@ -1,17 +1,29 @@ require('dotenv').config() module.exports = { + // the log level, default is 'debug' LOG_LEVEL: process.env.LOG_LEVEL || 'debug', + // the server port, default is 3000 PORT: process.env.PORT || 3000, + // the server api base path BASE_PATH: process.env.BASE_PATH || '/api/v5', + // The authorization secret used during token verification. AUTH_SECRET: process.env.AUTH_SECRET || 'mysecret', + // The valid issuer of tokens, a json array contains valid issuer. 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, used to get TC M2M token AUTH0_URL: process.env.AUTH0_URL, + // Auth0 audience, used to get TC M2M token AUTH0_AUDIENCE: process.env.AUTH0_AUDIENCE, + // Auth0 audience for U-Bahn AUTH0_AUDIENCE_UBAHN: process.env.AUTH0_AUDIENCE_UBAHN, + // Auth0 token cache time, used to get TC M2M token TOKEN_CACHE_TIME: process.env.TOKEN_CACHE_TIME, + // Auth0 client id, used to get TC M2M token AUTH0_CLIENT_ID: process.env.AUTH0_CLIENT_ID, + // Auth0 client secret, used to get TC M2M token AUTH0_CLIENT_SECRET: process.env.AUTH0_CLIENT_SECRET, + // Proxy Auth0 URL, used to get TC M2M token AUTH0_PROXY_SERVER_URL: process.env.AUTH0_PROXY_SERVER_URL, m2m: { @@ -19,45 +31,71 @@ module.exports = { M2M_AUDIT_HANDLE: process.env.M2M_AUDIT_HANDLE || 'TopcoderService' }, + // the Topcoder v5 url TC_API: process.env.TC_API || 'https://api.topcoder-dev.com/v5', + // the organization id ORG_ID: process.env.ORG_ID || '36ed815b-3da1-49f1-a043-aaed0a4e81ad', + // the referenced skill provider id TOPCODER_SKILL_PROVIDER_ID: process.env.TOPCODER_SKILL_PROVIDER_ID || '9cc0795a-6e12-4c84-9744-15858dba1861', TOPCODER_USERS_API: process.env.TOPCODER_USERS_API || 'https://api.topcoder-dev.com/v3/users', + // PostgreSQL database url. DATABASE_URL: process.env.DATABASE_URL || 'postgres://postgres:postgres@localhost:5432/postgres', + // string - PostgreSQL database target schema DB_SCHEMA_NAME: process.env.DB_SCHEMA_NAME || 'bookings', + // the project service url PROJECT_API_URL: process.env.PROJECT_API_URL || 'https://api.topcoder-dev.com', esConfig: { + // the elasticsearch host HOST: process.env.ES_HOST || 'http://localhost:9200', ELASTICCLOUD: { + // 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 id: process.env.ELASTICCLOUD_ID, + // The elastic cloud username for basic authentication. Provide this only if your elasticsearch instance is hosted on elastic cloud username: process.env.ELASTICCLOUD_USERNAME, + // The elastic cloud password for basic authentication. Provide this only if your elasticsearch instance is hosted on elastic cloud password: process.env.ELASTICCLOUD_PASSWORD }, + // The Amazon region to use when using AWS Elasticsearch service AWS_REGION: process.env.AWS_REGION || 'us-east-1', // AWS Region to be used if we use AWS ES + // the job index ES_INDEX_JOB: process.env.ES_INDEX_JOB || 'job', + // the job candidate index ES_INDEX_JOB_CANDIDATE: process.env.ES_INDEX_JOB_CANDIDATE || 'job_candidate', + // the resource booking index ES_INDEX_RESOURCE_BOOKING: process.env.ES_INDEX_RESOURCE_BOOKING || 'resource_booking' }, + // Topcoder Bus API URL BUSAPI_URL: process.env.BUSAPI_URL || 'https://api.topcoder-dev.com/v5', + // The error topic at which bus api will publish any errors KAFKA_ERROR_TOPIC: process.env.KAFKA_ERROR_TOPIC || 'common.error.reporting', + // The originator value for the kafka messages KAFKA_MESSAGE_ORIGINATOR: process.env.KAFKA_MESSAGE_ORIGINATOR || 'taas-api', // topics for job service + // the create job entity Kafka message topic TAAS_JOB_CREATE_TOPIC: process.env.TAAS_JOB_CREATE_TOPIC || 'taas.job.create', + // the update job entity Kafka message topic TAAS_JOB_UPDATE_TOPIC: process.env.TAAS_JOB_UPDATE_TOPIC || 'taas.job.update', + // the delete job entity Kafka message topic TAAS_JOB_DELETE_TOPIC: process.env.TAAS_JOB_DELETE_TOPIC || 'taas.job.delete', // topics for jobcandidate service + // the create job candidate entity Kafka message topic TAAS_JOB_CANDIDATE_CREATE_TOPIC: process.env.TAAS_JOB_CANDIDATE_CREATE_TOPIC || 'taas.jobcandidate.create', + // the update job candidate entity Kafka message topic TAAS_JOB_CANDIDATE_UPDATE_TOPIC: process.env.TAAS_JOB_CANDIDATE_UPDATE_TOPIC || 'taas.jobcandidate.update', + // the delete job candidate entity Kafka message topic TAAS_JOB_CANDIDATE_DELETE_TOPIC: process.env.TAAS_JOB_CANDIDATE_DELETE_TOPIC || 'taas.jobcandidate.delete', // topics for job service + // the create resource booking entity Kafka message topic TAAS_RESOURCE_BOOKING_CREATE_TOPIC: process.env.TAAS_RESOURCE_BOOKING_CREATE_TOPIC || 'taas.resourcebooking.create', + // the update resource booking entity Kafka message topic TAAS_RESOURCE_BOOKING_UPDATE_TOPIC: process.env.TAAS_RESOURCE_BOOKING_UPDATE_TOPIC || 'taas.resourcebooking.update', + // the delete resource booking entity Kafka message topic TAAS_RESOURCE_BOOKING_DELETE_TOPIC: process.env.TAAS_RESOURCE_BOOKING_DELETE_TOPIC || 'taas.resourcebooking.delete' } diff --git a/local/docker-compose.yml b/local/docker-compose.yml new file mode 100644 index 00000000..509d8640 --- /dev/null +++ b/local/docker-compose.yml @@ -0,0 +1,88 @@ +version: "3" +services: + postgres: + container_name: tc-taas-postgres + image: postgres + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - 5432:5432 + + zookeeper: + image: wurstmeister/zookeeper + container_name: tc-taas-zookeeper + ports: + - 2181:2181 + + kafka: + image: wurstmeister/kafka + container_name: tc-taas-kafka + depends_on: + - zookeeper + ports: + - 9092:9092 + environment: + KAFKA_ADVERTISED_LISTENERS: INSIDE://kafka:9093,OUTSIDE://localhost:9092 + KAFKA_LISTENERS: INSIDE://kafka:9093,OUTSIDE://0.0.0.0:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + + kafka-client: + container_name: tc-kafka-client + build: ./kafka-client + depends_on: + - kafka + - zookeeper + + elasticsearch: + image: elasticsearch:7.7.1 + container_name: tc-taas-elasticsearch + environment: + - discovery.type=single-node + ports: + - 9200:9200 + + taas-es-processor: + container_name: tc-taas-es-procesor + build: + context: ./generic-tc-service + args: + NODE_VERSION: 12.16.3 + GIT_URL: https://github.com/topcoder-platform/taas-es-processor + GIT_BRANCH: dev + command: start kafka-client + ports: + - 5000:5000 + depends_on: + - kafka-client + - elasticsearch + environment: + - KAFKA_URL=kafka:9093 + - ES_HOST=http://elasticsearch:9200 + + tc-bus-api: + container_name: tc-bus-api + build: + context: ./generic-tc-service + args: + NODE_VERSION: 8.11.3 + GIT_URL: https://github.com/topcoder-platform/tc-bus-api + GIT_BRANCH: dev + BYPASS_TOKEN_VALIDATION: 1 + command: start kafka-client + ports: + - 8002:8002 + depends_on: + - kafka-client + environment: + - PORT=8002 + - KAFKA_URL=kafka:9093 + - JWT_TOKEN_SECRET=secret + - VALID_ISSUERS="[\"https:\/\/topcoder-newauth.auth0.com\/\",\"https:\/\/api.topcoder-dev.com\",\"https:\/\/topcoder-dev.auth0.com\/\"]" + - AUTH0_URL + - AUTH0_AUDIENCE + - AUTH0_CLIENT_ID + - AUTH0_CLIENT_SECRET + - AUTH0_PROXY_SERVER_URL diff --git a/local/generic-tc-service/Dockerfile b/local/generic-tc-service/Dockerfile new file mode 100644 index 00000000..63d00bb3 --- /dev/null +++ b/local/generic-tc-service/Dockerfile @@ -0,0 +1,15 @@ +ARG NODE_VERSION=12.16.3 + +FROM node:$NODE_VERSION +ARG GIT_URL +ARG GIT_BRANCH +ARG BYPASS_TOKEN_VALIDATION + +RUN git clone $GIT_URL /opt/app +WORKDIR /opt/app +RUN git checkout -b node-branch origin/$GIT_BRANCH + +RUN npm install +RUN if [ $BYPASS_TOKEN_VALIDATION -eq 1 ]; then sed -i '/decodedToken = jwt.decode/a \ callback(undefined, decodedToken.payload); return;' node_modules/tc-core-library-js/lib/auth/verifier.js; fi +COPY docker-entrypoint.sh /opt/ +ENTRYPOINT ["/opt/docker-entrypoint.sh"] \ No newline at end of file diff --git a/local/generic-tc-service/docker-entrypoint.sh b/local/generic-tc-service/docker-entrypoint.sh new file mode 100755 index 00000000..771f3ba5 --- /dev/null +++ b/local/generic-tc-service/docker-entrypoint.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +HOST_DOMAIN="host.docker.internal" +ping -q -c1 $HOST_DOMAIN > /dev/null 2>&1 +if [ $? -ne 0 ]; then + HOST_IP=$(ip route | awk 'NR==1 {print $3}') + echo -e "$HOST_IP\t$HOST_DOMAIN" >> /etc/hosts +fi + +if [ $# -eq 2 ]; then + echo "Waiting for $2 to exit...." + while ping -c1 $2 &>/dev/null + do + sleep 1 + done + echo "$2 exited!" +fi + +cd /opt/app/ && npm run $1 \ No newline at end of file diff --git a/local/kafka-client/Dockerfile b/local/kafka-client/Dockerfile new file mode 100644 index 00000000..e34a3ae6 --- /dev/null +++ b/local/kafka-client/Dockerfile @@ -0,0 +1,5 @@ +From wurstmeister/kafka +WORKDIR /app/ +COPY topics.txt . +COPY create-topics.sh . +ENTRYPOINT ["/app/create-topics.sh"] \ No newline at end of file diff --git a/local/kafka-client/create-topics.sh b/local/kafka-client/create-topics.sh new file mode 100755 index 00000000..88339fe9 --- /dev/null +++ b/local/kafka-client/create-topics.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +/opt/kafka/bin/kafka-topics.sh --list --zookeeper zookeeper:2181 > exists-topics.txt +while read topic; do + /opt/kafka/bin/kafka-topics.sh --create --if-not-exists --zookeeper zookeeper:2181 --partitions 1 --replication-factor 1 --topic $topic +done < <(sort topics.txt exists-topics.txt exists-topics.txt | uniq -u) \ No newline at end of file diff --git a/local/kafka-client/topics.txt b/local/kafka-client/topics.txt new file mode 100644 index 00000000..6888c6bd --- /dev/null +++ b/local/kafka-client/topics.txt @@ -0,0 +1,9 @@ +taas.job.create +taas.jobcandidate.create +taas.resourcebooking.create +taas.job.update +taas.jobcandidate.update +taas.resourcebooking.update +taas.job.delete +taas.jobcandidate.delete +taas.resourcebooking.delete \ No newline at end of file diff --git a/package.json b/package.json index 15788a91..d65a6882 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,10 @@ "migrate:undo": "npx sequelize db:migrate:undo", "test-data": "node scripts/insert-es-data.js", "test": "mocha test/unit/*.test.js --timeout 30000 --exit", + "services:up": "docker-compose -f ./local/docker-compose.yml up -d", + "services:down": "docker-compose -f ./local/docker-compose.yml down", + "services:logs": "docker-compose -f ./local/docker-compose.yml logs", + "local:init": "npm run create-index && npm run init-db", "cov": "nyc --reporter=html --reporter=text mocha test/unit/*.test.js --timeout 30000 --exit" }, "keywords": [], From c6f0dc535b6b8006ef39cf64d247ee298b3ecfca Mon Sep 17 00:00:00 2001 From: maxceem Date: Tue, 2 Feb 2021 15:09:33 +0200 Subject: [PATCH 06/46] docs: improve README - list only local setup requirements - remove redundant verification guide - added .nvmrc file --- .nvmrc | 1 + README.md | 14 ++++---------- Verification.md | 40 ---------------------------------------- 3 files changed, 5 insertions(+), 50 deletions(-) create mode 100644 .nvmrc delete mode 100644 Verification.md diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..b06cd07c --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +12.18.0 diff --git a/README.md b/README.md index 24ba1f7e..e8870c8d 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,10 @@ # Topcoder Bookings API -## Dependencies +## Requirements -- nodejs https://nodejs.org/en/ (v12+) -- PostgreSQL -- ElasticSearch (7.x) -- Docker -- Docker-Compose +- [Node.js](https://nodejs.org/en/) v12+ +- [Docker](https://www.docker.com/) +- [Docker-Compose](https://docs.docker.com/compose/install/) ### Steps to run locally @@ -204,7 +202,3 @@ The following parameters can be set in the config file or via env variables: - 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) diff --git a/Verification.md b/Verification.md deleted file mode 100644 index 4867b6ea..00000000 --- a/Verification.md +++ /dev/null @@ -1,40 +0,0 @@ -# Topcoder Bookings API - -## Postman test -- 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 - - -``` bash - 96 passing (170ms) - -----------------------------|---------|----------|---------|---------|---------------------------- -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 -----------------------------|---------|----------|---------|---------|---------------------------- -``` From 6e52f14f0026b862ca0a5e5f7b01f1bcf61ff812 Mon Sep 17 00:00:00 2001 From: maxceem Date: Tue, 2 Feb 2021 15:50:57 +0200 Subject: [PATCH 07/46] docs: rename Bookings API to TaaS API --- README.md | 12 ++++++------ package.json | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e8870c8d..4c1df143 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Topcoder Bookings API +# Topcoder TaaS API ## Requirements @@ -43,7 +43,7 @@ Alternatively, you may update `.env` file and replace `dockerhost` with your docker IP address. -3. 🚢 Start docker-compose with services which are required to start Topcoder Bookings API locally +3. 🚢 Start docker-compose with services which are required to start Topcoder TaaS API locally ```bash npm run services:up @@ -109,7 +109,7 @@
- _NOTE: In production these dependencies / services are hosted & managed outside Topcoder Bookings API._ + _NOTE: In production these dependencies / services are hosted & managed outside Topcoder TaaS API._ 4. ♻ Init DB, ES @@ -122,14 +122,14 @@ - create Database table - create Elasticsearch indexes -5. 🚀 Start Topcoder Bookings API +5. 🚀 Start Topcoder TaaS API ```bash npm run dev ``` - Runs the Topcoder Bookings API using nodemon, so it would be restarted after any of the files is updated. - The Topcoder Bookings API will be served on `http://localhost:3000`. + Runs the Topcoder TaaS API using nodemon, so it would be restarted after any of the files is updated. + The Topcoder TaaS API will be served on `http://localhost:3000`. ## NPM Commands diff --git a/package.json b/package.json index eb282dde..65029201 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "topcoder-bookings-api", + "name": "taas-apis", "version": "1.0.0", - "description": "Topcoder Bookings API", + "description": "Topcoder TaaS API", "main": "app.js", "scripts": { "start": "node app.js", From 76a20ca64ff934b1f7310a24418eedb16f4e2e3c Mon Sep 17 00:00:00 2001 From: imcaizheng Date: Wed, 3 Feb 2021 03:09:24 +0800 Subject: [PATCH 08/46] RCRM Import script: add total records created or being already exist to ummary --- scripts/recruit-crm-job-import/index.js | 26 ++++++++++++------------ scripts/recruit-crm-job-import/report.js | 17 +++++++++++++--- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/scripts/recruit-crm-job-import/index.js b/scripts/recruit-crm-job-import/index.js index e807e7f9..b0d891ae 100644 --- a/scripts/recruit-crm-job-import/index.js +++ b/scripts/recruit-crm-job-import/index.js @@ -84,8 +84,8 @@ function getPathname () { * Process single job data. The processing consists of: * - Validate the data. * - Skip processing if externalId is missing. - * - Create a job if it is not already exists. - * - Create a resource booking if it is not already exists. + * - Create a job if it does not already exist. + * - Create a resource booking if it does not already exist. * - Update the resourceBooking based on startDate and endDate. * * @param {Object} job the job data @@ -96,47 +96,47 @@ async function processJob (job, info = []) { // validate the data const { value: data, error } = validateJob(job) if (error) { - info.push(error.details[0].message) + info.push({ text: error.details[0].message, tag: 'validation_error' }) return { status: constants.ProcessingStatus.Failed, info } } if (!data.externalId) { - info.push('externalId is missing') + info.push({ text: 'externalId is missing', tag: 'external_id_missing' }) return { status: constants.ProcessingStatus.Skipped, info } } data.projectId = (await helper.getProjectByDirectProjectId(data.directProjectId)).id - // create a job if it is not already exists + // create a job if it does not already exist try { const result = await helper.getJobByExternalId(data.externalId) - info.push(`id: ${result.id} externalId: ${data.externalId} job already exists`) + info.push({ text: `id: ${result.id} externalId: ${data.externalId} job already exists`, tag: 'job_already_exists' }) data.jobId = result.id } catch (err) { if (!(err.message && err.message.includes('job not found'))) { throw err } const result = await helper.createJob(_.pick(data, ['projectId', 'externalId', 'title', 'numPositions', 'skills'])) - info.push(`id: ${result.id} job created`) + info.push({ text: `id: ${result.id} job created`, tag: 'job_created' }) data.jobId = result.id } data.userId = (await helper.getUserByHandle(data.userHandle)).id logger.debug(`userHandle: ${data.userHandle} userId: ${data.userId}`) - // create a resource booking if it is not already exists + // create a resource booking if it does not already exist try { const result = await helper.getResourceBookingByJobIdAndUserId(data.jobId, data.userId) - info.push(`id: ${result.id} resource booking already exists`) + info.push({ text: `id: ${result.id} resource booking already exists`, tag: 'resource_booking_already_exists' }) return { status: constants.ProcessingStatus.Successful, info } } catch (err) { if (!(err.message && err.message.includes('resource booking not found'))) { throw err } const result = await helper.createResourceBooking(_.pick(data, ['projectId', 'jobId', 'userId', 'startDate', 'endDate', 'memberRate', 'customerRate', 'rateType'])) - info.push(`id: ${result.id} resource booking created`) + info.push({ text: `id: ${result.id} resource booking created`, tag: 'resource_booking_created' }) data.resourceBookingId = result.id } // update the resourceBooking based on startDate and endDate const resourceBookingStatus = dateFNS.compareAsc(new Date(data.startDate), new Date(data.endDate)) === 1 ? 'closed' : 'assigned' logger.debug(`resourceBookingId: ${data.resourceBookingId} status: ${resourceBookingStatus}`) await helper.updateResourceBookingStatus(data.resourceBookingId, resourceBookingStatus) - info.push(`id: ${data.resourceBookingId} status: ${resourceBookingStatus} resource booking updated`) + info.push({ text: `id: ${data.resourceBookingId} status: ${resourceBookingStatus} resource booking updated`, tag: 'resource_booking_status_updated' }) return { status: constants.ProcessingStatus.Successful, info } } @@ -156,9 +156,9 @@ async function main () { report.add({ lnum: job._lnum, ...result }) } catch (err) { if (err.response) { - report.add({ lnum: job._lnum, status: constants.ProcessingStatus.Failed, info: [err.response.error.toString().split('\n')[0]] }) + report.add({ lnum: job._lnum, status: constants.ProcessingStatus.Failed, info: [{ text: err.response.error.toString().split('\n')[0], tag: 'request_error' }] }) } else { - report.add({ lnum: job._lnum, status: constants.ProcessingStatus.Failed, info: [err.message] }) + report.add({ lnum: job._lnum, status: constants.ProcessingStatus.Failed, info: [{ text: err.message, tag: 'internal_error' }] }) } } report.print() diff --git a/scripts/recruit-crm-job-import/report.js b/scripts/recruit-crm-job-import/report.js index 574dd8f3..ef31a4c8 100644 --- a/scripts/recruit-crm-job-import/report.js +++ b/scripts/recruit-crm-job-import/report.js @@ -19,7 +19,7 @@ class Report { // print the last message to the console print () { const lastMessage = this.messages[this.messages.length - 1] - const output = `#${lastMessage.lnum} - ${lastMessage.info.join('; ')}` + const output = `#${lastMessage.lnum} - ${_.map(lastMessage.info, 'text').join('; ')}` if (lastMessage.status === constants.ProcessingStatus.Skipped) { logger.warn(output) } @@ -33,15 +33,26 @@ class Report { // print a summary to the console printSummary () { + // summarize total success, failure, skips const groups = _.groupBy(this.messages, 'status') - const sucesss = groups[constants.ProcessingStatus.Successful] || [] + const success = groups[constants.ProcessingStatus.Successful] || [] const failure = groups[constants.ProcessingStatus.Failed] || [] const skips = groups[constants.ProcessingStatus.Skipped] || [] + // summarize records created or already existing + const groupsByTag = _.groupBy(_.flatten(_.map(this.messages, message => message.info)), 'tag') + const jobsCreated = groupsByTag.job_created || [] + const resourceBookingsCreated = groupsByTag.resource_booking_created || [] + const jobsAlreadyExist = groupsByTag.job_already_exists || [] + const resourceBookingsAlreadyExist = groupsByTag.resource_booking_already_exists || [] logger.info('=== summary ===') logger.info(`total: ${this.messages.length}`) - logger.info(`success: ${sucesss.length}`) + logger.info(`success: ${success.length}`) logger.info(`failure: ${failure.length}`) logger.info(`skips: ${skips.length}`) + logger.info(`jobs created: ${jobsCreated.length}`) + logger.info(`resource bookings created: ${resourceBookingsCreated.length}`) + logger.info(`jobs already exist: ${jobsAlreadyExist.length}`) + logger.info(`resource bookings already exist: ${resourceBookingsAlreadyExist.length}`) logger.info('=== summary ===') } } From 2c58fad766ddbf3b4834d5dd109ba5966c57e79f Mon Sep 17 00:00:00 2001 From: Jason Starlight Date: Wed, 3 Feb 2021 12:44:49 +0530 Subject: [PATCH 09/46] final fixes --- README.md | 11 +- config/default.js | 10 +- data/demo-data.json | 1 + package-lock.json | 967 +++++++++++++++++++++++- package.json | 16 +- scripts/createIndex.js | 89 --- scripts/data/exportData.js | 24 + scripts/data/importData.js | 24 + scripts/deleteIndex.js | 26 - scripts/es/createIndex.js | 29 + scripts/es/deleteIndex.js | 29 + scripts/es/reIndexAll.js | 24 + scripts/es/reIndexJobCandidates.js | 37 + scripts/es/reIndexJobs.js | 37 + scripts/es/reIndexResourceBookings.js | 37 + scripts/feed-data/jobCandidates.json | 152 ---- scripts/feed-data/jobs.json | 302 -------- scripts/feed-data/resourceBookings.json | 195 ----- scripts/insert-es-data.js | 92 --- src/common/helper.js | 381 ++++++++++ src/models/Job.js | 2 +- 21 files changed, 1618 insertions(+), 867 deletions(-) create mode 100644 data/demo-data.json delete mode 100644 scripts/createIndex.js create mode 100644 scripts/data/exportData.js create mode 100644 scripts/data/importData.js delete mode 100644 scripts/deleteIndex.js create mode 100644 scripts/es/createIndex.js create mode 100644 scripts/es/deleteIndex.js create mode 100644 scripts/es/reIndexAll.js create mode 100644 scripts/es/reIndexJobCandidates.js create mode 100644 scripts/es/reIndexJobs.js create mode 100644 scripts/es/reIndexResourceBookings.js delete mode 100644 scripts/feed-data/jobCandidates.json delete mode 100644 scripts/feed-data/jobs.json delete mode 100644 scripts/feed-data/resourceBookings.json delete mode 100644 scripts/insert-es-data.js diff --git a/README.md b/README.md index 4c1df143..e296a80a 100644 --- a/README.md +++ b/README.md @@ -141,10 +141,15 @@ | `npm run start` | Start app in the production mode from prebuilt `dist` folder. | | `npm run dev` | Start app in the development mode using `nodemon`. | | `npm run test` | Run tests. | -| `npm run test-data` | Clears and imports Data into ES. | | `npm run init-db` | Initializes Database. | -| `npm run create-index` | Create Elasticsearch indexes. | -| `npm run delete-index` | Delete Elasticsearch indexes. | +| `npm run create-index` | Create Elasticsearch indexes. Use `-- --force` flag to skip confirmation | +| `npm run delete-index` | Delete Elasticsearch indexes. Use `-- --force` flag to skip confirmation | +| `npm run data:import ` | Imports data into ES and db from filePath (`./data/demo-data.json` is used as default). Use `-- --force` flag to skip confirmation | +| `npm run data:export ` | Exports data from ES and db into filePath (`./data/demo-data.json` is used as default). Use `-- --force` flag to skip confirmation | +| `npm run index:all` | Indexes all data from db into ES. Use `-- --force` flag to skip confirmation| +| `npm run index:jobs ` | Indexes job data from db into ES, if jobId is not given all data is indexed. Use `-- --force` flag to skip confirmation | +| `npm run index:job-candidates ` | Indexes job candidate data from db into ES, if jobCandidateId is not given all data is indexed. Use `-- --force` flag to skip confirmation | +| `npm run index:resource-bookings ` | Indexes resource bookings data from db into ES, if resourceBookingsId is not given all data is indexed. Use `-- --force` flag to skip confirmation | | `npm run services:up` | Start services via docker-compose for local development. | | `npm run services:down` | Stop services via docker-compose for local development. | | `npm run services:logs -- -f ` | View logs of some service inside docker-compose. | diff --git a/config/default.js b/config/default.js index e729d62b..afda7a7b 100644 --- a/config/default.js +++ b/config/default.js @@ -47,6 +47,9 @@ module.exports = { // the project service url PROJECT_API_URL: process.env.PROJECT_API_URL || 'https://api.topcoder-dev.com', + // the default path for importing and exporting data + DEFAULT_DATA_FILE_PATH: './data/demo-data.json', + esConfig: { // the elasticsearch host HOST: process.env.ES_HOST || 'http://localhost:9200', @@ -68,7 +71,12 @@ module.exports = { // the job candidate index ES_INDEX_JOB_CANDIDATE: process.env.ES_INDEX_JOB_CANDIDATE || 'job_candidate', // the resource booking index - ES_INDEX_RESOURCE_BOOKING: process.env.ES_INDEX_RESOURCE_BOOKING || 'resource_booking' + ES_INDEX_RESOURCE_BOOKING: process.env.ES_INDEX_RESOURCE_BOOKING || 'resource_booking', + + // the max bulk size in MB for ES indexing + MAX_BULK_REQUEST_SIZE_MB: process.env.MAX_BULK_REQUEST_SIZE_MB || 20, + // the max number of documents per bulk for ES indexing + MAX_BULK_NUM_DOCUMENTS: process.env.MAX_BULK_NUM_DOCUMENTS || 100 }, // Topcoder Bus API URL diff --git a/data/demo-data.json b/data/demo-data.json new file mode 100644 index 00000000..6c8872b4 --- /dev/null +++ b/data/demo-data.json @@ -0,0 +1 @@ +{"Job":[{"id":"b9887564-3d3d-4c70-8a7b-552576ef2e8d","projectId":111,"externalId":"0","description":"taas-demo-job1","title":"Demo Title","startDate":"2020-09-27T04:17:23.131Z","endDate":"2020-09-27T04:17:23.131Z","numPositions":13,"resourceType":"Dummy Resource Type","rateType":"weekly","workload":"full-time","skills":["ee4c50c1-c8c3-475e-b6b6-edbd136a19d6","89139c80-d0a2-47c2-aa16-14589d5afd10","9f2d9127-6a2e-4506-ad76-c4ab63577b09","9515e7ee-83b6-49d1-ba5c-6c59c5a8ef1b","c854ab55-5922-4be1-8ecc-b3bc1f8629af","8456002e-fa2d-44f0-b0e7-86b1c02b6e4c","114b4ec8-805e-4c60-b351-14a955a991a9","213408aa-f16f-46c8-bc57-9e569cee3f11","b37a48db-f775-4e4e-b403-8ad1d234cdea","99b930b5-1b91-4df1-8b17-d9307107bb51","6388a632-c3ad-4525-9a73-66a527c03672","23839f38-6f19-4de9-9d28-f020056bca73","289e42a3-23e9-49be-88e1-6deb93cd8c31","b403f209-63b5-42bc-9b5f-1564416640d8"],"status":"sourcing","createdAt":"2021-01-28T19:36:33.409Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":null,"updatedBy":null},{"id":"a5b3bf94-a8bf-4c7e-b685-70a29a4d7d6e","projectId":111,"externalId":"0","description":"taas-demo-job2","title":"Dummy title - at most 64 characters","startDate":"2020-09-27T04:17:23.131Z","endDate":"2020-09-27T04:17:23.131Z","numPositions":7,"resourceType":"Dummy Resource Type","rateType":"weekly","workload":"full-time","skills":["213408aa-f16f-46c8-bc57-9e569cee3f11","b37a48db-f775-4e4e-b403-8ad1d234cdea","99b930b5-1b91-4df1-8b17-d9307107bb51","6388a632-c3ad-4525-9a73-66a527c03672","23839f38-6f19-4de9-9d28-f020056bca73","289e42a3-23e9-49be-88e1-6deb93cd8c31","b403f209-63b5-42bc-9b5f-1564416640d8"],"status":"in-review","createdAt":"2021-01-28T19:36:44.975Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":"2021-01-28T19:38:17.463Z","updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c"},{"id":"2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0","projectId":111,"externalId":"0","description":"taas-demo-job3","title":"Dummy title - at most 64 characters","startDate":"2020-09-27T04:17:23.131Z","endDate":"2020-09-27T04:17:23.131Z","numPositions":7,"resourceType":"Dummy Resource Type","rateType":"weekly","workload":"full-time","skills":[],"status":"assigned","createdAt":"2021-01-28T19:38:23.739Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":"2021-01-28T19:41:10.607Z","updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c"},{"id":"84b73f90-0fef-4507-887a-074578e5ef38","projectId":111,"externalId":"0","description":"taas-demo-job4","title":"Dummy title - at most 64 characters","startDate":"2020-09-27T04:17:23.131Z","endDate":"2020-09-27T04:17:23.131Z","numPositions":7,"resourceType":"Dummy Resource Type","rateType":"weekly","workload":"full-time","skills":["8456002e-fa2d-44f0-b0e7-86b1c02b6e4c","114b4ec8-805e-4c60-b351-14a955a991a9","213408aa-f16f-46c8-bc57-9e569cee3f11","b37a48db-f775-4e4e-b403-8ad1d234cdea","99b930b5-1b91-4df1-8b17-d9307107bb51","6388a632-c3ad-4525-9a73-66a527c03672"],"status":"closed","createdAt":"2021-01-28T19:41:21.892Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":"2021-01-28T19:41:28.849Z","updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c"},{"id":"62399aa0-b088-41fe-9e9b-0c8071f1934f","projectId":111,"externalId":"0","description":"taas-demo-job5","title":"Dummy title - at most 64 characters","startDate":"2020-09-27T04:17:23.131Z","endDate":"2020-09-27T04:17:23.131Z","numPositions":7,"resourceType":"Dummy Resource Type","rateType":"weekly","workload":"full-time","skills":["b37a48db-f775-4e4e-b403-8ad1d234cdea","99b930b5-1b91-4df1-8b17-d9307107bb51","6388a632-c3ad-4525-9a73-66a527c03672"],"status":"cancelled","createdAt":"2021-01-28T19:41:35.098Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":"2021-01-28T19:41:42.124Z","updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c"}],"JobCandidate":[{"id":"debadcd8-64bf-4ab8-9cdb-297479eef6f5","jobId":"a5b3bf94-a8bf-4c7e-b685-70a29a4d7d6e","userId":"4dfc6090-4ba8-4387-b5c4-584fcef982ba","status":"open","externalId":null,"resume":null,"createdAt":"2021-01-28T19:37:05.723Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":null,"updatedBy":null},{"id":"7ff45b8f-2b71-4510-b760-8dfa62e79504","jobId":"a5b3bf94-a8bf-4c7e-b685-70a29a4d7d6e","userId":"243517dd-77d7-4f70-8951-0bc66da83076","status":"open","externalId":null,"resume":null,"createdAt":"2021-01-28T19:37:11.598Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":null,"updatedBy":null},{"id":"91d63d5f-01d5-419e-89df-6117ea92f535","jobId":"a5b3bf94-a8bf-4c7e-b685-70a29a4d7d6e","userId":"a2e28bf4-1147-41a6-a39f-e2509306f2a6","status":"open","externalId":null,"resume":null,"createdAt":"2021-01-28T19:37:18.066Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":null,"updatedBy":null},{"id":"257f98d9-45f7-4e13-a6c2-d7e7b6efc9fe","jobId":"a5b3bf94-a8bf-4c7e-b685-70a29a4d7d6e","userId":"b8649393-d32f-4b7f-a156-12e9776acb0e","status":"open","externalId":null,"resume":null,"createdAt":"2021-01-28T19:37:24.095Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":null,"updatedBy":null},{"id":"a01852d0-fa08-410c-b97b-67580ce62215","jobId":"a5b3bf94-a8bf-4c7e-b685-70a29a4d7d6e","userId":"a0a3a5ce-1de6-465d-91b2-518feb299851","status":"open","externalId":null,"resume":null,"createdAt":"2021-01-28T19:37:29.734Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":null,"updatedBy":null},{"id":"2fd7ca69-c8ec-4bf3-a7f3-655fbfe3e7df","jobId":"a5b3bf94-a8bf-4c7e-b685-70a29a4d7d6e","userId":"e6958d77-ffaf-4d24-9cdb-6391506695a4","status":"open","externalId":null,"resume":null,"createdAt":"2021-01-28T19:37:44.728Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":null,"updatedBy":null},{"id":"f0023058-2996-4bba-8c5f-d09a7023be38","jobId":"a5b3bf94-a8bf-4c7e-b685-70a29a4d7d6e","userId":"626bb327-e738-48e3-8f67-1fa2dc677d3c","status":"open","externalId":null,"resume":null,"createdAt":"2021-01-28T19:37:50.619Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":null,"updatedBy":null},{"id":"a189b34d-acde-4633-b18b-f7a34d7c5a74","jobId":"a5b3bf94-a8bf-4c7e-b685-70a29a4d7d6e","userId":"b49a0adb-1565-4de1-9189-a763c77f5ed4","status":"open","externalId":null,"resume":null,"createdAt":"2021-01-28T19:37:56.456Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":null,"updatedBy":null},{"id":"5191a860-4327-4c50-b76b-84beba04519b","jobId":"a5b3bf94-a8bf-4c7e-b685-70a29a4d7d6e","userId":"79ce2a3e-7679-48cf-8ac9-0a8ca4c4b463","status":"shortlist","externalId":null,"resume":null,"createdAt":"2021-01-28T19:36:51.222Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":"2021-01-28T19:38:02.293Z","updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c"},{"id":"e6d9635c-b122-4f69-9285-09fb1ab30106","jobId":"a5b3bf94-a8bf-4c7e-b685-70a29a4d7d6e","userId":"98ec2c16-442e-4b61-8ad1-66123ee37d3c","status":"rejected","externalId":null,"resume":null,"createdAt":"2021-01-28T19:36:58.774Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":"2021-01-28T19:38:13.553Z","updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c"},{"id":"f67b155e-0f09-4fdd-89a7-d79c5e46cac6","jobId":"2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0","userId":"05e988b7-7d54-4c10-ada1-1a04870a88a8","status":"open","externalId":null,"resume":null,"createdAt":"2021-01-28T19:38:38.332Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":null,"updatedBy":null},{"id":"8ffd33d3-4a43-4719-aee4-8e46be1d8f1c","jobId":"2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0","userId":"a2ffdeed-704d-4cf7-b70a-93fcf61de598","status":"open","externalId":null,"resume":null,"createdAt":"2021-01-28T19:38:43.967Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":null,"updatedBy":null},{"id":"2b8ba549-8878-43d6-ad5f-6a66e3b9d6c9","jobId":"2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0","userId":"4709473d-f060-4102-87f8-4d51ff0b34c1","status":"open","externalId":null,"resume":null,"createdAt":"2021-01-28T19:38:50.106Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":null,"updatedBy":null},{"id":"ae5a81ec-5d05-43c4-8253-847d91a54711","jobId":"2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0","userId":"39c7376e-2d5c-4601-bc47-6b60f505814d","status":"open","externalId":null,"resume":null,"createdAt":"2021-01-28T19:38:55.734Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":null,"updatedBy":null},{"id":"85d6649e-2682-4904-9480-a77b72fef27d","jobId":"2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0","userId":"213d2dd9-1fc3-4eda-ad97-2d56e2a84a1e","status":"selected","externalId":null,"resume":null,"createdAt":"2021-01-28T19:38:30.856Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":"2021-01-28T19:40:27.209Z","updatedBy":"00000000-0000-0000-0000-000000000000"},{"id":"922dfce3-4e06-4387-9fdb-64f70675e86b","jobId":"2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0","userId":"dd5adacb-444d-4992-8b7b-0c349be598db","status":"selected","externalId":null,"resume":null,"createdAt":"2021-01-28T19:39:02.435Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":"2021-01-28T19:40:49.349Z","updatedBy":"00000000-0000-0000-0000-000000000000"},{"id":"c26c38e2-a47d-405b-abc6-fe62a739561c","jobId":"2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0","userId":"6d0509c7-5f12-4d84-9a19-8e80ef7ddd66","status":"selected","externalId":null,"resume":null,"createdAt":"2021-01-28T19:39:08.233Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":"2021-01-28T19:40:53.659Z","updatedBy":"00000000-0000-0000-0000-000000000000"},{"id":"7bef2b37-e1ee-4638-bfc1-c911787ac955","jobId":"2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0","userId":"f65e2104-2987-4136-839d-ee4632f0b2e5","status":"selected","externalId":null,"resume":null,"createdAt":"2021-01-28T19:39:13.469Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":"2021-01-28T19:40:57.999Z","updatedBy":"00000000-0000-0000-0000-000000000000"},{"id":"e9716139-1f40-4bf1-9f8a-77ae4bcc621e","jobId":"2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0","userId":"e5e667ad-0950-43c2-8d1d-6e83ad7d1c7e","status":"selected","externalId":null,"resume":null,"createdAt":"2021-01-28T19:39:19.215Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":"2021-01-28T19:41:01.953Z","updatedBy":"00000000-0000-0000-0000-000000000000"},{"id":"a1731d01-eac9-4eff-8e5a-8a3c99bc66e0","jobId":"2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0","userId":"bef43122-426b-4b2b-acdd-9b5b3bd1c0bf","status":"selected","externalId":null,"resume":null,"createdAt":"2021-01-28T19:39:24.625Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":"2021-01-28T19:41:06.370Z","updatedBy":"00000000-0000-0000-0000-000000000000"}],"ResourceBooking":[{"id":"08f5e4b9-1088-496d-91a7-5b22a3583e3c","projectId":111,"userId":"213d2dd9-1fc3-4eda-ad97-2d56e2a84a1e","jobId":"2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0","status":"assigned","startDate":"2021-01-25T19:39:28.000Z","endDate":"2021-01-31T19:39:28.000Z","memberRate":1000,"customerRate":1200,"rateType":"weekly","createdAt":"2021-01-28T19:39:30.052Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":"2021-01-28T19:40:25.260Z","updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c"},{"id":"7d967fed-9792-4768-98a7-0b644aa84f2e","projectId":111,"userId":"05e988b7-7d54-4c10-ada1-1a04870a88a8","jobId":"2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0","status":"in-review","startDate":"2021-01-25T19:39:34.000Z","endDate":"2021-01-31T19:39:34.000Z","memberRate":1000,"customerRate":1200,"rateType":"weekly","createdAt":"2021-01-28T19:39:35.571Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":"2021-01-28T19:40:30.291Z","updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c"},{"id":"35e1abd8-1890-4664-bb52-aade382d7b66","projectId":111,"userId":"a2ffdeed-704d-4cf7-b70a-93fcf61de598","jobId":"2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0","status":"closed","startDate":"2021-01-25T19:39:39.000Z","endDate":"2021-01-31T19:39:39.000Z","memberRate":1000,"customerRate":1200,"rateType":"weekly","createdAt":"2021-01-28T19:39:41.205Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":"2021-01-28T19:40:34.859Z","updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c"},{"id":"a098e8d8-ce5b-47d9-afee-38b050d16745","projectId":111,"userId":"4709473d-f060-4102-87f8-4d51ff0b34c1","jobId":"2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0","status":"cancelled","startDate":"2021-01-25T19:39:45.000Z","endDate":"2021-01-31T19:39:45.000Z","memberRate":1000,"customerRate":1200,"rateType":"weekly","createdAt":"2021-01-28T19:39:46.515Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":"2021-01-28T19:40:38.820Z","updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c"},{"id":"d38a6223-3f91-4300-9ecb-6e5fee173625","projectId":111,"userId":"39c7376e-2d5c-4601-bc47-6b60f505814d","jobId":"2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0","status":"sourcing","startDate":"2021-01-25T19:39:50.000Z","endDate":"2021-01-31T19:39:50.000Z","memberRate":1000,"customerRate":1200,"rateType":"weekly","createdAt":"2021-01-28T19:39:52.063Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":"2021-01-28T19:40:43.021Z","updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c"},{"id":"51b45f5d-5df2-46d5-9c3d-8a1323df38dd","projectId":111,"userId":"dd5adacb-444d-4992-8b7b-0c349be598db","jobId":"2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0","status":"assigned","startDate":"2021-01-25T19:39:58.000Z","endDate":"2021-01-31T19:39:58.000Z","memberRate":800,"customerRate":1000,"rateType":"weekly","createdAt":"2021-01-28T19:39:59.432Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":"2021-01-28T19:40:47.743Z","updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c"},{"id":"0a6799d7-f5d1-456b-8bf1-90619284b295","projectId":111,"userId":"6d0509c7-5f12-4d84-9a19-8e80ef7ddd66","jobId":"2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0","status":"assigned","startDate":"2021-01-25T19:40:03.000Z","endDate":"2021-01-31T19:40:03.000Z","memberRate":2000,"customerRate":2500,"rateType":"weekly","createdAt":"2021-01-28T19:40:04.761Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":"2021-01-28T19:40:52.303Z","updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c"},{"id":"61f5d474-e41f-490b-ab58-9f983e3d4916","projectId":111,"userId":"f65e2104-2987-4136-839d-ee4632f0b2e5","jobId":"2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0","status":"assigned","startDate":"2000-07-27T04:17:23.131Z","endDate":"2000-09-27T04:17:23.131Z","memberRate":3000,"customerRate":3500,"rateType":"weekly","createdAt":"2021-01-28T19:40:09.879Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":"2021-01-28T19:40:56.381Z","updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c"},{"id":"dc4477ec-07f8-4c8e-a8fe-ffe38dd290fa","projectId":111,"userId":"e5e667ad-0950-43c2-8d1d-6e83ad7d1c7e","jobId":"2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0","status":"assigned","startDate":"2022-07-27T04:17:23.131Z","endDate":"2022-09-27T04:17:23.131Z","memberRate":1700,"customerRate":1900,"rateType":"weekly","createdAt":"2021-01-28T19:40:15.326Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":"2021-01-28T19:41:00.503Z","updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c"},{"id":"8173579e-4b3c-418d-a9a1-c999caa38404","projectId":111,"userId":"bef43122-426b-4b2b-acdd-9b5b3bd1c0bf","jobId":"2d5e2a52-e0dd-4cd9-8f4c-7cffa43951d0","status":"assigned","startDate":"2020-09-27T04:17:23.131Z","endDate":"2020-09-27T04:17:23.131Z","memberRate":0,"customerRate":0,"rateType":"weekly","createdAt":"2021-01-28T19:40:20.627Z","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":"2021-01-28T19:41:04.919Z","updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c"}]} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 26e22f89..52f6a14f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "topcoder-bookings-api", + "name": "taas-apis", "version": "1.0.0", "lockfileVersion": 1, "requires": true, @@ -778,12 +778,116 @@ } } }, + "ansi-bgblack": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-bgblack/-/ansi-bgblack-0.1.1.tgz", + "integrity": "sha1-poulAHiHcBtqr74/oNrf36juPKI=", + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-bgblue": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-bgblue/-/ansi-bgblue-0.1.1.tgz", + "integrity": "sha1-Z73ATtybm1J4lp2hlt6j11yMNhM=", + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-bgcyan": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-bgcyan/-/ansi-bgcyan-0.1.1.tgz", + "integrity": "sha1-WEiUJWAL3p9VBwaN2Wnr/bUP52g=", + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-bggreen": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-bggreen/-/ansi-bggreen-0.1.1.tgz", + "integrity": "sha1-TjGRJIUplD9DIelr8THRwTgWr0k=", + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-bgmagenta": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-bgmagenta/-/ansi-bgmagenta-0.1.1.tgz", + "integrity": "sha1-myhDLAduqpmUGGcqPvvhk5HCx6E=", + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-bgred": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-bgred/-/ansi-bgred-0.1.1.tgz", + "integrity": "sha1-p2+Sg4OCukMpCmwXeEJPmE1vEEE=", + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-bgwhite": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-bgwhite/-/ansi-bgwhite-0.1.1.tgz", + "integrity": "sha1-ZQRlE3elim7OzQMxmU5IAljhG6g=", + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-bgyellow": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-bgyellow/-/ansi-bgyellow-0.1.1.tgz", + "integrity": "sha1-w/4usIzUdmSAKeaHTRWgs49h1E8=", + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-black": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-black/-/ansi-black-0.1.1.tgz", + "integrity": "sha1-9hheiJNgslRaHsUMC/Bj/EMDJFM=", + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-blue": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-blue/-/ansi-blue-0.1.1.tgz", + "integrity": "sha1-FbgEmQ6S/JyoxUds6PaZd3wh7b8=", + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-bold": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-bold/-/ansi-bold-0.1.1.tgz", + "integrity": "sha1-PmOVCvWswq4uZw5vZ96xFdGl9QU=", + "requires": { + "ansi-wrap": "0.1.0" + } + }, "ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", "dev": true }, + "ansi-cyan": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz", + "integrity": "sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM=", + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-dim": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-dim/-/ansi-dim-0.1.1.tgz", + "integrity": "sha1-QN5MYDqoCG2Oeoa4/5mNXDbu/Ww=", + "requires": { + "ansi-wrap": "0.1.0" + } + }, "ansi-escapes": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", @@ -799,11 +903,91 @@ } } }, + "ansi-gray": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", + "integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=", + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-green": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-green/-/ansi-green-0.1.1.tgz", + "integrity": "sha1-il2al55FjVfEDjNYCzc5C44Q0Pc=", + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-grey": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-grey/-/ansi-grey-0.1.1.tgz", + "integrity": "sha1-WdmLasK6GfilF5jphT+6eDOaM8E=", + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-hidden": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-hidden/-/ansi-hidden-0.1.1.tgz", + "integrity": "sha1-7WpMSY0rt8uyidvyqNHcyFZ/rg8=", + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-inverse": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-inverse/-/ansi-inverse-0.1.1.tgz", + "integrity": "sha1-tq9Fgm/oJr+1KKbHmIV5Q1XM0mk=", + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-italic": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-italic/-/ansi-italic-0.1.1.tgz", + "integrity": "sha1-EEdDRj9iXBQqA2c5z4XtpoiYbyM=", + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-magenta": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-magenta/-/ansi-magenta-0.1.1.tgz", + "integrity": "sha1-BjtboW+z8j4c/aKwfAqJ3hHkMK4=", + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-red": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", + "integrity": "sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw=", + "requires": { + "ansi-wrap": "0.1.0" + } + }, "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" }, + "ansi-reset": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-reset/-/ansi-reset-0.1.1.tgz", + "integrity": "sha1-5+cSksPH3c1NYu9KbHwFmAkRw7c=", + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-strikethrough": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-strikethrough/-/ansi-strikethrough-0.1.1.tgz", + "integrity": "sha1-2Eh3FAss/wfRyT685pkE9oiF5Wg=", + "requires": { + "ansi-wrap": "0.1.0" + } + }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -812,6 +996,35 @@ "color-convert": "^1.9.0" } }, + "ansi-underline": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-underline/-/ansi-underline-0.1.1.tgz", + "integrity": "sha1-38kg9Ml7WXfqFi34/7mIMIqqcaQ=", + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-white": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-white/-/ansi-white-0.1.1.tgz", + "integrity": "sha1-nHe3wZPF7pkuYBHTbsTJIbRXiUQ=", + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-wrap": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", + "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=" + }, + "ansi-yellow": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-yellow/-/ansi-yellow-0.1.1.tgz", + "integrity": "sha1-y5NW8vRscy8OMZnmEClVp32oPB0=", + "requires": { + "ansi-wrap": "0.1.0" + } + }, "any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -850,6 +1063,29 @@ "sprintf-js": "~1.0.2" } }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + }, + "arr-swap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arr-swap/-/arr-swap-1.0.1.tgz", + "integrity": "sha1-FHWQ7WX8gVvAf+8Jl8Llgj1kNTQ=", + "requires": { + "is-number": "^3.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + } + } + } + }, "array-filter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-1.0.0.tgz", @@ -1311,6 +1547,16 @@ "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", "dev": true }, + "choices-separator": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/choices-separator/-/choices-separator-2.0.0.tgz", + "integrity": "sha1-kv0XYxgteQM/XFxR0Lo1LlVnxpY=", + "requires": { + "ansi-dim": "^0.1.1", + "debug": "^2.6.6", + "strip-color": "^0.1.0" + } + }, "chokidar": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz", @@ -1416,6 +1662,24 @@ } } }, + "clone-deep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-1.0.0.tgz", + "integrity": "sha512-hmJRX8x1QOJVV+GUjOBzi6iauhPqc9hIF6xitWRBbiPZOBb6vGo/mDRIK9P74RTKSQK7AE8B0DDWY/vpRrPmQw==", + "requires": { + "for-own": "^1.0.0", + "is-plain-object": "^2.0.4", + "kind-of": "^5.0.0", + "shallow-clone": "^1.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, "clone-response": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", @@ -1440,6 +1704,15 @@ } } }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, "color": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", @@ -1590,6 +1863,11 @@ "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==" }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" + }, "core-js": { "version": "2.6.11", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", @@ -1751,6 +2029,14 @@ "object-keys": "^1.0.12" } }, + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, "deglob": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/deglob/-/deglob-4.0.1.tgz", @@ -1920,6 +2206,11 @@ "is-arrayish": "^0.2.1" } }, + "error-symbol": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/error-symbol/-/error-symbol-0.1.0.tgz", + "integrity": "sha1-Ck2uN9YA0VopukU9jvkg8YRDM/Y=" + }, "es-abstract": { "version": "1.17.7", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", @@ -2425,6 +2716,14 @@ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, "external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -2637,6 +2936,19 @@ } } }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + }, + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "requires": { + "for-in": "^1.0.1" + } + }, "foreach": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", @@ -3127,6 +3439,11 @@ "wrappy": "1" } }, + "info-symbol": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/info-symbol/-/info-symbol-0.1.0.tgz", + "integrity": "sha1-J4QdcoZ920JCzWEtecEGM4gcang=" + }, "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", @@ -3217,6 +3534,21 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + }, + "dependencies": { + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" + } + } + }, "is-arguments": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", @@ -3257,11 +3589,48 @@ "ci-info": "^2.0.0" } }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + }, + "dependencies": { + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" + } + } + }, "is-date-object": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==" }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" + } + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3336,6 +3705,14 @@ "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", "dev": true }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "requires": { + "isobject": "^3.0.1" + } + }, "is-promise": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", @@ -3394,8 +3771,7 @@ "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" }, "is-yarn-global": { "version": "0.3.0", @@ -3421,6 +3797,11 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -3864,6 +4245,26 @@ "json-buffer": "3.0.0" } }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + }, + "dependencies": { + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + } + } + }, + "koalas": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/koalas/-/koalas-1.0.2.tgz", + "integrity": "sha1-MYQz8HQjXbePrlZhoCqMpT7ilc0=" + }, "kuler": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", @@ -3878,6 +4279,14 @@ "package-json": "^6.3.0" } }, + "lazy-cache": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz", + "integrity": "sha1-uRkKT5EzVGlIQIWfio9whNiCImQ=", + "requires": { + "set-getter": "^0.1.0" + } + }, "le_node": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/le_node/-/le_node-1.8.0.tgz", @@ -3996,6 +4405,15 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" }, + "log-ok": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/log-ok/-/log-ok-0.1.1.tgz", + "integrity": "sha1-vqPdNqzQuKckDXhza1uXxlREozQ=", + "requires": { + "ansi-green": "^0.1.1", + "success-symbol": "^0.1.0" + } + }, "log-symbols": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", @@ -4057,6 +4475,56 @@ } } }, + "log-utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/log-utils/-/log-utils-0.2.1.tgz", + "integrity": "sha1-pMIXoN2aUFFdm5ICBgkas9TgMc8=", + "requires": { + "ansi-colors": "^0.2.0", + "error-symbol": "^0.1.0", + "info-symbol": "^0.1.0", + "log-ok": "^0.1.1", + "success-symbol": "^0.1.0", + "time-stamp": "^1.0.1", + "warning-symbol": "^0.1.0" + }, + "dependencies": { + "ansi-colors": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-0.2.0.tgz", + "integrity": "sha1-csMd4qDZoszQysMMyYI+6y9kNLU=", + "requires": { + "ansi-bgblack": "^0.1.1", + "ansi-bgblue": "^0.1.1", + "ansi-bgcyan": "^0.1.1", + "ansi-bggreen": "^0.1.1", + "ansi-bgmagenta": "^0.1.1", + "ansi-bgred": "^0.1.1", + "ansi-bgwhite": "^0.1.1", + "ansi-bgyellow": "^0.1.1", + "ansi-black": "^0.1.1", + "ansi-blue": "^0.1.1", + "ansi-bold": "^0.1.1", + "ansi-cyan": "^0.1.1", + "ansi-dim": "^0.1.1", + "ansi-gray": "^0.1.1", + "ansi-green": "^0.1.1", + "ansi-grey": "^0.1.1", + "ansi-hidden": "^0.1.1", + "ansi-inverse": "^0.1.1", + "ansi-italic": "^0.1.1", + "ansi-magenta": "^0.1.1", + "ansi-red": "^0.1.1", + "ansi-reset": "^0.1.1", + "ansi-strikethrough": "^0.1.1", + "ansi-underline": "^0.1.1", + "ansi-white": "^0.1.1", + "ansi-yellow": "^0.1.1", + "lazy-cache": "^2.0.1" + } + } + } + }, "logform": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/logform/-/logform-2.2.0.tgz", @@ -4135,6 +4603,14 @@ } } }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "requires": { + "object-visit": "^1.0.0" + } + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -4213,6 +4689,22 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, + "mixin-object": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", + "integrity": "sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=", + "requires": { + "for-in": "^0.1.3", + "is-extendable": "^0.1.1" + }, + "dependencies": { + "for-in": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz", + "integrity": "sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=" + } + } + }, "mkdirp": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", @@ -4776,6 +5268,59 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + } + } + }, "object-inspect": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", @@ -4786,6 +5331,14 @@ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "requires": { + "isobject": "^3.0.0" + } + }, "object.assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz", @@ -5232,6 +5785,11 @@ "find-up": "^2.1.0" } }, + "pointer-symbol": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pointer-symbol/-/pointer-symbol-1.0.0.tgz", + "integrity": "sha1-YPkRAgTqepKbYmRKITFVQ8uz1Ec=" + }, "postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -5303,6 +5861,150 @@ "iterate-value": "^1.0.0" } }, + "prompt-actions": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/prompt-actions/-/prompt-actions-3.0.2.tgz", + "integrity": "sha512-dhz2Fl7vK+LPpmnQ/S/eSut4BnH4NZDLyddHKi5uTU/2PDn3grEMGkgsll16V5RpVUh/yxdiam0xsM0RD4xvtg==", + "requires": { + "debug": "^2.6.8" + } + }, + "prompt-base": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/prompt-base/-/prompt-base-4.1.0.tgz", + "integrity": "sha512-svGzgLUKZoqomz9SGMkf1hBG8Wl3K7JGuRCXc/Pv7xw8239hhaTBXrmjt7EXA9P/QZzdyT8uNWt9F/iJTXq75g==", + "requires": { + "component-emitter": "^1.2.1", + "debug": "^3.0.1", + "koalas": "^1.0.2", + "log-utils": "^0.2.1", + "prompt-actions": "^3.0.2", + "prompt-question": "^5.0.1", + "readline-ui": "^2.2.3", + "readline-utils": "^2.2.3", + "static-extend": "^0.1.2" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "prompt-choices": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/prompt-choices/-/prompt-choices-4.1.0.tgz", + "integrity": "sha512-ZNYLv6rW9z9n0WdwCkEuS+w5nUAGzRgtRt6GQ5aFNFz6MIcU7nHFlHOwZtzy7RQBk80KzUGPSRQphvMiQzB8pg==", + "requires": { + "arr-flatten": "^1.1.0", + "arr-swap": "^1.0.1", + "choices-separator": "^2.0.0", + "clone-deep": "^4.0.0", + "collection-visit": "^1.0.0", + "define-property": "^2.0.2", + "is-number": "^6.0.0", + "kind-of": "^6.0.2", + "koalas": "^1.0.2", + "log-utils": "^0.2.1", + "pointer-symbol": "^1.0.0", + "radio-symbol": "^2.0.0", + "set-value": "^3.0.0", + "strip-color": "^0.1.0", + "terminal-paginator": "^2.0.2", + "toggle-array": "^1.0.1" + }, + "dependencies": { + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + } + }, + "is-number": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-6.0.0.tgz", + "integrity": "sha512-Wu1VHeILBK8KAWJUAiSZQX94GmOE45Rg6/538fKwiloUu21KncEkYGPqob2oSZ5mUT73vLGrHQjKw3KMPwfDzg==" + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" + }, + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "requires": { + "kind-of": "^6.0.2" + } + } + } + }, + "prompt-confirm": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/prompt-confirm/-/prompt-confirm-2.0.4.tgz", + "integrity": "sha512-X5lzbC8/kMNHdPOqQPfMKpH4VV2f7v2OTRJoN69ZYBirSwTeQaf9ZhmzPEO9ybMA0YV2Pha5MV27u2/U4ahWfg==", + "requires": { + "ansi-cyan": "^0.1.1", + "prompt-base": "^4.0.1" + } + }, + "prompt-question": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/prompt-question/-/prompt-question-5.0.2.tgz", + "integrity": "sha512-wreaLbbu8f5+7zXds199uiT11Ojp59Z4iBi6hONlSLtsKGTvL2UY8VglcxQ3t/X4qWIxsNCg6aT4O8keO65v6Q==", + "requires": { + "clone-deep": "^1.0.0", + "debug": "^3.0.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "kind-of": "^5.0.2", + "koalas": "^1.0.2", + "prompt-choices": "^4.0.5" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, "prop-types": { "version": "15.7.2", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", @@ -5408,6 +6110,16 @@ } } }, + "radio-symbol": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/radio-symbol/-/radio-symbol-2.0.0.tgz", + "integrity": "sha1-eqm/xQSFY21S3XbWqOYxspB5muE=", + "requires": { + "ansi-gray": "^0.1.1", + "ansi-green": "^0.1.1", + "is-windows": "^1.0.1" + } + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -5499,6 +6211,82 @@ "picomatch": "^2.2.1" } }, + "readline-ui": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/readline-ui/-/readline-ui-2.2.3.tgz", + "integrity": "sha512-ix7jz0PxqQqcIuq3yQTHv1TOhlD2IHO74aNO+lSuXsRYm1d+pdyup1yF3zKyLK1wWZrVNGjkzw5tUegO2IDy+A==", + "requires": { + "component-emitter": "^1.2.1", + "debug": "^2.6.8", + "readline-utils": "^2.2.1", + "string-width": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "readline-utils": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/readline-utils/-/readline-utils-2.2.3.tgz", + "integrity": "sha1-b4R9a48ZFcORtYHDZ81HhzhiNRo=", + "requires": { + "arr-flatten": "^1.1.0", + "extend-shallow": "^2.0.1", + "is-buffer": "^1.1.5", + "is-number": "^3.0.0", + "is-windows": "^1.0.1", + "koalas": "^1.0.2", + "mute-stream": "0.0.7", + "strip-color": "^0.1.0", + "window-size": "^1.1.0" + }, + "dependencies": { + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + } + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" + } + } + }, "reconnect-core": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/reconnect-core/-/reconnect-core-1.3.0.tgz", @@ -5831,11 +6619,44 @@ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true }, + "set-getter": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/set-getter/-/set-getter-0.1.0.tgz", + "integrity": "sha1-12nBgsnVpR9AkUXy+6guXoboA3Y=", + "requires": { + "to-object-path": "^0.3.0" + } + }, + "set-value": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-3.0.2.tgz", + "integrity": "sha512-npjkVoz+ank0zjlV9F47Fdbjfj/PfXyVhZvGALWsyIYU/qrMzpi6avjKW3/7KeSU2Df3I46BrN1xOI1+6vW0hA==", + "requires": { + "is-plain-object": "^2.0.4" + } + }, "setprototypeof": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" }, + "shallow-clone": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-1.0.0.tgz", + "integrity": "sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA==", + "requires": { + "is-extendable": "^0.1.1", + "kind-of": "^5.0.0", + "mixin-object": "^2.0.1" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -6059,6 +6880,81 @@ "pkg-conf": "^3.1.0" } }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, "statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -6143,11 +7039,21 @@ "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", "dev": true }, + "strip-color": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/strip-color/-/strip-color-0.1.0.tgz", + "integrity": "sha1-EG9l09PmotlAHKwOsM6LinArT3s=" + }, "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" }, + "success-symbol": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/success-symbol/-/success-symbol-0.1.0.tgz", + "integrity": "sha1-JAIuSG878c3KCUKDt2nEctO3KJc=" + }, "superagent": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/superagent/-/superagent-6.1.0.tgz", @@ -6272,6 +7178,16 @@ "integrity": "sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw==", "dev": true }, + "terminal-paginator": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/terminal-paginator/-/terminal-paginator-2.0.2.tgz", + "integrity": "sha512-IZMT5ECF9p4s+sNCV8uvZSW9E1+9zy9Ji9xz2oee8Jfo7hUFpauyjxkhfRcIH6Lu3Wdepv5D1kVRc8Hx74/LfQ==", + "requires": { + "debug": "^2.6.6", + "extend-shallow": "^2.0.1", + "log-utils": "^0.2.1" + } + }, "test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -6298,6 +7214,11 @@ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, + "time-stamp": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", + "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=" + }, "timers-ext": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz", @@ -6322,6 +7243,14 @@ "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "requires": { + "kind-of": "^3.0.2" + } + }, "to-readable-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", @@ -6337,6 +7266,14 @@ "is-number": "^7.0.0" } }, + "toggle-array": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toggle-array/-/toggle-array-1.0.1.tgz", + "integrity": "sha1-y/WEB5K9UJfzMReugkyTKv/ofVg=", + "requires": { + "isobject": "^3.0.0" + } + }, "toidentifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", @@ -6674,6 +7611,11 @@ "extsprintf": "^1.2.0" } }, + "warning-symbol": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/warning-symbol/-/warning-symbol-0.1.0.tgz", + "integrity": "sha1-uzHdEbeg+dZ6su2V9Fe2WCW7rSE=" + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -6757,6 +7699,25 @@ "resolved": "https://registry.npmjs.org/winchan/-/winchan-0.2.2.tgz", "integrity": "sha512-pvN+IFAbRP74n/6mc6phNyCH8oVkzXsto4KCHPJ2AScniAnA1AmeLI03I2BzjePpaClGSI4GUMowzsD3qz5PRQ==" }, + "window-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-1.1.1.tgz", + "integrity": "sha512-5D/9vujkmVQ7pSmc0SCBmHXbkv6eaHwXEx65MywhmUMsI8sGqJ972APq1lotfcwMKPFLuCFfL8xGHLIp7jaBmA==", + "requires": { + "define-property": "^1.0.0", + "is-number": "^3.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + } + } + } + }, "winston": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz", diff --git a/package.json b/package.json index 65029201..28fdc09d 100644 --- a/package.json +++ b/package.json @@ -9,11 +9,16 @@ "lint": "standard", "lint:fix": "standard --fix", "init-db": "node src/init-db.js", - "create-index": "node scripts/createIndex.js", - "delete-index": "node scripts/deleteIndex.js", + "create-index": "node scripts/es/createIndex.js", + "delete-index": "node scripts/es/deleteIndex.js", + "index:all": "node scripts/es/reIndexAll.js", + "index:jobs": "node scripts/es/reIndexJobs.js", + "index:job-candidates": "node scripts/es/reIndexJobCandidates.js", + "index:resource-bookings": "node scripts/es/reIndexResourceBookings.js", + "data:export": "node scripts/data/exportData.js", + "data:import": "node scripts/data/importData.js", "migrate": "npx sequelize db:migrate", "migrate:undo": "npx sequelize db:migrate:undo", - "test-data": "node scripts/insert-es-data.js", "test": "mocha test/unit/*.test.js --timeout 30000 --exit", "services:up": "docker-compose -f ./local/docker-compose.yml up -d", "services:down": "docker-compose -f ./local/docker-compose.yml down", @@ -41,6 +46,7 @@ "lodash": "^4.17.20", "pg": "^8.4.0", "pg-hstore": "^2.3.3", + "prompt-confirm": "^2.0.4", "rewire": "^5.0.0", "sequelize": "^6.3.5", "superagent": "^6.1.0", @@ -61,6 +67,10 @@ "standard": "^14.3.4" }, "standard": { + "ignore": [ + "/docs", + "/migrations" + ], "env": [ "mocha" ] diff --git a/scripts/createIndex.js b/scripts/createIndex.js deleted file mode 100644 index 4bba1957..00000000 --- a/scripts/createIndex.js +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Create index in Elasticsearch - */ - -const config = require('config') -const logger = require('../src/common/logger') -const helper = require('../src/common/helper') - -async function createIndex () { - const esClient = helper.getESClient() - - const indices = [ - { - index: config.get('esConfig.ES_INDEX_JOB'), - body: { - mappings: { - properties: { - projectId: { type: 'integer' }, - externalId: { type: 'keyword' }, - description: { type: 'text' }, - title: { type: 'text' }, - startDate: { type: 'date' }, - endDate: { type: 'date' }, - numPositions: { type: 'integer' }, - resourceType: { type: 'keyword' }, - rateType: { type: 'keyword' }, - workload: { type: 'keyword' }, - skills: { type: 'keyword' }, - status: { type: 'keyword' }, - createdAt: { type: 'date' }, - createdBy: { type: 'keyword' }, - updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' } - } - } - } - }, - { - index: config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), - body: { - mappings: { - properties: { - jobId: { type: 'keyword' }, - userId: { type: 'keyword' }, - status: { type: 'keyword' }, - externalId: { type: 'keyword' }, - resume: { type: 'text' }, - createdAt: { type: 'date' }, - createdBy: { type: 'keyword' }, - updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' } - } - } - } - }, - { - index: config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), - body: { - mappings: { - properties: { - projectId: { type: 'integer' }, - userId: { type: 'keyword' }, - jobId: { type: 'keyword' }, - status: { type: 'keyword' }, - startDate: { type: 'date' }, - endDate: { type: 'date' }, - memberRate: { type: 'float' }, - customerRate: { type: 'float' }, - rateType: { type: 'keyword' }, - createdAt: { type: 'date' }, - createdBy: { type: 'keyword' }, - updatedAt: { type: 'date' }, - updatedBy: { type: 'keyword' } - } - } - } - }] - - for (const index of indices) { - await esClient.indices.create(index) - logger.info({ component: 'createIndex', message: `ES Index ${index.index} creation succeeded!` }) - } - process.exit(0) -} - -createIndex().catch((err) => { - logger.logFullError(err, { component: 'createIndex' }) - process.exit(1) -}) diff --git a/scripts/data/exportData.js b/scripts/data/exportData.js new file mode 100644 index 00000000..c443cb38 --- /dev/null +++ b/scripts/data/exportData.js @@ -0,0 +1,24 @@ +/** + * Export data to a json file + */ +const config = require('config') +const logger = require('../../src/common/logger') +const helper = require('../../src/common/helper') + +const filePath = helper.getParamFromCliArgs() || config.DEFAULT_DATA_FILE_PATH +const userPrompt = `WARNING: are you sure you want to export all data in the database to a json file with the path ${filePath}? This will overwrite the file.` +const dataModels = ['Job', 'JobCandidate', 'ResourceBooking'] + +async function exportData () { + await helper.promptUser(userPrompt, async () => { + try { + await helper.exportData(filePath, dataModels, logger) + process.exit(0) + } catch (err) { + logger.logFullError(err, { component: 'exportData' }) + process.exit(1) + } + }) +} + +exportData() diff --git a/scripts/data/importData.js b/scripts/data/importData.js new file mode 100644 index 00000000..1f51e61c --- /dev/null +++ b/scripts/data/importData.js @@ -0,0 +1,24 @@ +/** + * Import data from a json file into the db and index it in Elasticsearch + */ +const config = require('config') +const logger = require('../../src/common/logger') +const helper = require('../../src/common/helper') + +const filePath = helper.getParamFromCliArgs() || config.DEFAULT_DATA_FILE_PATH +const userPrompt = `WARNING: this would remove existing data. Are you sure you want to import data from a json file with the path ${filePath}?` +const dataModels = ['Job', 'JobCandidate', 'ResourceBooking'] + +async function importData () { + await helper.promptUser(userPrompt, async () => { + try { + await helper.importData(filePath, dataModels, logger) + process.exit(0) + } catch (err) { + logger.logFullError(err, { component: 'importData' }) + process.exit(1) + } + }) +} + +importData() diff --git a/scripts/deleteIndex.js b/scripts/deleteIndex.js deleted file mode 100644 index dd7ca7e6..00000000 --- a/scripts/deleteIndex.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Delete index in Elasticsearch - */ - -const config = require('config') -const logger = require('../src/common/logger') -const helper = require('../src/common/helper') - -async function deleteIndex () { - 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'), - config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] - for (const index of indices) { - await esClient.indices.delete({ - index - }) - logger.info({ component: 'deleteIndex', message: `ES Index ${index} deletion succeeded!` }) - } - process.exit(0) -} -deleteIndex().catch((err) => { - logger.logFullError(err, { component: 'deleteIndex' }) - process.exit(1) -}) diff --git a/scripts/es/createIndex.js b/scripts/es/createIndex.js new file mode 100644 index 00000000..d2c72943 --- /dev/null +++ b/scripts/es/createIndex.js @@ -0,0 +1,29 @@ +/** + * Create index in Elasticsearch + */ +const config = require('config') +const logger = require('../../src/common/logger') +const helper = require('../../src/common/helper') + +const indices = [ + config.get('esConfig.ES_INDEX_JOB'), + config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), + config.get('esConfig.ES_INDEX_RESOURCE_BOOKING') +] +const userPrompt = `WARNING: Are you sure want to create the following elasticsearch indices: ${indices}?` + +async function createIndex () { + await helper.promptUser(userPrompt, async () => { + for (const index of indices) { + try { + await helper.createIndex(index, logger) + } catch (err) { + logger.logFullError(err, { component: 'createIndex' }) + process.exit(1) + } + } + process.exit(0) + }) +} + +createIndex() diff --git a/scripts/es/deleteIndex.js b/scripts/es/deleteIndex.js new file mode 100644 index 00000000..6e30995a --- /dev/null +++ b/scripts/es/deleteIndex.js @@ -0,0 +1,29 @@ +/** + * Delete index in Elasticsearch + */ +const config = require('config') +const logger = require('../../src/common/logger') +const helper = require('../../src/common/helper') + +const indices = [ + config.get('esConfig.ES_INDEX_JOB'), + config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), + config.get('esConfig.ES_INDEX_RESOURCE_BOOKING') +] +const userPrompt = `WARNING: this would remove existent data! Are you sure want to delete the following eleasticsearch indices: ${indices}?` + +async function deleteIndex () { + await helper.promptUser(userPrompt, async () => { + for (const index of indices) { + try { + await helper.deleteIndex(index, logger) + } catch (err) { + logger.logFullError(err, { component: 'deleteIndex' }) + process.exit(1) + } + } + process.exit(0) + }) +} + +deleteIndex() diff --git a/scripts/es/reIndexAll.js b/scripts/es/reIndexAll.js new file mode 100644 index 00000000..65ddc2ec --- /dev/null +++ b/scripts/es/reIndexAll.js @@ -0,0 +1,24 @@ +/** + * Reindex all data in Elasticsearch using data from database + */ +const config = require('config') +const logger = require('../../src/common/logger') +const helper = require('../../src/common/helper') + +const userPrompt = 'WARNING: this would remove existent data! Are you sure want to reindex all indices?' + +async function indexAll () { + await helper.promptUser(userPrompt, async () => { + try { + await helper.indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger) + await helper.indexBulkDataToES('JobCandidate', config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), logger) + await helper.indexBulkDataToES('ResourceBooking', config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), logger) + process.exit(0) + } catch (err) { + logger.logFullError(err, { component: 'indexAll' }) + process.exit(1) + } + }) +} + +indexAll() diff --git a/scripts/es/reIndexJobCandidates.js b/scripts/es/reIndexJobCandidates.js new file mode 100644 index 00000000..7e0aec22 --- /dev/null +++ b/scripts/es/reIndexJobCandidates.js @@ -0,0 +1,37 @@ +/** + * Reindex JobCandidates data in Elasticsearch using data from database + */ +const config = require('config') +const logger = require('../../src/common/logger') +const helper = require('../../src/common/helper') + +const jobCandidateId = helper.getParamFromCliArgs() +const index = config.get('esConfig.ES_INDEX_JOB_CANDIDATE') +const reIndexAllJobCandidatesPrompt = `WARNING: this would remove existent data! Are you sure you want to reindex the index ${index}?` +const reIndexJobCandidatePrompt = `WARNING: this would remove existent data! Are you sure you want to reindex the document with id ${jobCandidateId} in index ${index}?` + +async function reIndexJobCandidates () { + if (jobCandidateId === null) { + await helper.promptUser(reIndexAllJobCandidatesPrompt, async () => { + try { + await helper.indexBulkDataToES('JobCandidate', index, logger) + process.exit(0) + } catch (err) { + logger.logFullError(err, { component: 'reIndexJobCandidates' }) + process.exit(1) + } + }) + } else { + await helper.promptUser(reIndexJobCandidatePrompt, async () => { + try { + await helper.indexDataToEsById(jobCandidateId, 'JobCandidate', index, logger) + process.exit(0) + } catch (err) { + logger.logFullError(err, { component: 'reIndexJobCandidates' }) + process.exit(1) + } + }) + } +} + +reIndexJobCandidates() diff --git a/scripts/es/reIndexJobs.js b/scripts/es/reIndexJobs.js new file mode 100644 index 00000000..5d44ba70 --- /dev/null +++ b/scripts/es/reIndexJobs.js @@ -0,0 +1,37 @@ +/** + * Reindex Jobs data in Elasticsearch using data from database + */ +const config = require('config') +const logger = require('../../src/common/logger') +const helper = require('../../src/common/helper') + +const jobId = helper.getParamFromCliArgs() +const index = config.get('esConfig.ES_INDEX_JOB') +const reIndexAllJobsPrompt = `WARNING: this would remove existent data! Are you sure you want to reindex the index ${index}?` +const reIndexJobPrompt = `WARNING: this would remove existent data! Are you sure you want to reindex the document with id ${jobId} in index ${index}?` + +async function reIndexJobs () { + if (jobId === null) { + await helper.promptUser(reIndexAllJobsPrompt, async () => { + try { + await helper.indexBulkDataToES('Job', index, logger) + process.exit(0) + } catch (err) { + logger.logFullError(err, { component: 'reIndexJobs' }) + process.exit(1) + } + }) + } else { + await helper.promptUser(reIndexJobPrompt, async () => { + try { + await helper.indexDataToEsById(jobId, 'Job', index, logger) + process.exit(0) + } catch (err) { + logger.logFullError(err, { component: 'reIndexJobs' }) + process.exit(1) + } + }) + } +} + +reIndexJobs() diff --git a/scripts/es/reIndexResourceBookings.js b/scripts/es/reIndexResourceBookings.js new file mode 100644 index 00000000..ef2bf940 --- /dev/null +++ b/scripts/es/reIndexResourceBookings.js @@ -0,0 +1,37 @@ +/** + * Reindex ResourceBookings data in Elasticsearch using data from database + */ +const config = require('config') +const logger = require('../../src/common/logger') +const helper = require('../../src/common/helper') + +const resourceBookingId = helper.getParamFromCliArgs() +const index = config.get('esConfig.ES_INDEX_RESOURCE_BOOKING') +const reIndexAllResourceBookingsPrompt = `WARNING: this would remove existent data! Are you sure you want to reindex the index ${index}` +const reIndexResourceBookingPrompt = `WARNING: this would remove existent data! Are you sure you want to reindex the document with id ${resourceBookingId} in index ${index}?` + +async function reIndexResourceBookings () { + if (resourceBookingId === null) { + await helper.promptUser(reIndexAllResourceBookingsPrompt, async () => { + try { + await helper.indexBulkDataToES('ResourceBooking', index, logger) + process.exit(0) + } catch (err) { + logger.logFullError(err, { component: 'reIndexResourceBookings' }) + process.exit(1) + } + }) + } else { + await helper.promptUser(reIndexResourceBookingPrompt, async () => { + try { + await helper.indexDataToEsById(resourceBookingId, 'ResourceBooking', index, logger) + process.exit(0) + } catch (err) { + logger.logFullError(err, { component: 'reIndexResourceBookings' }) + process.exit(1) + } + }) + } +} + +reIndexResourceBookings() diff --git a/scripts/feed-data/jobCandidates.json b/scripts/feed-data/jobCandidates.json deleted file mode 100644 index f76c8978..00000000 --- a/scripts/feed-data/jobCandidates.json +++ /dev/null @@ -1,152 +0,0 @@ -{ - "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 deleted file mode 100644 index 4106bf0a..00000000 --- a/scripts/feed-data/jobs.json +++ /dev/null @@ -1,302 +0,0 @@ -{ - "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 deleted file mode 100644 index 5c33b333..00000000 --- a/scripts/feed-data/resourceBookings.json +++ /dev/null @@ -1,195 +0,0 @@ -{ - "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 deleted file mode 100644 index 09ac281b..00000000 --- a/scripts/insert-es-data.js +++ /dev/null @@ -1,92 +0,0 @@ -/** - * 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/common/helper.js b/src/common/helper.js index a6a33d7c..c7673c0f 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -2,7 +2,9 @@ * This file defines helper methods */ +const fs = require('fs') const querystring = require('querystring') +const Confirm = require('prompt-confirm') const AWS = require('aws-sdk') const config = require('config') const HttpStatus = require('http-status-codes') @@ -50,6 +52,377 @@ function getBusApiClient () { // ES Client mapping const esClients = {} +// The es index property mapping +const esIndexPropertyMapping = {} +esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB')] = { + projectId: { type: 'integer' }, + externalId: { type: 'keyword' }, + description: { type: 'text' }, + title: { type: 'text' }, + startDate: { type: 'date' }, + endDate: { type: 'date' }, + numPositions: { type: 'integer' }, + resourceType: { type: 'keyword' }, + rateType: { type: 'keyword' }, + workload: { type: 'keyword' }, + skills: { type: 'keyword' }, + status: { type: 'keyword' }, + createdAt: { type: 'date' }, + createdBy: { type: 'keyword' }, + updatedAt: { type: 'date' }, + updatedBy: { type: 'keyword' } +} +esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB_CANDIDATE')] = { + jobId: { type: 'keyword' }, + userId: { type: 'keyword' }, + status: { type: 'keyword' }, + externalId: { type: 'keyword' }, + resume: { type: 'text' }, + createdAt: { type: 'date' }, + createdBy: { type: 'keyword' }, + updatedAt: { type: 'date' }, + updatedBy: { type: 'keyword' } +} +esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { + projectId: { type: 'integer' }, + userId: { type: 'keyword' }, + jobId: { type: 'keyword' }, + status: { type: 'keyword' }, + startDate: { type: 'date' }, + endDate: { type: 'date' }, + memberRate: { type: 'float' }, + customerRate: { type: 'float' }, + rateType: { type: 'keyword' }, + createdAt: { type: 'date' }, + createdBy: { type: 'keyword' }, + updatedAt: { type: 'date' }, + updatedBy: { type: 'keyword' } +} + +/** + * Get the first parameter from cli arguments + */ +function getParamFromCliArgs () { + const filteredArgs = process.argv.filter(arg => !arg.includes('--')) + + if (filteredArgs.length > 2) { + return filteredArgs[2] + } + + return null +} + +/** + * Prompt the user with a y/n query and call a callback function based on the answer + * @param {string} promptQuery the query to ask the user + * @param {function} cb the callback function + */ +async function promptUser (promptQuery, cb) { + if (process.argv.includes('--force')) { + await cb() + return + } + + const prompt = new Confirm(promptQuery) + prompt.ask(async (answer) => { + if (answer) { + await cb() + } + }) +} + +/** + * Create index in elasticsearch + * @param {Object} index the index name + * @param {Object} logger the logger object + * @param {Object} esClient the elasticsearch client (optional, will create if not given) + */ +async function createIndex (index, logger, esClient = null) { + if (!esClient) { + esClient = getESClient() + } + + await esClient.indices.create({ + index, + body: { + mappings: { + properties: esIndexPropertyMapping[index] + } + } + }) + logger.info({ component: 'createIndex', message: `ES Index ${index} creation succeeded!` }) +} + +/** + * Delete index in elasticsearch + * @param {Object} index the index name + * @param {Object} logger the logger object + * @param {Object} esClient the elasticsearch client (optional, will create if not given) + */ +async function deleteIndex (index, logger, esClient = null) { + if (!esClient) { + esClient = getESClient() + } + + await esClient.indices.delete({ index }) + logger.info({ component: 'deleteIndex', message: `ES Index ${index} deletion succeeded!` }) +} + +/** + * Split data into bulks + * @param {Array} data the array of data to split + */ +function getBulksFromDocuments (data) { + const maxBytes = config.get('esConfig.MAX_BULK_REQUEST_SIZE_MB') * 1e6 + const bulks = [] + let documentIndex = 0 + let currentBulkSize = 0 + let currentBulk = [] + + while (true) { + // break loop when parsed all documents + if (documentIndex >= data.length) { + bulks.push(currentBulk) + break + } + + // check if current document size is greater than the max bulk size, if so, throw error + const currentDocumentSize = Buffer.byteLength(JSON.stringify(data[documentIndex]), 'utf-8') + if (maxBytes < currentDocumentSize) { + throw new Error(`Document with id ${data[documentIndex]} has size ${currentDocumentSize}, which is greater than the max bulk size, ${maxBytes}. Consider increasing the max bulk size.`) + } + + if (currentBulkSize + currentDocumentSize > maxBytes || + currentBulk.length >= config.get('esConfig.MAX_BULK_NUM_DOCUMENTS')) { + // if adding the current document goes over the max bulk size OR goes over max number of docs + // then push the current bulk to bulks array and reset the current bulk + bulks.push(currentBulk) + currentBulk = [] + currentBulkSize = 0 + } else { + // otherwise, add document to current bulk + currentBulk.push(data[documentIndex]) + currentBulkSize += currentDocumentSize + documentIndex++ + } + } + return bulks +} + +/** +* Index records in bulk +* @param {Object} modelName the model name in db +* @param {Object} indexName the index name +* @param {Object} logger the logger object +*/ +async function indexBulkDataToES (modelName, indexName, logger) { + logger.info({ component: 'indexBulkDataToES', message: `Reindexing of ${modelName}s started!` }) + + const esClient = getESClient() + + // clear index + const indexExistsRes = await esClient.indices.exists({ index: indexName }) + if (indexExistsRes.statusCode !== 404) { + await deleteIndex(indexName, logger, esClient) + } + await createIndex(indexName, logger, esClient) + + // get data from db + logger.info({ component: 'indexBulkDataToES', message: 'Getting data from database' }) + const model = models[modelName] + const data = await model.findAll({ raw: true }) + if (_.isEmpty(data)) { + logger.info({ component: 'indexBulkDataToES', message: `No data in database for ${modelName}` }) + return + } + const bulks = getBulksFromDocuments(data) + + const startTime = Date.now() + let doneCount = 0 + for (const bulk of bulks) { + // send bulk to esclient + const body = bulk.flatMap(doc => [{ index: { _index: indexName, _id: doc.id } }, doc]) + await esClient.bulk({ refresh: true, body }) + doneCount += bulk.length + + // log metrics + const timeSpent = Date.now() - startTime + const avgTimePerDocument = timeSpent / doneCount + const estimatedLength = (avgTimePerDocument * data.length) + const timeLeft = (startTime + estimatedLength) - Date.now() + logger.info({ + component: 'indexBulkDataToES', + message: `Processed ${doneCount} of ${data.length} documents, average time per document ${formatTime(avgTimePerDocument)}, time spent: ${formatTime(timeSpent)}, time left: ${formatTime(timeLeft)}` + }) + } +} + +/** + * Index job by id + * @param {Object} modelName the model name in db + * @param {Object} indexName the index name + * @param {string} id the job id + * @param {Object} logger the logger object + */ +async function indexDataToEsById (id, modelName, indexName, logger) { + logger.info({ component: 'indexDataToEsById', message: `Reindexing of ${modelName} with id ${id} started!` }) + const esClient = getESClient() + + logger.info({ component: 'indexDataToEsById', message: 'Getting data from database' }) + const model = models[modelName] + + const data = await model.findById(id) + logger.info({ component: 'indexDataToEsById', message: 'Indexing data into Elasticsearch' }) + await esClient.index({ + index: indexName, + id: id, + body: _.omit(data.dataValues, 'id') + }) + logger.info({ component: 'indexDataToEsById', message: 'Indexing complete!' }) +} + +/** + * Import data from a json file into the database + * @param {string} pathToFile the path to the json file + * @param {Array} dataModels the data models to import + * @param {Object} logger the logger object + */ +async function importData (pathToFile, dataModels, logger) { + // check if file exists + if (!fs.existsSync(pathToFile)) { + throw new Error(`File with path ${pathToFile} does not exist`) + } + + // clear database + logger.info({ component: 'importData', message: 'Clearing database...' }) + await models.sequelize.sync({ force: true }) + + let transaction = null + let currentModelName = null + try { + // Start a transaction + transaction = await models.sequelize.transaction() + const jsonData = JSON.parse(fs.readFileSync(pathToFile).toString()) + + for (let index = 0; index < dataModels.length; index += 1) { + const modelName = dataModels[index] + currentModelName = modelName + const model = models[modelName] + const modelRecords = jsonData[modelName] + + if (modelRecords && modelRecords.length > 0) { + logger.info({ component: 'importData', message: `Importing data for model: ${modelName}` }) + + await model.bulkCreate(modelRecords, { transaction }) + logger.info({ component: 'importData', message: `Records imported for model: ${modelName} = ${modelRecords.length}` }) + } else { + logger.info({ component: 'importData', message: `No records to import for model: ${modelName}` }) + } + } + // commit transaction only if all things went ok + logger.info({ component: 'importData', message: 'committing transaction to database...' }) + await transaction.commit() + } catch (error) { + logger.error({ component: 'importData', message: `Error while writing data of model: ${currentModelName}` }) + // rollback all insert operations + if (transaction) { + logger.info({ component: 'importData', message: 'rollback database transaction...' }) + transaction.rollback() + } + if (error.name && error.errors && error.fields) { + // For sequelize validation errors, we throw only fields with data that helps in debugging error, + // because the error object has many fields that contains very big sql query for the insert bulk operation + throw new Error( + JSON.stringify({ + modelName: currentModelName, + name: error.name, + errors: error.errors, + fields: error.fields + }) + ) + } else { + throw error + } + } + + // after importing, index data + await indexBulkDataToES('Job', config.get('esConfig.ES_INDEX_JOB'), logger) + await indexBulkDataToES('JobCandidate', config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), logger) + await indexBulkDataToES('ResourceBooking', config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), logger) +} + +/** + * Export data from the database into a json file + * @param {string} pathToFile the path to the json file + * @param {Array} dataModels the data models to export + * @param {Object} logger the logger object + */ +async function exportData (pathToFile, dataModels, logger) { + logger.info({ component: 'exportData', message: `Start Saving data to file with path ${pathToFile}....` }) + + const allModelsRecords = {} + for (let index = 0; index < dataModels.length; index += 1) { + const modelName = dataModels[index] + const modelRecords = await models[modelName].findAll({ + raw: true, + where: { + deletedAt: null + }, + attributes: { + exclude: ['deletedAt'] + } + }) + allModelsRecords[modelName] = modelRecords + logger.info({ component: 'exportData', message: `Records loaded for model: ${modelName} = ${modelRecords.length}` }) + } + + fs.writeFileSync(pathToFile, JSON.stringify(allModelsRecords)) + logger.info({ component: 'exportData', message: 'End Saving data to file....' }) +} + +/** + * Format a time in milliseconds into a human readable format + * @param {Date} milliseconds the number of milliseconds + */ +function formatTime (millisec) { + const ms = Math.floor(millisec % 1000) + const secs = Math.floor((millisec / 1000) % 60) + const mins = Math.floor((millisec / (1000 * 60)) % 60) + const hrs = Math.floor((millisec / (1000 * 60 * 60)) % 24) + const days = Math.floor((millisec / (1000 * 60 * 60 * 24)) % 7) + const weeks = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7)) % 4) + const mnths = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7 * 4)) % 12) + const yrs = Math.floor((millisec / (1000 * 60 * 60 * 24 * 7 * 4 * 12))) + + let formattedTime = '0 milliseconds' + if (ms > 0) { + formattedTime = `${ms} milliseconds` + } + if (secs > 0) { + formattedTime = `${secs} seconds ${formattedTime}` + } + if (mins > 0) { + formattedTime = `${mins} minutes ${formattedTime}` + } + if (hrs > 0) { + formattedTime = `${hrs} hours ${formattedTime}` + } + if (days > 0) { + formattedTime = `${days} days ${formattedTime}` + } + if (weeks > 0) { + formattedTime = `${weeks} weeks ${formattedTime}` + } + if (mnths > 0) { + formattedTime = `${mnths} months ${formattedTime}` + } + if (yrs > 0) { + formattedTime = `${yrs} years ${formattedTime}` + } + + return formattedTime.trim() +} + /** * Check if exists. * @@ -615,6 +988,14 @@ async function checkIsMemberOfProject (userId, projectId) { } module.exports = { + getParamFromCliArgs, + promptUser, + createIndex, + deleteIndex, + indexBulkDataToES, + indexDataToEsById, + importData, + exportData, checkIfExists, autoWrapExpress, setResHeaders, diff --git a/src/models/Job.js b/src/models/Job.js index 14cec753..d6cc3955 100644 --- a/src/models/Job.js +++ b/src/models/Job.js @@ -69,7 +69,7 @@ module.exports = (sequelize) => { type: Sequelize.STRING(255) }, description: { - type: Sequelize.TEXT, // technically unlimited length + type: Sequelize.TEXT // technically unlimited length }, title: { type: Sequelize.STRING(128), From cb03be8a156d6d52180cd52c45ab0a76abe8f1c5 Mon Sep 17 00:00:00 2001 From: imcaizheng Date: Wed, 3 Feb 2021 19:58:22 +0800 Subject: [PATCH 10/46] Fix to RCRM Import script: compare endDate with today date --- scripts/recruit-crm-job-import/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/recruit-crm-job-import/index.js b/scripts/recruit-crm-job-import/index.js index b0d891ae..427a64f6 100644 --- a/scripts/recruit-crm-job-import/index.js +++ b/scripts/recruit-crm-job-import/index.js @@ -133,7 +133,7 @@ async function processJob (job, info = []) { data.resourceBookingId = result.id } // update the resourceBooking based on startDate and endDate - const resourceBookingStatus = dateFNS.compareAsc(new Date(data.startDate), new Date(data.endDate)) === 1 ? 'closed' : 'assigned' + const resourceBookingStatus = dateFNS.isBefore(data.endDate, dateFNS.startOfToday()) ? 'closed' : 'assigned' logger.debug(`resourceBookingId: ${data.resourceBookingId} status: ${resourceBookingStatus}`) await helper.updateResourceBookingStatus(data.resourceBookingId, resourceBookingStatus) info.push({ text: `id: ${data.resourceBookingId} status: ${resourceBookingStatus} resource booking updated`, tag: 'resource_booking_status_updated' }) From faded40ea18ae921fcaa816806c71ad1723b3bd4 Mon Sep 17 00:00:00 2001 From: maxceem Date: Wed, 3 Feb 2021 15:16:03 +0200 Subject: [PATCH 11/46] chore: improve local:init and local:reset - now these commands clears data if exists and don't ask for confirmation --- README.md | 10 ++++++---- package.json | 3 ++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4c1df143..8544f415 100644 --- a/README.md +++ b/README.md @@ -117,10 +117,11 @@ npm run local:init ``` - This command will do 2 things: + This command will do 3 things: - - create Database table - - create Elasticsearch indexes + - create Database tables (drop if exists) + - create Elasticsearch indexes (drop if exists) + - import demo data to Database and index it to ElasticSearch (clears any existent data if exist) 5. 🚀 Start Topcoder TaaS API @@ -148,7 +149,8 @@ | `npm run services:up` | Start services via docker-compose for local development. | | `npm run services:down` | Stop services via docker-compose for local development. | | `npm run services:logs -- -f ` | View logs of some service inside docker-compose. | -| `npm run local:init` | Creates Elasticsearch indexes and initializes Database. | +| `npm run local:init` | Recreate Database and Elasticsearch indexes and populate demo data for local development (removes any existent data). | +| `npm run local:reset` | Recreate Database and Elasticsearch indexes (removes any existent data). | | `npm run cov` | Code Coverage Report. | | `npm run migrate` | Run any migration files which haven't run yet. | | `npm run migrate:undo` | Revert most recent migration. | diff --git a/package.json b/package.json index 65029201..cc0508e0 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "services:up": "docker-compose -f ./local/docker-compose.yml up -d", "services:down": "docker-compose -f ./local/docker-compose.yml down", "services:logs": "docker-compose -f ./local/docker-compose.yml logs", - "local:init": "npm run create-index && npm run init-db", + "local:init": "npm run local:reset && npm run data:import -- --force", + "local:reset": "npm run delete-index -- --force && npm run create-index -- --force && npm run init-db force", "cov": "nyc --reporter=html --reporter=text mocha test/unit/*.test.js --timeout 30000 --exit" }, "keywords": [], From 2f84be7a5bb1d72f16e5f47cebdd932d5eec6977 Mon Sep 17 00:00:00 2001 From: imcaizheng Date: Thu, 4 Feb 2021 16:27:57 +0800 Subject: [PATCH 12/46] ndex Data Script: Include the `id` field inside the body of a document --- src/common/helper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/helper.js b/src/common/helper.js index c7673c0f..d30357e5 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -276,7 +276,7 @@ async function indexDataToEsById (id, modelName, indexName, logger) { await esClient.index({ index: indexName, id: id, - body: _.omit(data.dataValues, 'id') + body: data.dataValues }) logger.info({ component: 'indexDataToEsById', message: 'Indexing complete!' }) } From 837bc3eb900714f661cd3fe8ecbb738312240ac3 Mon Sep 17 00:00:00 2001 From: maxceem Date: Thu, 4 Feb 2021 12:23:20 +0200 Subject: [PATCH 13/46] chore: remove redundant config we use "config.js" instead of "config.json" --- config/config.json | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 config/config.json diff --git a/config/config.json b/config/config.json deleted file mode 100644 index b8ffd027..00000000 --- a/config/config.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "development": { - "username": "postgres", - "password": "postgres", - "database": "postgres", - "host": "127.0.0.1", - "dialect": "postgres" - }, - "test": { - "username": "root", - "password": null, - "database": "database_test", - "host": "127.0.0.1", - "dialect": "mysql" - }, - "production": { - "username": "root", - "password": null, - "database": "database_production", - "host": "127.0.0.1", - "dialect": "mysql" - } -} From cc983dec61f18f0267f1d9b74299978a2b074ddb Mon Sep 17 00:00:00 2001 From: maxceem Date: Thu, 4 Feb 2021 13:12:54 +0200 Subject: [PATCH 14/46] fix: don't index soft-deleted records --- src/common/helper.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/common/helper.js b/src/common/helper.js index d30357e5..2984e569 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -230,7 +230,15 @@ async function indexBulkDataToES (modelName, indexName, logger) { // get data from db logger.info({ component: 'indexBulkDataToES', message: 'Getting data from database' }) const model = models[modelName] - const data = await model.findAll({ raw: true }) + const data = await model.findAll({ + where: { + deletedAt: null + }, + raw: true, + attributes: { + exclude: ['deletedAt'] + }, + }) if (_.isEmpty(data)) { logger.info({ component: 'indexBulkDataToES', message: `No data in database for ${modelName}` }) return From 28979e52cfec1136286c13a2553e87d881908dc0 Mon Sep 17 00:00:00 2001 From: maxceem Date: Thu, 4 Feb 2021 13:13:18 +0200 Subject: [PATCH 15/46] fix: local:init should't fail if index not exists --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 26c34c85..c7b71abe 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "services:down": "docker-compose -f ./local/docker-compose.yml down", "services:logs": "docker-compose -f ./local/docker-compose.yml logs", "local:init": "npm run local:reset && npm run data:import -- --force", - "local:reset": "npm run delete-index -- --force && npm run create-index -- --force && npm run init-db force", + "local:reset": "npm run delete-index -- --force || true && npm run create-index -- --force && npm run init-db force", "cov": "nyc --reporter=html --reporter=text mocha test/unit/*.test.js --timeout 30000 --exit" }, "keywords": [], From 7d847dc79e20d206f03282d09d096f8a14bc4b72 Mon Sep 17 00:00:00 2001 From: maxceem Date: Thu, 4 Feb 2021 13:13:38 +0200 Subject: [PATCH 16/46] chore: don't use port 5000 as we use it for auth --- local/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local/docker-compose.yml b/local/docker-compose.yml index 509d8640..7c7104b8 100644 --- a/local/docker-compose.yml +++ b/local/docker-compose.yml @@ -54,7 +54,7 @@ services: GIT_BRANCH: dev command: start kafka-client ports: - - 5000:5000 + - 5001:5001 depends_on: - kafka-client - elasticsearch From fea3c45a664913919dd408f8a5f7fc217fbb9a55 Mon Sep 17 00:00:00 2001 From: maxceem Date: Thu, 4 Feb 2021 15:30:35 +0200 Subject: [PATCH 17/46] fix: allow empty "description" and "resourceType" this is done to make it easier creating UI for editing these fields, as usually forms send empty value instead of "null" or "undefined" --- src/bootstrap.js | 4 ++++ src/services/JobService.js | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/bootstrap.js b/src/bootstrap.js index eaaf88ec..fc4a1983 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -10,6 +10,10 @@ Joi.jobStatus = () => Joi.string().valid('sourcing', 'in-review', 'assigned', 'c Joi.workload = () => Joi.string().valid('full-time', 'fractional') Joi.jobCandidateStatus = () => Joi.string().valid('open', 'selected', 'shortlist', 'rejected', 'cancelled') Joi.title = () => Joi.string().max(128) +// Empty string is not allowed by Joi by default and must be enabled with allow(''). +// See https://joi.dev/api/?v=17.3.0#string fro details why it's like this. +// In many cases we would like to allow empty string to make it easier to create UI for editing data. +Joi.stringAllowEmpty = () => Joi.string().allow('') function buildServices (dir) { const files = fs.readdirSync(dir) diff --git a/src/services/JobService.js b/src/services/JobService.js index ff616155..fce7c028 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -164,12 +164,12 @@ createJob.schema = Joi.object().keys({ status: Joi.jobStatus().default('sourcing'), projectId: Joi.number().integer().required(), externalId: Joi.string(), - description: Joi.string(), + description: Joi.stringAllowEmpty(), title: Joi.title().required(), startDate: Joi.date(), endDate: Joi.date(), numPositions: Joi.number().integer().min(1).required(), - resourceType: Joi.string(), + resourceType: Joi.stringAllowEmpty(), rateType: Joi.rateType(), workload: Joi.workload(), skills: Joi.array().items(Joi.string().uuid()).required() @@ -226,12 +226,12 @@ partiallyUpdateJob.schema = Joi.object().keys({ data: Joi.object().keys({ status: Joi.jobStatus(), externalId: Joi.string(), - description: Joi.string(), + description: Joi.stringAllowEmpty(), title: Joi.title(), startDate: Joi.date(), endDate: Joi.date(), numPositions: Joi.number().integer().min(1), - resourceType: Joi.string(), + resourceType: Joi.stringAllowEmpty(), rateType: Joi.rateType(), workload: Joi.workload(), skills: Joi.array().items(Joi.string().uuid()) From 80bfd1898ad756955ed47f835c8c5944128eebdf Mon Sep 17 00:00:00 2001 From: imcaizheng Date: Sat, 6 Feb 2021 13:58:12 +0800 Subject: [PATCH 18/46] remove the clearObject function --- src/common/helper.js | 16 ---------------- src/services/JobCandidateService.js | 8 ++++---- src/services/JobService.js | 12 ++++++------ src/services/ResourceBookingService.js | 10 +++++----- 4 files changed, 15 insertions(+), 31 deletions(-) diff --git a/src/common/helper.js b/src/common/helper.js index 2984e569..75e6b732 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -540,21 +540,6 @@ function setResHeaders (req, res, result) { } } -/** - * Clear the object, remove all null or empty array field - * @param {Object|Array} obj the given object - */ -function clearObject (obj) { - if (_.isNull(obj)) { - return undefined - } - if (_.isArray(obj)) { - return _.map(obj, e => _.omitBy(e, _.isNull)) - } else { - return _.omitBy(obj, (p) => { return _.isNull(p) || (_.isArray(p) && _.isEmpty(p)) }) - } -} - /** * Get ES Client * @return {Object} Elastic Host Client Instance @@ -1007,7 +992,6 @@ module.exports = { checkIfExists, autoWrapExpress, setResHeaders, - clearObject, getESClient, getUserId: async (userId) => { // check m2m user id diff --git a/src/services/JobCandidateService.js b/src/services/JobCandidateService.js index 7153ebb4..f0f31b56 100644 --- a/src/services/JobCandidateService.js +++ b/src/services/JobCandidateService.js @@ -65,7 +65,7 @@ async function getJobCandidate (currentUser, id, fromDb = false) { await _checkUserPermissionForGetJobCandidate(currentUser, jobCandidate.jobId) // check user permission - return helper.clearObject(jobCandidate.dataValues) + return jobCandidate.dataValues } getJobCandidate.schema = Joi.object().keys({ @@ -95,7 +95,7 @@ async function createJobCandidate (currentUser, jobCandidate) { const created = await JobCandidate.create(jobCandidate) await helper.postEvent(config.TAAS_JOB_CANDIDATE_CREATE_TOPIC, jobCandidate) - return helper.clearObject(created.dataValues) + return created.dataValues } createJobCandidate.schema = Joi.object().keys({ @@ -131,7 +131,7 @@ async function updateJobCandidate (currentUser, id, data) { await jobCandidate.update(data) await helper.postEvent(config.TAAS_JOB_CANDIDATE_UPDATE_TOPIC, { id, ...data }) - const result = helper.clearObject(_.assign(jobCandidate.dataValues, data)) + const result = _.assign(jobCandidate.dataValues, data) return result } @@ -289,7 +289,7 @@ async function searchJobCandidates (currentUser, criteria) { total: jobCandidates.length, page, perPage, - result: _.map(jobCandidates, jobCandidate => helper.clearObject(jobCandidate.dataValues)) + result: _.map(jobCandidates, jobCandidate => jobCandidate.dataValues) } } diff --git a/src/services/JobService.js b/src/services/JobService.js index fce7c028..9ad4f3df 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -126,8 +126,8 @@ async function getJob (currentUser, id, fromDb = false) { await _checkUserPermissionForGetJob(currentUser, job.projectId) // check user permission - job.dataValues.candidates = _.map(job.dataValues.candidates, (c) => helper.clearObject(c.dataValues)) - return helper.clearObject(job.dataValues) + job.dataValues.candidates = _.map(job.dataValues.candidates, (c) => c.dataValues) + return job.dataValues } getJob.schema = Joi.object().keys({ @@ -155,7 +155,7 @@ async function createJob (currentUser, job) { const created = await Job.create(job) await helper.postEvent(config.TAAS_JOB_CREATE_TOPIC, job) - return helper.clearObject(created.dataValues) + return created.dataValues } createJob.schema = Joi.object().keys({ @@ -205,8 +205,8 @@ async function updateJob (currentUser, id, data) { await job.update(data) await helper.postEvent(config.TAAS_JOB_UPDATE_TOPIC, { id, ...data }, { oldValue: oldValue }) job = await Job.findById(id, true) - job.dataValues.candidates = _.map(job.dataValues.candidates, (c) => helper.clearObject(c.dataValues)) - return helper.clearObject(job.dataValues) + job.dataValues.candidates = _.map(job.dataValues.candidates, (c) => c.dataValues) + return job.dataValues } /** @@ -466,7 +466,7 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } total: jobs.length, page, perPage, - result: _.map(jobs, job => helper.clearObject(job.dataValues)) + result: _.map(jobs, job => job.dataValues) } } diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index 77b7d72a..900c779e 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -24,9 +24,9 @@ const esClient = helper.getESClient() */ async function _getResourceBookingFilteringFields (currentUser, resourceBooking) { if (currentUser.hasManagePermission || currentUser.isMachine) { - return helper.clearObject(resourceBooking) + return resourceBooking } - return _.omit(helper.clearObject(resourceBooking), 'memberRate') + return _.omit(resourceBooking, 'memberRate') } /** @@ -108,7 +108,7 @@ async function createResourceBooking (currentUser, resourceBooking) { const created = await ResourceBooking.create(resourceBooking) await helper.postEvent(config.TAAS_RESOURCE_BOOKING_CREATE_TOPIC, resourceBooking) - return helper.clearObject(created.dataValues) + return created.dataValues } createResourceBooking.schema = Joi.object().keys({ @@ -147,7 +147,7 @@ async function updateResourceBooking (currentUser, id, data) { await resourceBooking.update(data) await helper.postEvent(config.TAAS_RESOURCE_BOOKING_UPDATE_TOPIC, { id, ...data }, { oldValue: oldValue }) - const result = helper.clearObject(_.assign(resourceBooking.dataValues, data)) + const result = _.assign(resourceBooking.dataValues, data) return result } @@ -348,7 +348,7 @@ async function searchResourceBookings (currentUser, criteria, options = { return total: resourceBookings.length, page, perPage, - result: _.map(resourceBookings, resourceBooking => helper.clearObject(resourceBooking.dataValues)) + result: _.map(resourceBookings, resourceBooking => resourceBooking.dataValues) } } From afa07b04493bf2f5a9328c0e46a1ec8f655ad66c Mon Sep 17 00:00:00 2001 From: imcaizheng Date: Sat, 6 Feb 2021 14:58:03 +0800 Subject: [PATCH 19/46] Post data to Kafka Events from created/updated in DB --- src/models/index.js | 10 ++++++++++ src/services/JobCandidateService.js | 6 +++--- src/services/JobService.js | 6 +++--- src/services/ResourceBookingService.js | 6 +++--- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/models/index.js b/src/models/index.js index 84b77a0a..8134860e 100644 --- a/src/models/index.js +++ b/src/models/index.js @@ -17,11 +17,21 @@ const sequelize = new Sequelize(config.get('DATABASE_URL'), { logging: false }) +// add customized toJSON method to a model +const addMethodOfToJSON = (model) => { + model.prototype.toJSON = function () { + const result = Object.assign({}, this.get()) + delete result.deletedAt + return result + } +} + fs .readdirSync(__dirname) .filter(file => (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js')) .forEach((file) => { const model = require(path.join(__dirname, file))(sequelize) + addMethodOfToJSON(model) db[model.name] = model }) diff --git a/src/services/JobCandidateService.js b/src/services/JobCandidateService.js index 7153ebb4..b33046c4 100644 --- a/src/services/JobCandidateService.js +++ b/src/services/JobCandidateService.js @@ -94,7 +94,7 @@ async function createJobCandidate (currentUser, jobCandidate) { jobCandidate.createdBy = await helper.getUserId(currentUser.userId) const created = await JobCandidate.create(jobCandidate) - await helper.postEvent(config.TAAS_JOB_CANDIDATE_CREATE_TOPIC, jobCandidate) + await helper.postEvent(config.TAAS_JOB_CANDIDATE_CREATE_TOPIC, created.toJSON()) return helper.clearObject(created.dataValues) } @@ -129,8 +129,8 @@ async function updateJobCandidate (currentUser, id, data) { data.updatedAt = new Date() data.updatedBy = userId - await jobCandidate.update(data) - await helper.postEvent(config.TAAS_JOB_CANDIDATE_UPDATE_TOPIC, { id, ...data }) + const updated = await jobCandidate.update(data) + await helper.postEvent(config.TAAS_JOB_CANDIDATE_UPDATE_TOPIC, updated.toJSON()) const result = helper.clearObject(_.assign(jobCandidate.dataValues, data)) return result } diff --git a/src/services/JobService.js b/src/services/JobService.js index fce7c028..25bbe955 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -154,7 +154,7 @@ async function createJob (currentUser, job) { job.createdBy = await helper.getUserId(currentUser.userId) const created = await Job.create(job) - await helper.postEvent(config.TAAS_JOB_CREATE_TOPIC, job) + await helper.postEvent(config.TAAS_JOB_CREATE_TOPIC, created.toJSON()) return helper.clearObject(created.dataValues) } @@ -202,8 +202,8 @@ async function updateJob (currentUser, id, data) { data.updatedAt = new Date() data.updatedBy = ubahnUserId - await job.update(data) - await helper.postEvent(config.TAAS_JOB_UPDATE_TOPIC, { id, ...data }, { oldValue: oldValue }) + const updated = await job.update(data) + await helper.postEvent(config.TAAS_JOB_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue }) job = await Job.findById(id, true) job.dataValues.candidates = _.map(job.dataValues.candidates, (c) => helper.clearObject(c.dataValues)) return helper.clearObject(job.dataValues) diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index 77b7d72a..27dd8b32 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -107,7 +107,7 @@ async function createResourceBooking (currentUser, resourceBooking) { resourceBooking.createdBy = await helper.getUserId(currentUser.userId) const created = await ResourceBooking.create(resourceBooking) - await helper.postEvent(config.TAAS_RESOURCE_BOOKING_CREATE_TOPIC, resourceBooking) + await helper.postEvent(config.TAAS_RESOURCE_BOOKING_CREATE_TOPIC, created.toJSON()) return helper.clearObject(created.dataValues) } @@ -145,8 +145,8 @@ async function updateResourceBooking (currentUser, id, data) { data.updatedAt = new Date() data.updatedBy = await helper.getUserId(currentUser.userId) - await resourceBooking.update(data) - await helper.postEvent(config.TAAS_RESOURCE_BOOKING_UPDATE_TOPIC, { id, ...data }, { oldValue: oldValue }) + const updated = await resourceBooking.update(data) + await helper.postEvent(config.TAAS_RESOURCE_BOOKING_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue }) const result = helper.clearObject(_.assign(resourceBooking.dataValues, data)) return result } From 89d253a3865d134d7599fcf9b7cb6cea750eaf76 Mon Sep 17 00:00:00 2001 From: imcaizheng Date: Sun, 7 Feb 2021 10:33:21 +0800 Subject: [PATCH 20/46] Allow null values in request body for optional fields --- src/services/JobCandidateService.js | 12 ++++---- src/services/JobService.js | 42 +++++++++++++------------- src/services/ResourceBookingService.js | 28 ++++++++--------- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/src/services/JobCandidateService.js b/src/services/JobCandidateService.js index f0f31b56..2713ba8c 100644 --- a/src/services/JobCandidateService.js +++ b/src/services/JobCandidateService.js @@ -104,8 +104,8 @@ createJobCandidate.schema = Joi.object().keys({ status: Joi.jobCandidateStatus().default('open'), jobId: Joi.string().uuid().required(), userId: Joi.string().uuid().required(), - externalId: Joi.string(), - resume: Joi.string().uri() + externalId: Joi.string().allow(null), + resume: Joi.string().uri().allow(null) }).required() }).required() @@ -151,8 +151,8 @@ partiallyUpdateJobCandidate.schema = Joi.object().keys({ id: Joi.string().uuid().required(), data: Joi.object().keys({ status: Joi.jobCandidateStatus(), - externalId: Joi.string(), - resume: Joi.string().uri() + externalId: Joi.string().allow(null), + resume: Joi.string().uri().allow(null) }).required() }).required() @@ -176,8 +176,8 @@ fullyUpdateJobCandidate.schema = Joi.object().keys({ jobId: Joi.string().uuid().required(), userId: Joi.string().uuid().required(), status: Joi.jobCandidateStatus(), - externalId: Joi.string(), - resume: Joi.string().uri() + externalId: Joi.string().allow(null), + resume: Joi.string().uri().allow(null) }).required() }).required() diff --git a/src/services/JobService.js b/src/services/JobService.js index 9ad4f3df..a445e575 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -163,15 +163,15 @@ createJob.schema = Joi.object().keys({ job: Joi.object().keys({ status: Joi.jobStatus().default('sourcing'), projectId: Joi.number().integer().required(), - externalId: Joi.string(), - description: Joi.stringAllowEmpty(), + externalId: Joi.string().allow(null), + description: Joi.stringAllowEmpty().allow(null), title: Joi.title().required(), - startDate: Joi.date(), - endDate: Joi.date(), + startDate: Joi.date().allow(null), + endDate: Joi.date().allow(null), numPositions: Joi.number().integer().min(1).required(), - resourceType: Joi.stringAllowEmpty(), - rateType: Joi.rateType(), - workload: Joi.workload(), + resourceType: Joi.stringAllowEmpty().allow(null), + rateType: Joi.rateType().allow(null), + workload: Joi.workload().allow(null), skills: Joi.array().items(Joi.string().uuid()).required() }).required() }).required() @@ -225,15 +225,15 @@ partiallyUpdateJob.schema = Joi.object().keys({ id: Joi.string().guid().required(), data: Joi.object().keys({ status: Joi.jobStatus(), - externalId: Joi.string(), - description: Joi.stringAllowEmpty(), + externalId: Joi.string().allow(null), + description: Joi.stringAllowEmpty().allow(null), title: Joi.title(), - startDate: Joi.date(), - endDate: Joi.date(), + startDate: Joi.date().allow(null), + endDate: Joi.date().allow(null), numPositions: Joi.number().integer().min(1), - resourceType: Joi.stringAllowEmpty(), - rateType: Joi.rateType(), - workload: Joi.workload(), + resourceType: Joi.stringAllowEmpty().allow(null), + rateType: Joi.rateType().allow(null), + workload: Joi.workload().allow(null), skills: Joi.array().items(Joi.string().uuid()) }).required() }).required() @@ -254,15 +254,15 @@ fullyUpdateJob.schema = Joi.object().keys({ id: Joi.string().guid().required(), data: Joi.object().keys({ projectId: Joi.number().integer().required(), - externalId: Joi.string(), - description: Joi.string(), + externalId: Joi.string().allow(null), + description: Joi.string().allow(null), title: Joi.title().required(), - startDate: Joi.date(), - endDate: Joi.date(), + startDate: Joi.date().allow(null), + endDate: Joi.date().allow(null), numPositions: Joi.number().integer().min(1).required(), - resourceType: Joi.string(), - rateType: Joi.rateType(), - workload: Joi.workload(), + resourceType: Joi.string().allow(null), + rateType: Joi.rateType().allow(null), + workload: Joi.workload().allow(null), skills: Joi.array().items(Joi.string().uuid()).required(), status: Joi.jobStatus() }).required() diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index 900c779e..bacfb27d 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -117,11 +117,11 @@ createResourceBooking.schema = Joi.object().keys({ status: Joi.jobStatus().default('sourcing'), projectId: Joi.number().integer().required(), userId: Joi.string().uuid().required(), - jobId: Joi.string().uuid(), - startDate: Joi.date(), - endDate: Joi.date(), - memberRate: Joi.number(), - customerRate: Joi.number(), + jobId: Joi.string().uuid().allow(null), + startDate: Joi.date().allow(null), + endDate: Joi.date().allow(null), + memberRate: Joi.number().allow(null), + customerRate: Joi.number().allow(null), rateType: Joi.rateType().required() }).required() }).required() @@ -167,10 +167,10 @@ partiallyUpdateResourceBooking.schema = Joi.object().keys({ id: Joi.string().uuid().required(), data: Joi.object().keys({ status: Joi.jobStatus(), - startDate: Joi.date(), - endDate: Joi.date(), - memberRate: Joi.number(), - customerRate: Joi.number(), + startDate: Joi.date().allow(null), + endDate: Joi.date().allow(null), + memberRate: Joi.number().allow(null), + customerRate: Joi.number().allow(null), rateType: Joi.rateType() }).required() }).required() @@ -196,11 +196,11 @@ fullyUpdateResourceBooking.schema = Joi.object().keys({ data: Joi.object().keys({ projectId: Joi.number().integer().required(), userId: Joi.string().uuid().required(), - jobId: Joi.string().uuid(), - startDate: Joi.date(), - endDate: Joi.date(), - memberRate: Joi.number(), - customerRate: Joi.number(), + jobId: Joi.string().uuid().allow(null), + startDate: Joi.date().allow(null), + endDate: Joi.date().allow(null), + memberRate: Joi.number().allow(null), + customerRate: Joi.number().allow(null), rateType: Joi.rateType().required(), status: Joi.jobStatus().required() }).required() From e68f038e60fb41e2c5065de2428e8d58b0a38ea6 Mon Sep 17 00:00:00 2001 From: imcaizheng Date: Sun, 7 Feb 2021 16:54:58 +0800 Subject: [PATCH 21/46] enable paranoid mode for models --- src/common/helper.js | 16 +------ src/eventHandlers/JobEventHandler.js | 3 +- .../ResourceBookingEventHandler.js | 6 +-- src/models/Job.js | 43 ++++++++----------- src/models/JobCandidate.js | 35 +++++++-------- src/models/ResourceBooking.js | 26 ++++++----- src/models/index.js | 10 ----- src/services/JobCandidateService.js | 9 +--- src/services/JobService.js | 19 ++------ src/services/ResourceBookingService.js | 9 +--- 10 files changed, 62 insertions(+), 114 deletions(-) diff --git a/src/common/helper.js b/src/common/helper.js index 75e6b732..4b049fe7 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -231,13 +231,7 @@ async function indexBulkDataToES (modelName, indexName, logger) { logger.info({ component: 'indexBulkDataToES', message: 'Getting data from database' }) const model = models[modelName] const data = await model.findAll({ - where: { - deletedAt: null - }, - raw: true, - attributes: { - exclude: ['deletedAt'] - }, + raw: true }) if (_.isEmpty(data)) { logger.info({ component: 'indexBulkDataToES', message: `No data in database for ${modelName}` }) @@ -372,13 +366,7 @@ async function exportData (pathToFile, dataModels, logger) { for (let index = 0; index < dataModels.length; index += 1) { const modelName = dataModels[index] const modelRecords = await models[modelName].findAll({ - raw: true, - where: { - deletedAt: null - }, - attributes: { - exclude: ['deletedAt'] - } + raw: true }) allModelsRecords[modelName] = modelRecords logger.info({ component: 'exportData', message: `Records loaded for model: ${modelName} = ${modelRecords.length}` }) diff --git a/src/eventHandlers/JobEventHandler.js b/src/eventHandlers/JobEventHandler.js index adce00ad..e1938de2 100644 --- a/src/eventHandlers/JobEventHandler.js +++ b/src/eventHandlers/JobEventHandler.js @@ -39,8 +39,7 @@ async function cancelJob (payload) { jobId: job.id, status: { [Op.not]: 'cancelled' - }, - deletedAt: null + } } }) await Promise.all(candidates.map(candidate => JobCandidateService.partiallyUpdateJobCandidate( diff --git a/src/eventHandlers/ResourceBookingEventHandler.js b/src/eventHandlers/ResourceBookingEventHandler.js index bc023e9d..69dbe1dd 100644 --- a/src/eventHandlers/ResourceBookingEventHandler.js +++ b/src/eventHandlers/ResourceBookingEventHandler.js @@ -50,8 +50,7 @@ async function selectJobCandidate (payload) { userId: resourceBooking.userId, status: { [Op.not]: 'selected' - }, - deletedAt: null + } } }) await Promise.all(candidates.map(candidate => JobCandidateService.partiallyUpdateJobCandidate( @@ -111,8 +110,7 @@ async function assignJob (payload) { const resourceBookings = await models.ResourceBooking.findAll({ where: { jobId: job.id, - status: 'assigned', - deletedAt: null + status: 'assigned' } }) logger.debug({ diff --git a/src/models/Job.js b/src/models/Job.js index d6cc3955..5fa4a9ac 100644 --- a/src/models/Job.js +++ b/src/models/Job.js @@ -24,24 +24,14 @@ module.exports = (sequelize) => { static async findById (id, withCandidates = false) { const criteria = { where: { - id, - deletedAt: null - }, - attributes: { - exclude: ['deletedAt'] + id } } if (withCandidates) { criteria.include = [{ model: Job._models.JobCandidate, as: 'candidates', - where: { - deletedAt: null - }, - required: false, - attributes: { - exclude: ['deletedAt'] - } + required: false }] } const job = await Job.findOne(criteria) @@ -108,34 +98,35 @@ module.exports = (sequelize) => { type: Sequelize.STRING(255), allowNull: false }, - createdAt: { - field: 'created_at', - type: Sequelize.DATE, - allowNull: false - }, createdBy: { field: 'created_by', type: Sequelize.UUID, allowNull: false }, - updatedAt: { - field: 'updated_at', - type: Sequelize.DATE - }, updatedBy: { field: 'updated_by', type: Sequelize.UUID - }, - deletedAt: { - field: 'deleted_at', - type: Sequelize.DATE } }, { schema: config.DB_SCHEMA_NAME, sequelize, tableName: 'jobs', - timestamps: false + paranoid: true, + deletedAt: 'deletedAt', + createdAt: 'createdAt', + updatedAt: 'updatedAt', + timestamps: true, + defaultScope: { + attributes: { + exclude: ['deletedAt'] + } + }, + hooks: { + afterCreate: (job) => { + delete job.dataValues.deletedAt + } + } } ) diff --git a/src/models/JobCandidate.js b/src/models/JobCandidate.js index 70632b66..626dd86b 100644 --- a/src/models/JobCandidate.js +++ b/src/models/JobCandidate.js @@ -21,11 +21,7 @@ module.exports = (sequelize) => { static async findById (id) { const jobCandidate = await JobCandidate.findOne({ where: { - id, - deletedAt: null - }, - attributes: { - exclude: ['deletedAt'] + id } }) if (!jobCandidate) { @@ -63,34 +59,35 @@ module.exports = (sequelize) => { resume: { type: Sequelize.STRING(2048) }, - createdAt: { - field: 'created_at', - type: Sequelize.DATE, - allowNull: false - }, createdBy: { field: 'created_by', type: Sequelize.UUID, allowNull: false }, - updatedAt: { - field: 'updated_at', - type: Sequelize.DATE - }, updatedBy: { field: 'updated_by', type: Sequelize.UUID - }, - deletedAt: { - field: 'deleted_at', - type: Sequelize.DATE } }, { schema: config.DB_SCHEMA_NAME, sequelize, tableName: 'job_candidates', - timestamps: false + paranoid: true, + deletedAt: 'deletedAt', + createdAt: 'createdAt', + updatedAt: 'updatedAt', + timestamps: true, + defaultScope: { + attributes: { + exclude: ['deletedAt'] + } + }, + hooks: { + afterCreate: (jobCandidate) => { + delete jobCandidate.dataValues.deletedAt + } + } } ) diff --git a/src/models/ResourceBooking.js b/src/models/ResourceBooking.js index 63557904..6f6f7c1a 100644 --- a/src/models/ResourceBooking.js +++ b/src/models/ResourceBooking.js @@ -20,11 +20,7 @@ module.exports = (sequelize) => { static async findById (id) { const resourceBooking = await ResourceBooking.findOne({ where: { - id, - deletedAt: null - }, - attributes: { - exclude: ['deletedAt'] + id } }) if (!resourceBooking) { @@ -97,17 +93,27 @@ module.exports = (sequelize) => { updatedBy: { field: 'updated_by', type: Sequelize.UUID - }, - deletedAt: { - field: 'deleted_at', - type: Sequelize.DATE } }, { schema: config.DB_SCHEMA_NAME, sequelize, tableName: 'resource_bookings', - timestamps: false + paranoid: true, + deletedAt: 'deletedAt', + createdAt: 'createdAt', + updatedAt: 'updatedAt', + timestamps: true, + defaultScope: { + attributes: { + exclude: ['deletedAt'] + } + }, + hooks: { + afterCreate: (resourceBooking) => { + delete resourceBooking.dataValues.deletedAt + } + } } ) diff --git a/src/models/index.js b/src/models/index.js index 8134860e..84b77a0a 100644 --- a/src/models/index.js +++ b/src/models/index.js @@ -17,21 +17,11 @@ const sequelize = new Sequelize(config.get('DATABASE_URL'), { logging: false }) -// add customized toJSON method to a model -const addMethodOfToJSON = (model) => { - model.prototype.toJSON = function () { - const result = Object.assign({}, this.get()) - delete result.deletedAt - return result - } -} - fs .readdirSync(__dirname) .filter(file => (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js')) .forEach((file) => { const model = require(path.join(__dirname, file))(sequelize) - addMethodOfToJSON(model) db[model.name] = model }) diff --git a/src/services/JobCandidateService.js b/src/services/JobCandidateService.js index e54fa5dc..91e0bff4 100644 --- a/src/services/JobCandidateService.js +++ b/src/services/JobCandidateService.js @@ -193,7 +193,7 @@ async function deleteJobCandidate (currentUser, id) { } const jobCandidate = await JobCandidate.findById(id) - await jobCandidate.update({ deletedAt: new Date() }) + await jobCandidate.destroy() await helper.postEvent(config.TAAS_JOB_CANDIDATE_DELETE_TOPIC, { id }) } @@ -269,17 +269,12 @@ async function searchJobCandidates (currentUser, criteria) { logger.logFullError(err, { component: 'JobCandidateService', context: 'searchJobCandidates' }) } logger.info({ component: 'JobCandidateService', context: 'searchJobCandidates', message: 'fallback to DB query' }) - const filter = { - [Op.and]: [{ deletedAt: null }] - } + const filter = {} _.each(_.pick(criteria, ['jobId', 'userId', 'status', 'externalId']), (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]] diff --git a/src/services/JobService.js b/src/services/JobService.js index 67bba937..821f58d8 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -155,7 +155,7 @@ async function createJob (currentUser, job) { const created = await Job.create(job) await helper.postEvent(config.TAAS_JOB_CREATE_TOPIC, created.toJSON()) - return created.dataValues + return created.toJSON() } createJob.schema = Joi.object().keys({ @@ -280,7 +280,7 @@ async function deleteJob (currentUser, id) { } const job = await Job.findById(id) - await job.update({ deletedAt: new Date() }) + await job.destroy() await helper.postEvent(config.TAAS_JOB_DELETE_TOPIC, { id }) } @@ -411,9 +411,7 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } logger.logFullError(err, { component: 'JobService', context: 'searchJobs' }) } logger.info({ component: 'JobService', context: 'searchJobs', message: 'fallback to DB query' }) - const filter = { - [Op.and]: [{ deletedAt: null }] - } + const filter = {} _.each(_.pick(criteria, [ 'projectId', 'externalId', @@ -443,22 +441,13 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } } 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'] - } + required: false }] }) return { diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index 0266f9a7..4de0d139 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -218,7 +218,7 @@ async function deleteResourceBooking (currentUser, id) { } const resourceBooking = await ResourceBooking.findById(id) - await resourceBooking.update({ deletedAt: new Date() }) + await resourceBooking.destroy() await helper.postEvent(config.TAAS_RESOURCE_BOOKING_DELETE_TOPIC, { id }) } @@ -325,9 +325,7 @@ async function searchResourceBookings (currentUser, criteria, options = { return logger.logFullError(err, { component: 'ResourceBookingService', context: 'searchResourceBookings' }) } logger.info({ component: 'ResourceBookingService', context: 'searchResourceBookings', message: 'fallback to DB query' }) - const filter = { - [Op.and]: [{ deletedAt: null }] - } + const filter = {} _.each(_.pick(criteria, ['status', 'startDate', 'endDate', 'rateType', 'projectId', 'jobId', 'userId']), (value, key) => { filter[Op.and].push({ [key]: value }) }) @@ -336,9 +334,6 @@ async function searchResourceBookings (currentUser, criteria, options = { return } const resourceBookings = await ResourceBooking.findAll({ where: filter, - attributes: { - exclude: ['deletedAt'] - }, offset: ((page - 1) * perPage), limit: perPage, order: [[criteria.sortBy, criteria.sortOrder]] From 30c74745f41fc4d631c6b7c375af3b5c779f8a9d Mon Sep 17 00:00:00 2001 From: maxceem Date: Sun, 7 Feb 2021 13:26:36 +0200 Subject: [PATCH 22/46] fix: allow empty when PUT jobs --- src/services/JobService.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/JobService.js b/src/services/JobService.js index a445e575..2d994970 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -255,12 +255,12 @@ fullyUpdateJob.schema = Joi.object().keys({ data: Joi.object().keys({ projectId: Joi.number().integer().required(), externalId: Joi.string().allow(null), - description: Joi.string().allow(null), + description: Joi.stringAllowEmpty().allow(null), title: Joi.title().required(), startDate: Joi.date().allow(null), endDate: Joi.date().allow(null), numPositions: Joi.number().integer().min(1).required(), - resourceType: Joi.string().allow(null), + resourceType: Joi.stringAllowEmpty().allow(null), rateType: Joi.rateType().allow(null), workload: Joi.workload().allow(null), skills: Joi.array().items(Joi.string().uuid()).required(), From 77865d50c0a6a54412e8fa3b5b30585450a2642c Mon Sep 17 00:00:00 2001 From: imcaizheng Date: Sun, 7 Feb 2021 20:15:37 +0800 Subject: [PATCH 23/46] remove manually setting createdAt and updatedAt --- src/services/JobCandidateService.js | 2 -- src/services/JobService.js | 2 -- src/services/ResourceBookingService.js | 2 -- 3 files changed, 6 deletions(-) diff --git a/src/services/JobCandidateService.js b/src/services/JobCandidateService.js index 91e0bff4..684ed683 100644 --- a/src/services/JobCandidateService.js +++ b/src/services/JobCandidateService.js @@ -90,7 +90,6 @@ async function createJobCandidate (currentUser, jobCandidate) { await helper.ensureUserById(jobCandidate.userId) // ensure user exists jobCandidate.id = uuid() - jobCandidate.createdAt = new Date() jobCandidate.createdBy = await helper.getUserId(currentUser.userId) const created = await JobCandidate.create(jobCandidate) @@ -126,7 +125,6 @@ async function updateJobCandidate (currentUser, id, data) { await helper.checkIsMemberOfProject(currentUser.userId, job.projectId) } - data.updatedAt = new Date() data.updatedBy = userId const updated = await jobCandidate.update(data) diff --git a/src/services/JobService.js b/src/services/JobService.js index 821f58d8..61282472 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -150,7 +150,6 @@ async function createJob (currentUser, job) { await _validateSkills(job.skills) job.id = uuid() - job.createdAt = new Date() job.createdBy = await helper.getUserId(currentUser.userId) const created = await Job.create(job) @@ -199,7 +198,6 @@ async function updateJob (currentUser, id, data) { } } - data.updatedAt = new Date() data.updatedBy = ubahnUserId const updated = await job.update(data) diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index 4de0d139..f6f86e34 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -103,7 +103,6 @@ async function createResourceBooking (currentUser, resourceBooking) { await helper.ensureUserById(resourceBooking.userId) // ensure user exists resourceBooking.id = uuid() - resourceBooking.createdAt = new Date() resourceBooking.createdBy = await helper.getUserId(currentUser.userId) const created = await ResourceBooking.create(resourceBooking) @@ -142,7 +141,6 @@ async function updateResourceBooking (currentUser, id, data) { const resourceBooking = await ResourceBooking.findById(id) const oldValue = resourceBooking.toJSON() - data.updatedAt = new Date() data.updatedBy = await helper.getUserId(currentUser.userId) const updated = await resourceBooking.update(data) From 9657d712cf2e6ae0b9eefcdd227c185210100027 Mon Sep 17 00:00:00 2001 From: imcaizheng Date: Mon, 8 Feb 2021 10:15:44 +0800 Subject: [PATCH 24/46] Return invites and members property for teams --- docs/swagger.yaml | 24 ++++++++++++++++++++++++ src/common/helper.js | 4 ++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 6ea1bd14..a080782b 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2094,6 +2094,18 @@ components: type: string example: "1212" description: "The team name." + invites: + type: array + items: + type: object + description: "The invites of the project" + example: [{"userIds": [40152855], "role": "copilot"}] + members: + type: array + items: + type: object + description: "The members of the project" + example: [{"lastName": "L_NAME", "role": "customer", "updatedBy": 21926562, "handle": "Tester123", "userId": 21926562, "deletedBy": null, "createdAt": "2021-01-12T10:58:26.237Z", "firstName": "F_NAME", "createdBy": 21926562, "isPrimary": false, "id": 13833, "projectId": 16893, "email": "email@domain.com.z", "updatedAt": "2021-01-12T10:58:26.237Z"}] startDate: type: string format: date-time @@ -2177,6 +2189,18 @@ components: type: string example: "1212" description: "The team name." + invites: + type: array + items: + type: object + description: "The invites of the project" + example: [{"userIds": [40152855], "role": "copilot"}] + members: + type: array + items: + type: object + description: "The members of the project" + example: [{"lastName": "J", "role": "manager", "updatedBy": 8547899, "handle": "TonyJ", "userId": 8547899, "deletedBy": null, "createdAt": "2021-01-12T04:34:15.006Z", "firstName": "Tony", "createdBy": 8547899, "isPrimary": true, "id": 13819, "projectId": 16893, "email": "ajefts@topcoder.com", "updatedAt": "2021-01-12T04:34:15.018Z"}] startDate: type: string format: date-time diff --git a/src/common/helper.js b/src/common/helper.js index 75e6b732..1bf7fcbe 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -698,7 +698,7 @@ async function getProjects (currentUser, criteria = {}) { .set('Accept', 'application/json') localLogger.debug({ context: 'getProjects', message: `response body: ${JSON.stringify(res.body)}` }) const result = _.map(res.body, item => { - return _.pick(item, ['id', 'name']) + return _.pick(item, ['id', 'name', 'invites', 'members']) }) return { total: Number(_.get(res.headers, 'x-total')), @@ -828,7 +828,7 @@ async function getProjectById (currentUser, id) { .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']) + return _.pick(res.body, ['id', 'name', 'invites', 'members']) } catch (err) { if (err.status === HttpStatus.FORBIDDEN) { throw new errors.ForbiddenError(`You are not allowed to access the project with id ${id}`) From 94f7b37f6c4dd93604086c83e5651038d223f069 Mon Sep 17 00:00:00 2001 From: imcaizheng Date: Mon, 8 Feb 2021 22:57:10 +0800 Subject: [PATCH 25/46] Fix: use snakecase for timestamps fields in DB --- src/models/Job.js | 12 ++++++++++++ src/models/JobCandidate.js | 12 ++++++++++++ src/models/ResourceBooking.js | 19 +++++++++++-------- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/models/Job.js b/src/models/Job.js index 5fa4a9ac..388e33bb 100644 --- a/src/models/Job.js +++ b/src/models/Job.js @@ -106,6 +106,18 @@ module.exports = (sequelize) => { updatedBy: { field: 'updated_by', type: Sequelize.UUID + }, + createdAt: { + field: 'created_at', + type: Sequelize.DATE, + }, + updatedAt: { + field: 'updated_at', + type: Sequelize.DATE + }, + deletedAt: { + field: 'deleted_at', + type: Sequelize.DATE } }, { diff --git a/src/models/JobCandidate.js b/src/models/JobCandidate.js index 626dd86b..1846569d 100644 --- a/src/models/JobCandidate.js +++ b/src/models/JobCandidate.js @@ -67,6 +67,18 @@ module.exports = (sequelize) => { updatedBy: { field: 'updated_by', type: Sequelize.UUID + }, + createdAt: { + field: 'created_at', + type: Sequelize.DATE, + }, + updatedAt: { + field: 'updated_at', + type: Sequelize.DATE + }, + deletedAt: { + field: 'deleted_at', + type: Sequelize.DATE } }, { diff --git a/src/models/ResourceBooking.js b/src/models/ResourceBooking.js index 6f6f7c1a..df861a89 100644 --- a/src/models/ResourceBooking.js +++ b/src/models/ResourceBooking.js @@ -76,23 +76,26 @@ module.exports = (sequelize) => { type: Sequelize.STRING(255), allowNull: false }, - createdAt: { - field: 'created_at', - type: Sequelize.DATE, - allowNull: false - }, createdBy: { field: 'created_by', type: Sequelize.UUID, allowNull: false }, + updatedBy: { + field: 'updated_by', + type: Sequelize.UUID + }, + createdAt: { + field: 'created_at', + type: Sequelize.DATE, + }, updatedAt: { field: 'updated_at', type: Sequelize.DATE }, - updatedBy: { - field: 'updated_by', - type: Sequelize.UUID + deletedAt: { + field: 'deleted_at', + type: Sequelize.DATE } }, { From a0f0f7a85a828dc2b4e5590fe6c880c9bd18d332 Mon Sep 17 00:00:00 2001 From: imcaizheng Date: Mon, 8 Feb 2021 23:22:16 +0800 Subject: [PATCH 26/46] fix example values for members and invites in Swagger document --- docs/swagger.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index a080782b..4d3f463d 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2099,7 +2099,7 @@ components: items: type: object description: "The invites of the project" - example: [{"userIds": [40152855], "role": "copilot"}] + example: [{"createdAt": "2021-02-08T09:21:00.885Z", "createdBy": 40159127, "deletedBy": null, "email": null, "id": 3008, "projectId": 16819, "role": "customer", "status": "pending", "updatedAt": "2021-02-08T09:21:00.885Z", "updatedBy": 40159127, "userId": 40153913}] members: type: array items: @@ -2194,13 +2194,13 @@ components: items: type: object description: "The invites of the project" - example: [{"userIds": [40152855], "role": "copilot"}] + example: [{"createdAt": "2021-02-08T09:21:00.885Z", "createdBy": 40159127, "deletedBy": null, "email": null, "id": 3008, "projectId": 16819, "role": "customer", "status": "pending", "updatedAt": "2021-02-08T09:21:00.885Z", "updatedBy": 40159127, "userId": 40153913}] members: type: array items: type: object description: "The members of the project" - example: [{"lastName": "J", "role": "manager", "updatedBy": 8547899, "handle": "TonyJ", "userId": 8547899, "deletedBy": null, "createdAt": "2021-01-12T04:34:15.006Z", "firstName": "Tony", "createdBy": 8547899, "isPrimary": true, "id": 13819, "projectId": 16893, "email": "ajefts@topcoder.com", "updatedAt": "2021-01-12T04:34:15.018Z"}] + example: [{"lastName": "L_NAME", "role": "customer", "updatedBy": 21926562, "handle": "Tester123", "userId": 21926562, "deletedBy": null, "createdAt": "2021-01-12T10:58:26.237Z", "firstName": "F_NAME", "createdBy": 21926562, "isPrimary": false, "id": 13833, "projectId": 16893, "email": "email@domain.com.z", "updatedAt": "2021-01-12T10:58:26.237Z"}] startDate: type: string format: date-time From e2e10844cd4980fc167b67aeb2e4782979121964 Mon Sep 17 00:00:00 2001 From: imcaizheng Date: Wed, 10 Feb 2021 03:00:16 +0800 Subject: [PATCH 27/46] Remove `endDate` from Job and add `duration` to Job MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Updated model definition - Updated Joi schemas - Updated Swagger - Added migration script - Fixed existing lint errors - Updated Postman collection The following requests in Postman is updated: * Jobs * create job with booking manager * create job with m2m create * create job with connect user * create job with member success * create job with user id not exist * create job with invalid token * put job with booking manager * put job with m2m update * put job with connect user * put job with member 403 * put job with member success * put job with member with user id not exist * put job with invalid token * patch job with booking manager * patch job with m2m update * patch job with connect user * patch job with member 403 * patch job with member success * patch job with user id not exist * patch job with invalid token * Create Demo Data For Team * Create job #1 "sourcing" * create job #1 * Create job #2 "in-review" * create job #2 * Create job #3 "assigned" * create job #3 * Create job #4 "closed" * create job #4 * Create job #5 "cancelled" * create job #5 * Test Permission Rules * Request with Administrator Role * Jobs * ✔ create job with administrator * ✔ put job with administrator * ✔ patch job with administrator * Request with Topcoder User Role * Jobs * ✔ create job with member * ✔ put job with member * ✔ patch job with member * Request with Connect Manager Role * Jobs * ✔ create job with connect manager * ✔ put job with connect manager * ✔ patch job with connect manager --- ...coder-bookings-api.postman_collection.json | 70 +++++++++---------- docs/swagger.yaml | 45 +++++------- ...2-10-job-replace-end-date-with-duration.js | 18 +++++ src/common/helper.js | 2 +- src/models/Job.js | 8 +-- src/models/JobCandidate.js | 2 +- src/models/ResourceBooking.js | 2 +- src/services/JobService.js | 11 ++- src/services/TeamService.js | 2 +- 9 files changed, 82 insertions(+), 78 deletions(-) create mode 100644 migrations/2021-02-10-job-replace-end-date-with-duration.js diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index 6eb03c1f..5438a757 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -33,7 +33,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -77,7 +77,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -121,7 +121,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -165,7 +165,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -208,7 +208,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"56fdc405-eccc-4189-9e83-c78abf844f50\",\n \"f91ae184-aba2-4485-a8cb-9336988c05ab\",\n \"edfc7b4f-636f-44bd-96fc-949ffc58e38b\",\n \"4ca63bb6-f515-4ab0-a6bc-c2d8531e084f\",\n \"ee03c041-d53b-4c08-b7d9-80d7461da3e4\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"56fdc405-eccc-4189-9e83-c78abf844f50\",\n \"f91ae184-aba2-4485-a8cb-9336988c05ab\",\n \"edfc7b4f-636f-44bd-96fc-949ffc58e38b\",\n \"4ca63bb6-f515-4ab0-a6bc-c2d8531e084f\",\n \"ee03c041-d53b-4c08-b7d9-80d7461da3e4\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -251,7 +251,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"56fdc405-eccc-4189-9e83-c78abf844f50\",\n \"f91ae184-aba2-4485-a8cb-9336988c05ab\",\n \"edfc7b4f-636f-44bd-96fc-949ffc58e38b\",\n \"4ca63bb6-f515-4ab0-a6bc-c2d8531e084f\",\n \"ee03c041-d53b-4c08-b7d9-80d7461da3e4\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"56fdc405-eccc-4189-9e83-c78abf844f50\",\n \"f91ae184-aba2-4485-a8cb-9336988c05ab\",\n \"edfc7b4f-636f-44bd-96fc-949ffc58e38b\",\n \"4ca63bb6-f515-4ab0-a6bc-c2d8531e084f\",\n \"ee03c041-d53b-4c08-b7d9-80d7461da3e4\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -909,7 +909,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -942,7 +942,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -975,7 +975,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n \"cc41ddc4-cacc-4570-9bdb-1229c12b9784\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n \"cc41ddc4-cacc-4570-9bdb-1229c12b9784\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -1021,7 +1021,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description updated\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description updated\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -1054,7 +1054,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description updated\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description updated\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -1087,7 +1087,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n \"cc41ddc4-cacc-4570-9bdb-1229c12b9784\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n \"cc41ddc4-cacc-4570-9bdb-1229c12b9784\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -1120,7 +1120,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n \"cc41ddc4-cacc-4570-9bdb-1229c12b9784\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n \"cc41ddc4-cacc-4570-9bdb-1229c12b9784\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -1153,7 +1153,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -1186,7 +1186,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -1219,7 +1219,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -1265,7 +1265,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"description\": \"Dummy Description updated 2\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"description\": \"Dummy Description updated 2\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -1298,7 +1298,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"description\": \"Dummy Description updated 2\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"description\": \"Dummy Description updated 2\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -1331,7 +1331,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -1364,7 +1364,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"3fa85f64-5717-4562-b3fc-2c963f66afa6\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -4563,7 +4563,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"0\",\n \"description\": \"taas-demo-job1\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"weekly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"ee4c50c1-c8c3-475e-b6b6-edbd136a19d6\",\n \"89139c80-d0a2-47c2-aa16-14589d5afd10\",\n \"9f2d9127-6a2e-4506-ad76-c4ab63577b09\",\n \"9515e7ee-83b6-49d1-ba5c-6c59c5a8ef1b\",\n \"c854ab55-5922-4be1-8ecc-b3bc1f8629af\",\n \"8456002e-fa2d-44f0-b0e7-86b1c02b6e4c\",\n \"114b4ec8-805e-4c60-b351-14a955a991a9\",\n \"213408aa-f16f-46c8-bc57-9e569cee3f11\",\n \"b37a48db-f775-4e4e-b403-8ad1d234cdea\",\n \"99b930b5-1b91-4df1-8b17-d9307107bb51\",\n \"6388a632-c3ad-4525-9a73-66a527c03672\",\n \"23839f38-6f19-4de9-9d28-f020056bca73\",\n \"289e42a3-23e9-49be-88e1-6deb93cd8c31\",\n \"b403f209-63b5-42bc-9b5f-1564416640d8\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"0\",\n \"description\": \"taas-demo-job1\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"weekly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"ee4c50c1-c8c3-475e-b6b6-edbd136a19d6\",\n \"89139c80-d0a2-47c2-aa16-14589d5afd10\",\n \"9f2d9127-6a2e-4506-ad76-c4ab63577b09\",\n \"9515e7ee-83b6-49d1-ba5c-6c59c5a8ef1b\",\n \"c854ab55-5922-4be1-8ecc-b3bc1f8629af\",\n \"8456002e-fa2d-44f0-b0e7-86b1c02b6e4c\",\n \"114b4ec8-805e-4c60-b351-14a955a991a9\",\n \"213408aa-f16f-46c8-bc57-9e569cee3f11\",\n \"b37a48db-f775-4e4e-b403-8ad1d234cdea\",\n \"99b930b5-1b91-4df1-8b17-d9307107bb51\",\n \"6388a632-c3ad-4525-9a73-66a527c03672\",\n \"23839f38-6f19-4de9-9d28-f020056bca73\",\n \"289e42a3-23e9-49be-88e1-6deb93cd8c31\",\n \"b403f209-63b5-42bc-9b5f-1564416640d8\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -4617,7 +4617,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"0\",\n \"description\": \"taas-demo-job2\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 7,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"weekly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"213408aa-f16f-46c8-bc57-9e569cee3f11\",\n \"b37a48db-f775-4e4e-b403-8ad1d234cdea\",\n \"99b930b5-1b91-4df1-8b17-d9307107bb51\",\n \"6388a632-c3ad-4525-9a73-66a527c03672\",\n \"23839f38-6f19-4de9-9d28-f020056bca73\",\n \"289e42a3-23e9-49be-88e1-6deb93cd8c31\",\n \"b403f209-63b5-42bc-9b5f-1564416640d8\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"0\",\n \"description\": \"taas-demo-job2\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 7,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"weekly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"213408aa-f16f-46c8-bc57-9e569cee3f11\",\n \"b37a48db-f775-4e4e-b403-8ad1d234cdea\",\n \"99b930b5-1b91-4df1-8b17-d9307107bb51\",\n \"6388a632-c3ad-4525-9a73-66a527c03672\",\n \"23839f38-6f19-4de9-9d28-f020056bca73\",\n \"289e42a3-23e9-49be-88e1-6deb93cd8c31\",\n \"b403f209-63b5-42bc-9b5f-1564416640d8\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -5267,7 +5267,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"0\",\n \"description\": \"taas-demo-job3\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 7,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"weekly\",\n \"workload\": \"full-time\",\n \"skills\": [],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"0\",\n \"description\": \"taas-demo-job3\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 7,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"weekly\",\n \"workload\": \"full-time\",\n \"skills\": [],\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -6904,7 +6904,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"0\",\n \"description\": \"taas-demo-job4\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 7,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"weekly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"8456002e-fa2d-44f0-b0e7-86b1c02b6e4c\",\n \"114b4ec8-805e-4c60-b351-14a955a991a9\",\n \"213408aa-f16f-46c8-bc57-9e569cee3f11\",\n \"b37a48db-f775-4e4e-b403-8ad1d234cdea\",\n \"99b930b5-1b91-4df1-8b17-d9307107bb51\",\n \"6388a632-c3ad-4525-9a73-66a527c03672\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"0\",\n \"description\": \"taas-demo-job4\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 7,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"weekly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"8456002e-fa2d-44f0-b0e7-86b1c02b6e4c\",\n \"114b4ec8-805e-4c60-b351-14a955a991a9\",\n \"213408aa-f16f-46c8-bc57-9e569cee3f11\",\n \"b37a48db-f775-4e4e-b403-8ad1d234cdea\",\n \"99b930b5-1b91-4df1-8b17-d9307107bb51\",\n \"6388a632-c3ad-4525-9a73-66a527c03672\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -7004,7 +7004,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"0\",\n \"description\": \"taas-demo-job5\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 7,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"weekly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"b37a48db-f775-4e4e-b403-8ad1d234cdea\",\n \"99b930b5-1b91-4df1-8b17-d9307107bb51\",\n \"6388a632-c3ad-4525-9a73-66a527c03672\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"0\",\n \"description\": \"taas-demo-job5\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 7,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"weekly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"b37a48db-f775-4e4e-b403-8ad1d234cdea\",\n \"99b930b5-1b91-4df1-8b17-d9307107bb51\",\n \"6388a632-c3ad-4525-9a73-66a527c03672\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -7107,7 +7107,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -7263,7 +7263,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{projectId}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -7296,7 +7296,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -7874,7 +7874,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{project_id_16718}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{project_id_16718}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -8029,7 +8029,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{project_id_16718}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{project_id_16718}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -8062,7 +8062,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -8741,7 +8741,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{project_id_16843}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{project_id_16843}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"full-time\",\n \"skills\": [\n \"23e00d92-207a-4b5b-b3c9-4c5662644941\",\n \"7d076384-ccf6-4e43-a45d-1b24b1e624aa\",\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -8897,7 +8897,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"projectId\": {{project_id_16843}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"projectId\": {{project_id_16843}},\n \"externalId\": \"1212\",\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\",\n \"a2b4bc11-c641-4a19-9eb7-33980378f82e\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -8930,7 +8930,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"endDate\": \"2020-09-27T04:17:23.131Z\",\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", + "raw": "{\n \"description\": \"Dummy Description\",\n \"startDate\": \"2020-09-27T04:17:23.131Z\",\n \"duration\": 1,\n \"numPositions\": 13,\n \"resourceType\": \"Dummy Resource Type\",\n \"rateType\": \"hourly\",\n \"workload\": \"fractional\",\n \"skills\": [\n \"cbac57a3-7180-4316-8769-73af64893158\"\n ],\n \"status\": \"sourcing\",\n \"title\": \"Dummy title - at most 64 characters\"\n}", "options": { "raw": { "language": "json" @@ -9562,4 +9562,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 4d3f463d..103be9ec 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -91,7 +91,7 @@ paths: schema: type: string default: id - enum: ['id','createdAt','startDate','endDate','rateType','status'] + enum: ['id','createdAt','startDate','rateType','status'] description: The sort by column. - in: query name: sortOrder @@ -139,13 +139,6 @@ paths: type: string format: date-time description: The job start date. - - in: query - name: endDate - required: false - schema: - type: string - format: date-time - description: The job end date. - in: query name: resourceType required: false @@ -1683,11 +1676,10 @@ components: 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." + duration: + type: integer + example: 1 + description: "The duration in weeks" numPositions: type: integer example: 13 @@ -1770,11 +1762,10 @@ components: 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." + duration: + type: integer + example: 1 + description: "The duration in weeks" numPositions: type: integer example: 13 @@ -1900,11 +1891,10 @@ components: 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." + duration: + type: integer + example: 1 + description: "The duration in weeks" numPositions: type: integer example: 13 @@ -2315,11 +2305,10 @@ components: 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." + duration: + type: integer + example: 1 + description: "The duration in weeks" numPositions: type: integer example: 13 diff --git a/migrations/2021-02-10-job-replace-end-date-with-duration.js b/migrations/2021-02-10-job-replace-end-date-with-duration.js new file mode 100644 index 00000000..d7e81096 --- /dev/null +++ b/migrations/2021-02-10-job-replace-end-date-with-duration.js @@ -0,0 +1,18 @@ +/* + * Replace endData with duration in Job model. + */ + +module.exports = { + up: queryInterface => { + return Promise.all([ + queryInterface.sequelize.query('ALTER TABLE bookings.jobs DROP end_date'), + queryInterface.sequelize.query('ALTER TABLE bookings.jobs ADD duration INTEGER') + ]) + }, + down: queryInterface => { + return Promise.all([ + queryInterface.sequelize.query('ALTER TABLE bookings.jobs ADD end_date DATE'), + queryInterface.sequelize.query('ALTER TABLE bookings.jobs DROP duration') + ]) + } +} diff --git a/src/common/helper.js b/src/common/helper.js index e05dfc18..5ac0b6dd 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -60,7 +60,7 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_JOB')] = { description: { type: 'text' }, title: { type: 'text' }, startDate: { type: 'date' }, - endDate: { type: 'date' }, + duration: { type: 'integer' }, numPositions: { type: 'integer' }, resourceType: { type: 'keyword' }, rateType: { type: 'keyword' }, diff --git a/src/models/Job.js b/src/models/Job.js index 388e33bb..c124caec 100644 --- a/src/models/Job.js +++ b/src/models/Job.js @@ -69,9 +69,9 @@ module.exports = (sequelize) => { field: 'start_date', type: Sequelize.DATE }, - endDate: { - field: 'end_date', - type: Sequelize.DATE + duration: { + field: 'duration', + type: Sequelize.INTEGER }, numPositions: { field: 'num_positions', @@ -109,7 +109,7 @@ module.exports = (sequelize) => { }, createdAt: { field: 'created_at', - type: Sequelize.DATE, + type: Sequelize.DATE }, updatedAt: { field: 'updated_at', diff --git a/src/models/JobCandidate.js b/src/models/JobCandidate.js index 1846569d..00e83991 100644 --- a/src/models/JobCandidate.js +++ b/src/models/JobCandidate.js @@ -70,7 +70,7 @@ module.exports = (sequelize) => { }, createdAt: { field: 'created_at', - type: Sequelize.DATE, + type: Sequelize.DATE }, updatedAt: { field: 'updated_at', diff --git a/src/models/ResourceBooking.js b/src/models/ResourceBooking.js index df861a89..2b6b0b7f 100644 --- a/src/models/ResourceBooking.js +++ b/src/models/ResourceBooking.js @@ -87,7 +87,7 @@ module.exports = (sequelize) => { }, createdAt: { field: 'created_at', - type: Sequelize.DATE, + type: Sequelize.DATE }, updatedAt: { field: 'updated_at', diff --git a/src/services/JobService.js b/src/services/JobService.js index 00167e03..9f6fdd80 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -166,7 +166,7 @@ createJob.schema = Joi.object().keys({ description: Joi.stringAllowEmpty().allow(null), title: Joi.title().required(), startDate: Joi.date().allow(null), - endDate: Joi.date().allow(null), + duration: Joi.number().integer().min(1).allow(null), numPositions: Joi.number().integer().min(1).required(), resourceType: Joi.stringAllowEmpty().allow(null), rateType: Joi.rateType().allow(null), @@ -227,7 +227,7 @@ partiallyUpdateJob.schema = Joi.object().keys({ description: Joi.stringAllowEmpty().allow(null), title: Joi.title(), startDate: Joi.date().allow(null), - endDate: Joi.date().allow(null), + duration: Joi.number().integer().min(1).allow(null), numPositions: Joi.number().integer().min(1), resourceType: Joi.stringAllowEmpty().allow(null), rateType: Joi.rateType().allow(null), @@ -256,7 +256,7 @@ fullyUpdateJob.schema = Joi.object().keys({ description: Joi.stringAllowEmpty().allow(null), title: Joi.title().required(), startDate: Joi.date().allow(null), - endDate: Joi.date().allow(null), + duration: Joi.number().integer().min(1).allow(null), numPositions: Joi.number().integer().min(1).required(), resourceType: Joi.stringAllowEmpty().allow(null), rateType: Joi.rateType().allow(null), @@ -344,7 +344,6 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } 'externalId', 'description', 'startDate', - 'endDate', 'resourceType', 'skill', 'rateType', @@ -414,7 +413,6 @@ async function searchJobs (currentUser, criteria, options = { returnAll: false } 'projectId', 'externalId', 'startDate', - 'endDate', 'resourceType', 'rateType', 'workload', @@ -462,14 +460,13 @@ searchJobs.schema = Joi.object().keys({ criteria: Joi.object().keys({ page: Joi.number().integer(), perPage: Joi.number().integer(), - sortBy: Joi.string().valid('id', 'createdAt', 'startDate', 'endDate', 'rateType', 'status'), + sortBy: Joi.string().valid('id', 'createdAt', 'startDate', 'rateType', 'status'), sortOrder: Joi.string().valid('desc', 'asc'), projectId: Joi.number().integer(), externalId: Joi.string(), description: Joi.string(), title: Joi.title(), startDate: Joi.date(), - endDate: Joi.date(), resourceType: Joi.string(), skill: Joi.string().uuid(), rateType: Joi.rateType(), diff --git a/src/services/TeamService.js b/src/services/TeamService.js index f27b2260..b528aacf 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -188,7 +188,7 @@ async function getTeamDetail (currentUser, projects, isSearch = true) { } } else { res.jobs = _.map(jobsTmp, job => { - return _.pick(job, ['id', 'description', 'startDate', 'endDate', 'numPositions', 'rateType', 'skills', 'customerRate', 'status', 'title']) + return _.pick(job, ['id', 'description', 'startDate', 'duration', 'numPositions', 'rateType', 'skills', 'customerRate', 'status', 'title']) }) } } From 331a855b76962ba3ebea73890bfe1a8e1ab45614 Mon Sep 17 00:00:00 2001 From: imcaizheng Date: Wed, 10 Feb 2021 03:46:27 +0800 Subject: [PATCH 28/46] Remove the endDate parameter from searching jobs in Postman --- ...coder-bookings-api.postman_collection.json | 40 ------------------- 1 file changed, 40 deletions(-) diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index 5438a757..367b11aa 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -456,11 +456,6 @@ "value": "2020-09-27T04:17:23.131Z", "disabled": true }, - { - "key": "endDate", - "value": "2020-09-27T04:17:23.131Z", - "disabled": true - }, { "key": "resourceType", "value": "Dummy Resource Type", @@ -556,11 +551,6 @@ "value": "2020-09-27T04:17:23.131Z", "disabled": true }, - { - "key": "endDate", - "value": "2020-09-27T04:17:23.131Z", - "disabled": true - }, { "key": "resourceType", "value": "Dummy Resource Type", @@ -656,11 +646,6 @@ "value": "2020-09-27T04:17:23.131Z", "disabled": true }, - { - "key": "endDate", - "value": "2020-09-27T04:17:23.131Z", - "disabled": true - }, { "key": "resourceType", "value": "Dummy Resource Type", @@ -756,11 +741,6 @@ "value": "2020-09-27T04:17:23.131Z", "disabled": true }, - { - "key": "endDate", - "value": "2020-09-27T04:17:23.131Z", - "disabled": true - }, { "key": "resourceType", "value": "Dummy Resource Type", @@ -856,11 +836,6 @@ "value": "2020-09-27T04:17:23.131Z", "disabled": true }, - { - "key": "endDate", - "value": "2020-09-27T04:17:23.131Z", - "disabled": true - }, { "key": "resourceType", "value": "Dummy Resource Type", @@ -7210,11 +7185,6 @@ "value": "2020-09-27T04:17:23.131Z", "disabled": true }, - { - "key": "endDate", - "value": "2020-09-27T04:17:23.131Z", - "disabled": true - }, { "key": "resourceType", "value": "Dummy Resource Type", @@ -7976,11 +7946,6 @@ "value": "2020-09-27T04:17:23.131Z", "disabled": true }, - { - "key": "endDate", - "value": "2020-09-27T04:17:23.131Z", - "disabled": true - }, { "key": "resourceType", "value": "Dummy Resource Type", @@ -8844,11 +8809,6 @@ "value": "2020-09-27T04:17:23.131Z", "disabled": true }, - { - "key": "endDate", - "value": "2020-09-27T04:17:23.131Z", - "disabled": true - }, { "key": "resourceType", "value": "Dummy Resource Type", From 1d19d13faa6816b2c396e0cea589e1a8c581f940 Mon Sep 17 00:00:00 2001 From: imcaizheng Date: Wed, 10 Feb 2021 07:12:42 +0800 Subject: [PATCH 29/46] Fix PUT operations: fully replace the content of a record with a new one --- docs/swagger.yaml | 1 - src/services/JobCandidateService.js | 6 +++--- src/services/JobService.js | 16 ++++++++-------- src/services/ResourceBookingService.js | 12 ++++++------ 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 4d3f463d..03848eab 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2003,7 +2003,6 @@ components: required: - projectId - userId - - status - rateType properties: projectId: diff --git a/src/services/JobCandidateService.js b/src/services/JobCandidateService.js index e4d4f92a..0dec8a18 100644 --- a/src/services/JobCandidateService.js +++ b/src/services/JobCandidateService.js @@ -173,9 +173,9 @@ fullyUpdateJobCandidate.schema = Joi.object().keys({ data: Joi.object().keys({ jobId: Joi.string().uuid().required(), userId: Joi.string().uuid().required(), - status: Joi.jobCandidateStatus(), - externalId: Joi.string().allow(null), - resume: Joi.string().uri().allow(null) + status: Joi.jobCandidateStatus().default('open'), + externalId: Joi.string().allow(null).default(null), + resume: Joi.string().uri().allow(null).default(null) }).required() }).required() diff --git a/src/services/JobService.js b/src/services/JobService.js index 00167e03..5f2f3f01 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -252,17 +252,17 @@ fullyUpdateJob.schema = Joi.object().keys({ id: Joi.string().guid().required(), data: Joi.object().keys({ projectId: Joi.number().integer().required(), - externalId: Joi.string().allow(null), - description: Joi.stringAllowEmpty().allow(null), + externalId: Joi.string().allow(null).default(null), + description: Joi.stringAllowEmpty().allow(null).default(null), title: Joi.title().required(), - startDate: Joi.date().allow(null), - endDate: Joi.date().allow(null), + startDate: Joi.date().allow(null).default(null), + endDate: Joi.date().allow(null).default(null), numPositions: Joi.number().integer().min(1).required(), - resourceType: Joi.stringAllowEmpty().allow(null), - rateType: Joi.rateType().allow(null), - workload: Joi.workload().allow(null), + resourceType: Joi.stringAllowEmpty().allow(null).default(null), + rateType: Joi.rateType().allow(null).default(null), + workload: Joi.workload().allow(null).default(null), skills: Joi.array().items(Joi.string().uuid()).required(), - status: Joi.jobStatus() + status: Joi.jobStatus().default('sourcing') }).required() }).required() diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index a29b3445..120b9539 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -194,13 +194,13 @@ fullyUpdateResourceBooking.schema = Joi.object().keys({ data: Joi.object().keys({ projectId: Joi.number().integer().required(), userId: Joi.string().uuid().required(), - jobId: Joi.string().uuid().allow(null), - startDate: Joi.date().allow(null), - endDate: Joi.date().allow(null), - memberRate: Joi.number().allow(null), - customerRate: Joi.number().allow(null), + jobId: Joi.string().uuid().allow(null).default(null), + startDate: Joi.date().allow(null).default(null), + endDate: Joi.date().allow(null).default(null), + memberRate: Joi.number().allow(null).default(null), + customerRate: Joi.number().allow(null).default(null), rateType: Joi.rateType().required(), - status: Joi.jobStatus().required() + status: Joi.jobStatus().default('sourcing') }).required() }).required() From 1a5d2deadc229ed4827abdf575f14ae9f7363a53 Mon Sep 17 00:00:00 2001 From: imcaizheng Date: Mon, 15 Feb 2021 15:29:33 +0800 Subject: [PATCH 30/46] add new endpoint `POST /taas-teams/email` --- config/default.js | 11 ++- config/email_template.config.js | 47 ++++++++++++ ...coder-bookings-api.postman_collection.json | 76 +++++++++++++++++++ docs/swagger.yaml | 51 +++++++++++++ local/kafka-client/topics.txt | 3 +- package-lock.json | 35 +++++++++ package.json | 1 + src/controllers/TeamController.js | 14 +++- src/routes/TeamRoutes.js | 8 ++ src/services/TeamService.js | 39 +++++++++- 10 files changed, 281 insertions(+), 4 deletions(-) create mode 100644 config/email_template.config.js diff --git a/config/default.js b/config/default.js index afda7a7b..67165603 100644 --- a/config/default.js +++ b/config/default.js @@ -105,5 +105,14 @@ module.exports = { // the update resource booking entity Kafka message topic TAAS_RESOURCE_BOOKING_UPDATE_TOPIC: process.env.TAAS_RESOURCE_BOOKING_UPDATE_TOPIC || 'taas.resourcebooking.update', // the delete resource booking entity Kafka message topic - TAAS_RESOURCE_BOOKING_DELETE_TOPIC: process.env.TAAS_RESOURCE_BOOKING_DELETE_TOPIC || 'taas.resourcebooking.delete' + TAAS_RESOURCE_BOOKING_DELETE_TOPIC: process.env.TAAS_RESOURCE_BOOKING_DELETE_TOPIC || 'taas.resourcebooking.delete', + + // the Kafka message topic for sending email + EMAIL_TOPIC: process.env.EMAIL_TOPIC || 'external.action.email', + // the email address for receiving the issue report + REPORT_ISSUE_EMAIL: process.env.REPORT_ISSUE_EMAIL || 'test@gtest.com', + // SendGrid email template ID for reporting issue + REPORT_ISSUE_SENDGRID_TEMPLATE_ID: process.env.REPORT_ISSUE_SENDGRID_TEMPLATE_ID || 'aaaaaaa-bbbb-cccc-dddd-asdfsdfsdfsd', + // the URL where TaaS App is hosted + TAAS_APP_URL: process.env.TAAS_APP_URL || 'https://platform.topcoder-dev.com/taas/myteams' } diff --git a/config/email_template.config.js b/config/email_template.config.js new file mode 100644 index 00000000..ed251a5d --- /dev/null +++ b/config/email_template.config.js @@ -0,0 +1,47 @@ +/* + * Configure email templates. + * Variables can be used inside the subject and the message of a template(enclosed in double curly braces). + */ + +const config = require('config') + +module.exports = { + /* Report a general issue for a team. + * + * - projectId: the project ID. Example: 123412 + * - projectName: the project name. Example: "TaaS API Misc Updates" + * - reportText: the body of reported issue. Example: "I have issue with ... \n ... Thank you in advance!" + */ + 'team-issue-report': { + subject: 'Issue Reported on TaaS Team {{projectName}} ({{projectId}}).', + message: 'Project Name: {{projectName}}' + '\n' + + 'Project ID: {{projectId}}' + '\n' + + `Project URL: ${config.TAAS_APP_URL}/{{projectId}}` + '\n' + + '\n' + + '{{reportText}}', + recipients: [ + config.REPORT_ISSUE_EMAIL + ], + sendgridTemplateId: config.REPORT_ISSUE_SENDGRID_TEMPLATE_ID + }, + /* Report issue for a particular member + * + * - userHandle: the user handle. Example: "bili_2021" + * - projectId: the project ID. Example: 123412 + * - projectName: the project name. Example: "TaaS API Misc Updates" + * - reportText: the body of reported issue. Example: "I have issue with ... \n ... Thank you in advance!" + */ + 'member-issue-report': { + subject: 'Issue Reported for member {{userHandle}} on TaaS Team {{projectName}} ({{projectId}}).', + message: 'User Handle: {{userHandle}}' + '\n' + + 'Project Name: {{projectName}}' + '\n' + + 'Project ID: {{projectId}}' + '\n' + + `Project URL: ${config.TAAS_APP_URL}/{{projectId}}` + '\n' + + '\n' + + '{{reportText}}', + recipients: [ + config.REPORT_ISSUE_EMAIL + ], + sendgridTemplateId: config.REPORT_ISSUE_SENDGRID_TEMPLATE_ID + } +} diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index 367b11aa..a13896de 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -4406,6 +4406,82 @@ } }, "response": [] + }, + { + "name": "POST /taas-teams/email - team-issue-report", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token_member}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"template\": \"team-issue-report\",\n \"data\": {\n \"projectName\": \"TaaS Project Name\",\n \"projectId\": 12345,\n \"reportText\": \"I have issue with ...\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/email", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "email" + ] + } + }, + "response": [] + }, + { + "name": "POST /taas-teams/email - member-issue-report", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_member}}" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"template\": \"member-issue-report\",\n \"data\": {\n \"projectName\": \"TaaS Project Name\",\n \"projectId\": 12345,\n \"userHandle\": \"pshah_manager\",\n \"reportText\": \"I have issue with ...\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/email", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "email" + ] + } + }, + "response": [] } ] }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index fb778c98..b28b57d1 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1613,6 +1613,46 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /taas-teams/email: + post: + tags: + - Teams + description: | + Send emails through one of predefined templates. + security: + - bearerAuth: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TeamEmailRequestBody' + responses: + '204': + description: OK + '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: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' /health: get: tags: @@ -2368,6 +2408,17 @@ components: type: array items: $ref: '#/components/schemas/Skill' + TeamEmailRequestBody: + type: object + properties: + template: + type: string + description: "the email template name" + example: "team-issue-report" + data: + type: object + example: {"projectName": "TaaS Project Name", "projectId": 12345, "reportText": "I have issue with ..."} + description: "Arbitrary data to feed the specified template" Error: required: - message diff --git a/local/kafka-client/topics.txt b/local/kafka-client/topics.txt index 6888c6bd..7bf08bc8 100644 --- a/local/kafka-client/topics.txt +++ b/local/kafka-client/topics.txt @@ -6,4 +6,5 @@ taas.jobcandidate.update taas.resourcebooking.update taas.job.delete taas.jobcandidate.delete -taas.resourcebooking.delete \ No newline at end of file +taas.resourcebooking.delete +external.action.email diff --git a/package-lock.json b/package-lock.json index 52f6a14f..ec16c9c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3197,6 +3197,25 @@ "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", "dev": true }, + "handlebars": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", + "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -4940,6 +4959,11 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, "next-tick": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", @@ -7383,6 +7407,12 @@ "is-typedarray": "^1.0.0" } }, + "uglify-js": { + "version": "3.12.8", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.12.8.tgz", + "integrity": "sha512-fvBeuXOsvqjecUtF/l1dwsrrf5y2BCUk9AOJGzGcm6tE7vegku5u/YvqjyDaAGr422PLoLnrxg3EnRvTqsdC1w==", + "optional": true + }, "umzug": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz", @@ -7780,6 +7810,11 @@ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" + }, "workerpool": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.0.0.tgz", diff --git a/package.json b/package.json index c7b71abe..39747993 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "express": "^4.17.1", "express-interceptor": "^1.2.0", "get-parameter-names": "^0.3.0", + "handlebars": "^4.7.6", "http-status": "^1.4.2", "http-status-codes": "^2.1.4", "joi": "^17.2.1", diff --git a/src/controllers/TeamController.js b/src/controllers/TeamController.js index 9e1f7946..d15fb131 100644 --- a/src/controllers/TeamController.js +++ b/src/controllers/TeamController.js @@ -1,6 +1,7 @@ /** * Controller for TaaS teams endpoints */ +const HttpStatus = require('http-status-codes') const service = require('../services/TeamService') const helper = require('../common/helper') @@ -33,8 +34,19 @@ async function getTeamJob (req, res) { res.send(await service.getTeamJob(req.authUser, req.params.id, req.params.jobId)) } +/** + * Send email through a particular template + * @param req the request + * @param res the response + */ +async function sendEmail (req, res) { + await service.sendEmail(req.body) + res.status(HttpStatus.NO_CONTENT).end() +} + module.exports = { searchTeams, getTeam, - getTeamJob + getTeamJob, + sendEmail } diff --git a/src/routes/TeamRoutes.js b/src/routes/TeamRoutes.js index 7cfff792..b4c8b0d2 100644 --- a/src/routes/TeamRoutes.js +++ b/src/routes/TeamRoutes.js @@ -12,6 +12,14 @@ module.exports = { scopes: [constants.Scopes.READ_TAAS_TEAM] } }, + '/taas-teams/email': { + post: { + controller: 'TeamController', + method: 'sendEmail', + auth: 'jwt', + scopes: [constants.Scopes.READ_TAAS_TEAM] + } + }, '/taas-teams/skills': { get: { controller: 'SkillController', diff --git a/src/services/TeamService.js b/src/services/TeamService.js index b528aacf..4071e9eb 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -5,12 +5,24 @@ const _ = require('lodash') const Joi = require('joi') const dateFNS = require('date-fns') +const Handlebars = require('handlebars') +const config = require('config') +const emailTemplateConfig = require('../../config/email_template.config') const helper = require('../common/helper') const logger = require('../common/logger') const errors = require('../common/errors') const JobService = require('./JobService') const ResourceBookingService = require('./ResourceBookingService') +const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { + return { + subjectTemplate: Handlebars.compile(template.subject), + messageTemplate: Handlebars.compile(template.message), + recipients: template.recipients, + sendgridTemplateId: template.sendgridTemplateId + } +}) + /** * Function to get assigned resource bookings with specific projectIds * @param {Object} currentUser the user who perform this operation. @@ -296,8 +308,33 @@ getTeamJob.schema = Joi.object().keys({ jobId: Joi.string().guid().required() }).required() +/** + * Send email through a particular template + * @param {Object} data the email object + * @returns {undefined} + */ +async function sendEmail (data) { + const template = emailTemplates[data.template] + await helper.postEvent(config.EMAIL_TOPIC, { + subject: template.subjectTemplate(data.data), + handle: data.data.userHandle, + message: template.messageTemplate(data.data), + sendgrid_template_id: template.sendgridTemplateId, + version: 'v3', + recipients: template.recipients + }) +} + +sendEmail.schema = Joi.object().keys({ + data: Joi.object().keys({ + template: Joi.string().valid(...Object.keys(emailTemplates)).required(), + data: Joi.object().required() + }).required() +}).required() + module.exports = { searchTeams, getTeam, - getTeamJob + getTeamJob, + sendEmail } From 929ae69d126c5590d5a60e380dba973b4fe9fea3 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Mon, 15 Feb 2021 15:00:32 +0200 Subject: [PATCH 31/46] fix: email Kafka Event Payload - use "data" propery - use "handle" of user who performs action --- src/controllers/TeamController.js | 2 +- src/services/TeamService.js | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/controllers/TeamController.js b/src/controllers/TeamController.js index d15fb131..c20bdf5d 100644 --- a/src/controllers/TeamController.js +++ b/src/controllers/TeamController.js @@ -40,7 +40,7 @@ async function getTeamJob (req, res) { * @param res the response */ async function sendEmail (req, res) { - await service.sendEmail(req.body) + await service.sendEmail(req.authUser, req.body) res.status(HttpStatus.NO_CONTENT).end() } diff --git a/src/services/TeamService.js b/src/services/TeamService.js index 4071e9eb..803066e3 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -310,15 +310,18 @@ getTeamJob.schema = Joi.object().keys({ /** * Send email through a particular template + * @param {Object} currentUser the user who perform this operation * @param {Object} data the email object * @returns {undefined} */ -async function sendEmail (data) { +async function sendEmail (currentUser, data) { const template = emailTemplates[data.template] await helper.postEvent(config.EMAIL_TOPIC, { - subject: template.subjectTemplate(data.data), - handle: data.data.userHandle, - message: template.messageTemplate(data.data), + data: { + handle: currentUser.handle, + subject: template.subjectTemplate(data.data), + message: template.messageTemplate(data.data), + }, sendgrid_template_id: template.sendgridTemplateId, version: 'v3', recipients: template.recipients @@ -326,6 +329,7 @@ async function sendEmail (data) { } sendEmail.schema = Joi.object().keys({ + currentUser: Joi.object().required(), data: Joi.object().keys({ template: Joi.string().valid(...Object.keys(emailTemplates)).required(), data: Joi.object().required() From a1ed7e1ca32a2ee6843382ac619394576552914b Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Mon, 15 Feb 2021 15:00:45 +0200 Subject: [PATCH 32/46] chore: use any Node 12 version --- .nvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nvmrc b/.nvmrc index b06cd07c..48082f72 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -12.18.0 +12 From 8ec5de1d53f00cd6371d8009bee75b02bd4fbea1 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Mon, 15 Feb 2021 15:03:19 +0200 Subject: [PATCH 33/46] chore: remove not real config values --- config/default.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/default.js b/config/default.js index 67165603..3c88f71e 100644 --- a/config/default.js +++ b/config/default.js @@ -110,9 +110,9 @@ module.exports = { // the Kafka message topic for sending email EMAIL_TOPIC: process.env.EMAIL_TOPIC || 'external.action.email', // the email address for receiving the issue report - REPORT_ISSUE_EMAIL: process.env.REPORT_ISSUE_EMAIL || 'test@gtest.com', + REPORT_ISSUE_EMAIL: process.env.REPORT_ISSUE_EMAIL, // SendGrid email template ID for reporting issue - REPORT_ISSUE_SENDGRID_TEMPLATE_ID: process.env.REPORT_ISSUE_SENDGRID_TEMPLATE_ID || 'aaaaaaa-bbbb-cccc-dddd-asdfsdfsdfsd', + REPORT_ISSUE_SENDGRID_TEMPLATE_ID: process.env.REPORT_ISSUE_SENDGRID_TEMPLATE_ID, // the URL where TaaS App is hosted TAAS_APP_URL: process.env.TAAS_APP_URL || 'https://platform.topcoder-dev.com/taas/myteams' } From fbcc1af07277df92dbb7587b10cda97d7192482f Mon Sep 17 00:00:00 2001 From: imcaizheng Date: Fri, 19 Feb 2021 15:51:02 +0800 Subject: [PATCH 34/46] add new endpoint POST /taas-teams/:id/members --- app.js | 4 +- config/default.js | 4 + ...coder-bookings-api.postman_collection.json | 47 +++++++- docs/swagger.yaml | 106 ++++++++++++++++++ package-lock.json | 5 + package.json | 1 + src/common/helper.js | 89 ++++++++++++++- src/controllers/TeamController.js | 12 +- src/routes/TeamRoutes.js | 8 ++ src/services/TeamService.js | 93 ++++++++++++++- 10 files changed, 362 insertions(+), 7 deletions(-) diff --git a/app.js b/app.js index 56521d9a..bc37cd0a 100644 --- a/app.js +++ b/app.js @@ -75,8 +75,8 @@ app.use((err, req, res, next) => { } if (err.response) { - // extract error message from V3 API - errorResponse.message = _.get(err, 'response.body.result.content') + // extract error message from V3/V5 API + errorResponse.message = _.get(err, 'response.body.result.content') || _.get(err, 'response.body.message') } if (_.isUndefined(errorResponse.message)) { diff --git a/config/default.js b/config/default.js index 3c88f71e..06b08c4f 100644 --- a/config/default.js +++ b/config/default.js @@ -39,6 +39,10 @@ module.exports = { TOPCODER_SKILL_PROVIDER_ID: process.env.TOPCODER_SKILL_PROVIDER_ID || '9cc0795a-6e12-4c84-9744-15858dba1861', TOPCODER_USERS_API: process.env.TOPCODER_USERS_API || 'https://api.topcoder-dev.com/v3/users', + // the api to find topcoder members + TOPCODER_MEMBERS_API: process.env.TOPCODER_MEMBERS_API || 'https://api.topcoder-dev.com/v3/members', + // rate limit of requests to user api + MAX_PARALLEL_REQUEST_TOPCODER_USERS_API: process.env.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API || 100, // PostgreSQL database url. DATABASE_URL: process.env.DATABASE_URL || 'postgres://postgres:postgres@localhost:5432/postgres', diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index a13896de..a3cc1b66 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -4482,7 +4482,52 @@ } }, "response": [] - } + }, + { + "name": "POST /taas-teams/:id/members", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"handles\": [\n \"tester1234\",\n \"non-existing\"\n ],\n \"emails\": [\n \"non-existing@domain.com\",\n \"email@domain.com\"\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/taas-teams/:id/members", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + ":id", + "members" + ], + "variable": [ + { + "key": "id", + "value": "16705" + } + ] + } + }, + "response": [] + } ] }, { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index b28b57d1..4c042bab 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1523,6 +1523,63 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /taas-teams/{id}/members: + post: + tags: + - Teams + description: | + Add members to a team by handle or email. + security: + - bearerAuth: [] + parameters: + - in: path + name: id + required: true + schema: + type: integer + description: The team/project id. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AddMembersRequestBody' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/AddMembersResponseBody' + '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' + '404': + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' /taas-teams/skills: get: tags: @@ -2419,6 +2476,55 @@ components: type: object example: {"projectName": "TaaS Project Name", "projectId": 12345, "reportText": "I have issue with ..."} description: "Arbitrary data to feed the specified template" + AddMembersRequestBody: + properties: + handles: + type: array + description: "The handles." + items: + type: string + description: "the handle of a member" + example: topcoder321 + emails: + type: array + description: "The emails." + items: + type: string + description: "the email of a member" + example: 'xxx@xxx.com' + AddMembersResponseBody: + properties: + success: + type: array + description: "The handles." + items: + type: object + example: {"createdAt": "2021-02-18T19:58:50.610Z", "createdBy": -101, "email": "email@domain.com", "handle": "Scud", "id": 14155, "photoURL": "https://topcoder-dev-media.s3.amazonaws.com/member/profile/Scud-1450982908556.png", "role": "customer", "timeZone": null, "updatedAt": "2021-02-18T19:58:50.611Z", "updatedBy": -101, "userId": 1800091, "workingHourEnd": null, "workingHourStart": null} + failed: + type: array + description: "The emails." + items: + oneOf: + - type: object + properties: + error: + type: string + description: the error message + example: "User doesn't exist" + handle: + type: string + description: "the handle of a member" + example: topcoder321 + - type: object + properties: + error: + type: string + description: the error message + example: "User is already added" + email: + type: string + description: "the email of a member" + example: 'xxx@xxx.com' Error: required: - message diff --git a/package-lock.json b/package-lock.json index ec16c9c6..97150812 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1324,6 +1324,11 @@ "type-is": "~1.6.17" } }, + "bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==" + }, "boxen": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", diff --git a/package.json b/package.json index 39747993..23621ca5 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@elastic/elasticsearch": "^7.9.1", "@topcoder-platform/topcoder-bus-api-wrapper": "github:topcoder-platform/tc-bus-api-wrapper", "aws-sdk": "^2.787.0", + "bottleneck": "^2.19.5", "config": "^3.3.2", "cors": "^2.8.5", "date-fns": "^2.16.1", diff --git a/src/common/helper.js b/src/common/helper.js index 5ac0b6dd..10fc5e19 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -5,6 +5,7 @@ const fs = require('fs') const querystring = require('querystring') const Confirm = require('prompt-confirm') +const Bottleneck = require('bottleneck') const AWS = require('aws-sdk') const config = require('config') const HttpStatus = require('http-status-codes') @@ -968,6 +969,89 @@ async function checkIsMemberOfProject (userId, projectId) { } } +/** + * Find topcoder members by handles. + * + * @param {Array} handles the array of handles + * @returns {Array} the member details + */ +async function getMemberDetailsByHandles (handles) { + if (!handles.length) { + return [] + } + const token = await getM2MToken() + const res = await request + .get(`${config.TOPCODER_MEMBERS_API}/_search`) + .query({ + query: _.map(handles, handle => `handleLower:${handle.toLowerCase()}`).join(' OR '), + fields: 'userId,handle,firstName,lastName,email' + }) + .set('Authorization', `Bearer ${token}`) + .set('Accept', 'application/json') + localLogger.debug({ context: 'getMemberDetailsByHandles', message: `response body: ${JSON.stringify(res.body)}` }) + return _.get(res.body, 'result.content') +} + +/** + * Find topcoder members by email. + * + * @param {String} token the auth token + * @param {String} email the email + * @returns {Array} the member details + */ +async function _getMemberDetailsByEmail (token, email) { + const res = await request + .get(config.TOPCODER_USERS_API) + .query({ + filter: `email=${email}`, + fields: 'handle,id,email' + }) + .set('Authorization', `Bearer ${token}`) + .set('Accept', 'application/json') + localLogger.debug({ context: '_getMemberDetailsByEmail', message: `response body: ${JSON.stringify(res.body)}` }) + return _.get(res.body, 'result.content') +} + +/** + * Find topcoder members by emails. + * Maximum concurrent requests is limited by MAX_PARALLEL_REQUEST_TOPCODER_USERS_API. + * + * @param {Array} emails the array of emails + * @returns {Array} the member details + */ +async function getMemberDetailsByEmails (emails) { + const token = await getM2MToken() + const limiter = new Bottleneck({ maxConcurrent: config.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API }) + const membersArray = await Promise.all(emails.map(email => limiter.schedule(() => _getMemberDetailsByEmail(token, email) + .catch(() => { + localLogger.error({ context: 'getMemberDetailsByEmails', message: `email: ${email} user not found` }) + return [] + }) + ))) + return _.flatten(membersArray) +} + +/** + * Add a member to a project. + * + * @param {Number} projectId project id + * @param {Object} data the userId and the role of the member + * @param {Object} criteria the filtering criteria + * @returns {Object} the member created + */ +async function createProjectMember (projectId, data, criteria) { + const m2mToken = await getM2MToken() + const { body: member } = await request + .post(`${config.TC_API}/projects/${projectId}/members`) + .set('Authorization', `Bearer ${m2mToken}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .query(criteria) + .send(data) + localLogger.debug({ context: 'createProjectMember', message: `response body: ${JSON.stringify(member)}` }) + return member +} + module.exports = { getParamFromCliArgs, promptUser, @@ -1002,5 +1086,8 @@ module.exports = { ensureJobById, ensureUserById, getAuditM2Muser, - checkIsMemberOfProject + checkIsMemberOfProject, + getMemberDetailsByHandles, + getMemberDetailsByEmails, + createProjectMember } diff --git a/src/controllers/TeamController.js b/src/controllers/TeamController.js index c20bdf5d..c60698a2 100644 --- a/src/controllers/TeamController.js +++ b/src/controllers/TeamController.js @@ -44,9 +44,19 @@ async function sendEmail (req, res) { res.status(HttpStatus.NO_CONTENT).end() } +/** + * Add members to a team. + * @param req the request + * @param res the response + */ +async function addMembers (req, res) { + res.send(await service.addMembers(req.authUser, req.params.id, req.body)) +} + module.exports = { searchTeams, getTeam, getTeamJob, - sendEmail + sendEmail, + addMembers } diff --git a/src/routes/TeamRoutes.js b/src/routes/TeamRoutes.js index b4c8b0d2..0fd06402 100644 --- a/src/routes/TeamRoutes.js +++ b/src/routes/TeamRoutes.js @@ -43,5 +43,13 @@ module.exports = { auth: 'jwt', scopes: [constants.Scopes.READ_TAAS_TEAM] } + }, + '/taas-teams/:id/members': { + post: { + controller: 'TeamController', + method: 'addMembers', + auth: 'jwt', + scopes: [constants.Scopes.READ_TAAS_TEAM] + } } } diff --git a/src/services/TeamService.js b/src/services/TeamService.js index 803066e3..d8629ae1 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -320,7 +320,7 @@ async function sendEmail (currentUser, data) { data: { handle: currentUser.handle, subject: template.subjectTemplate(data.data), - message: template.messageTemplate(data.data), + message: template.messageTemplate(data.data) }, sendgrid_template_id: template.sendgridTemplateId, version: 'v3', @@ -336,9 +336,98 @@ sendEmail.schema = Joi.object().keys({ }).required() }).required() +/** + * Add a member to a team as customer. + * + * @param {Number} projectId project id + * @param {String} userId user id + * @returns {Object} the member added + */ +async function _addMemberToProjectAsCustomer (projectId, userId) { + try { + const member = await helper.createProjectMember( + projectId, + { userId: userId, role: 'customer' }, + { fields: 'id,userId,role,createdAt,updatedAt,createdBy,updatedBy,handle,photoURL,workingHourStart,workingHourEnd,timeZone,email' } + ) + return member + } catch (err) { + err.message = _.get(err, 'response.body.message') || err.message + if (err.message && err.message.includes('User already registered')) { + throw new Error('User is already added') + } + logger.error({ + component: 'TeamService', + context: '_addMemberToProjectAsCustomer', + message: err.message + }) + throw err + } +} + +/** + * Add members to a team by handle or email. + * @param {Object} currentUser the user who perform this operation + * @param {String} id the team id + * @param {Object} data the object including members with handle/email to be added + * @returns {Object} the success/failed added members + */ +async function addMembers (currentUser, id, data) { + await helper.checkIsMemberOfProject(currentUser.userId, id) + const result = { + success: [], + failed: [] + } + const membersByHandle = await helper.getMemberDetailsByHandles(data.handles) + .then(members => { + return _.groupBy(members, 'handle') + }) + const membersByEmail = await helper.getMemberDetailsByEmails(data.emails) + .then(members => { + return _.groupBy(members, 'email') + }) + await Promise.all([ + Promise.all(data.handles.map(handle => { + if (!membersByHandle[handle]) { + result.failed.push({ error: 'User doesn\'t exist', handle }) + return + } + return _addMemberToProjectAsCustomer(id, membersByHandle[handle][0].userId) + .then(member => { + result.success.push(({ ...member, handle })) + }).catch(err => { + result.failed.push({ error: err.message, handle }) + }) + })), + Promise.all(data.emails.map(email => { + if (!membersByEmail[email]) { + result.failed.push({ error: 'User doesn\'t exist', email }) + return + } + return _addMemberToProjectAsCustomer(id, membersByEmail[email][0].id) + .then(member => { + result.success.push(({ ...member, email })) + }).catch(err => { + result.failed.push({ error: err.message, email }) + }) + })) + ]) + return result +} + +addMembers.schema = Joi.object().keys({ + currentUser: Joi.object().required(), + id: Joi.number().integer().required(), + data: Joi.object().keys({ + handles: Joi.array().items(Joi.string()), + emails: Joi.array().items(Joi.string().email()) + }).or('handles', 'emails').required() +}).required() + module.exports = { searchTeams, getTeam, getTeamJob, - sendEmail + sendEmail, + addMembers } From 5fcd8b45789ad53ab6bd0c4339b990fbd38ec013 Mon Sep 17 00:00:00 2001 From: imcaizheng Date: Fri, 19 Feb 2021 22:53:34 +0800 Subject: [PATCH 35/46] check whether the user can access the project before adding members --- src/services/TeamService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/TeamService.js b/src/services/TeamService.js index d8629ae1..8a91a78f 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -373,7 +373,7 @@ async function _addMemberToProjectAsCustomer (projectId, userId) { * @returns {Object} the success/failed added members */ async function addMembers (currentUser, id, data) { - await helper.checkIsMemberOfProject(currentUser.userId, id) + await helper.getProjectById(currentUser, id) // check whether the user can access the project const result = { success: [], failed: [] From 4f58107dbd4d1f01b542a1b9ec26af6348a7c8f3 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Sun, 21 Feb 2021 14:15:15 +0200 Subject: [PATCH 36/46] fix: create members endpoint allow sending either emails or handles ref issue #148 --- src/services/TeamService.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/services/TeamService.js b/src/services/TeamService.js index 8a91a78f..9c6e16d3 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -374,20 +374,27 @@ async function _addMemberToProjectAsCustomer (projectId, userId) { */ async function addMembers (currentUser, id, data) { await helper.getProjectById(currentUser, id) // check whether the user can access the project + const result = { success: [], failed: [] } - const membersByHandle = await helper.getMemberDetailsByHandles(data.handles) + + const handles = data.handles || [] + const emails = data.emails || [] + + const membersByHandle = await helper.getMemberDetailsByHandles(handles) .then(members => { return _.groupBy(members, 'handle') }) - const membersByEmail = await helper.getMemberDetailsByEmails(data.emails) + + const membersByEmail = await helper.getMemberDetailsByEmails(emails) .then(members => { return _.groupBy(members, 'email') }) + await Promise.all([ - Promise.all(data.handles.map(handle => { + Promise.all(handles.map(handle => { if (!membersByHandle[handle]) { result.failed.push({ error: 'User doesn\'t exist', handle }) return @@ -399,7 +406,7 @@ async function addMembers (currentUser, id, data) { result.failed.push({ error: err.message, handle }) }) })), - Promise.all(data.emails.map(email => { + Promise.all(emails.map(email => { if (!membersByEmail[email]) { result.failed.push({ error: 'User doesn\'t exist', email }) return @@ -412,6 +419,7 @@ async function addMembers (currentUser, id, data) { }) })) ]) + return result } From 88cdea53d64bec2b6c58c8ec4a40765d2105ed9a Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Mon, 22 Feb 2021 12:54:01 +0200 Subject: [PATCH 37/46] fix: allow multiple email addresses red issue #144 --- config/default.js | 5 +++-- config/email_template.config.js | 8 ++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/config/default.js b/config/default.js index 06b08c4f..7831923c 100644 --- a/config/default.js +++ b/config/default.js @@ -113,8 +113,9 @@ module.exports = { // the Kafka message topic for sending email EMAIL_TOPIC: process.env.EMAIL_TOPIC || 'external.action.email', - // the email address for receiving the issue report - REPORT_ISSUE_EMAIL: process.env.REPORT_ISSUE_EMAIL, + // the emails address for receiving the issue report + // REPORT_ISSUE_EMAILS may contain comma-separated list of email which is converted to array + REPORT_ISSUE_EMAILS: (process.env.REPORT_ISSUE_EMAILS || '').split(','), // SendGrid email template ID for reporting issue REPORT_ISSUE_SENDGRID_TEMPLATE_ID: process.env.REPORT_ISSUE_SENDGRID_TEMPLATE_ID, // the URL where TaaS App is hosted diff --git a/config/email_template.config.js b/config/email_template.config.js index ed251a5d..bcbd2f4d 100644 --- a/config/email_template.config.js +++ b/config/email_template.config.js @@ -19,9 +19,7 @@ module.exports = { `Project URL: ${config.TAAS_APP_URL}/{{projectId}}` + '\n' + '\n' + '{{reportText}}', - recipients: [ - config.REPORT_ISSUE_EMAIL - ], + recipients: config.REPORT_ISSUE_EMAILS, sendgridTemplateId: config.REPORT_ISSUE_SENDGRID_TEMPLATE_ID }, /* Report issue for a particular member @@ -39,9 +37,7 @@ module.exports = { `Project URL: ${config.TAAS_APP_URL}/{{projectId}}` + '\n' + '\n' + '{{reportText}}', - recipients: [ - config.REPORT_ISSUE_EMAIL - ], + recipients: config.REPORT_ISSUE_EMAILS, sendgridTemplateId: config.REPORT_ISSUE_SENDGRID_TEMPLATE_ID } } From ba6a8c70af77cada951a93de5b1c57cf90b9985a Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 23 Feb 2021 10:11:55 +0200 Subject: [PATCH 38/46] fix: email event format ref issue #144 --- config/email_template.config.js | 4 ++-- src/services/TeamService.js | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/config/email_template.config.js b/config/email_template.config.js index bcbd2f4d..ea4375f7 100644 --- a/config/email_template.config.js +++ b/config/email_template.config.js @@ -14,7 +14,7 @@ module.exports = { */ 'team-issue-report': { subject: 'Issue Reported on TaaS Team {{projectName}} ({{projectId}}).', - message: 'Project Name: {{projectName}}' + '\n' + + body: 'Project Name: {{projectName}}' + '\n' + 'Project ID: {{projectId}}' + '\n' + `Project URL: ${config.TAAS_APP_URL}/{{projectId}}` + '\n' + '\n' + @@ -31,7 +31,7 @@ module.exports = { */ 'member-issue-report': { subject: 'Issue Reported for member {{userHandle}} on TaaS Team {{projectName}} ({{projectId}}).', - message: 'User Handle: {{userHandle}}' + '\n' + + body: 'User Handle: {{userHandle}}' + '\n' + 'Project Name: {{projectName}}' + '\n' + 'Project ID: {{projectId}}' + '\n' + `Project URL: ${config.TAAS_APP_URL}/{{projectId}}` + '\n' + diff --git a/src/services/TeamService.js b/src/services/TeamService.js index 9c6e16d3..bc5ed3ff 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -17,7 +17,7 @@ const ResourceBookingService = require('./ResourceBookingService') const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { return { subjectTemplate: Handlebars.compile(template.subject), - messageTemplate: Handlebars.compile(template.message), + bodyTemplate: Handlebars.compile(template.body), recipients: template.recipients, sendgridTemplateId: template.sendgridTemplateId } @@ -318,9 +318,8 @@ async function sendEmail (currentUser, data) { const template = emailTemplates[data.template] await helper.postEvent(config.EMAIL_TOPIC, { data: { - handle: currentUser.handle, subject: template.subjectTemplate(data.data), - message: template.messageTemplate(data.data) + body: template.bodyTemplate(data.data) }, sendgrid_template_id: template.sendgridTemplateId, version: 'v3', From 847f79722243f14e8cffe8a0f48e8643246418e0 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 23 Feb 2021 10:22:00 +0200 Subject: [PATCH 39/46] fix: format email body as HTML ref issue #144 --- config/email_template.config.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/config/email_template.config.js b/config/email_template.config.js index ea4375f7..a29a8582 100644 --- a/config/email_template.config.js +++ b/config/email_template.config.js @@ -13,11 +13,11 @@ module.exports = { * - reportText: the body of reported issue. Example: "I have issue with ... \n ... Thank you in advance!" */ 'team-issue-report': { - subject: 'Issue Reported on TaaS Team {{projectName}} ({{projectId}}).', - body: 'Project Name: {{projectName}}' + '\n' + - 'Project ID: {{projectId}}' + '\n' + - `Project URL: ${config.TAAS_APP_URL}/{{projectId}}` + '\n' + - '\n' + + subject: 'Issue Reported on TaaS Team "{{projectName}}" ({{projectId}}).', + body: 'Project Name: {{projectName}}' + '
\n' + + 'Project ID: {{projectId}}' + '
\n' + + `Project URL: ${config.TAAS_APP_URL}/{{projectId}}` + '
\n' + + '
\n' + '{{reportText}}', recipients: config.REPORT_ISSUE_EMAILS, sendgridTemplateId: config.REPORT_ISSUE_SENDGRID_TEMPLATE_ID @@ -30,12 +30,12 @@ module.exports = { * - reportText: the body of reported issue. Example: "I have issue with ... \n ... Thank you in advance!" */ 'member-issue-report': { - subject: 'Issue Reported for member {{userHandle}} on TaaS Team {{projectName}} ({{projectId}}).', - body: 'User Handle: {{userHandle}}' + '\n' + - 'Project Name: {{projectName}}' + '\n' + - 'Project ID: {{projectId}}' + '\n' + - `Project URL: ${config.TAAS_APP_URL}/{{projectId}}` + '\n' + - '\n' + + subject: 'Issue Reported for member "{{userHandle}}" on TaaS Team "{{projectName}}" ({{projectId}}).', + body: 'User Handle: {{userHandle}}' + '
\n' + + 'Project Name: {{projectName}}' + '
\n' + + 'Project ID: {{projectId}}' + '
\n' + + `Project URL: ${config.TAAS_APP_URL}/{{projectId}}` + '
\n' + + '
\n' + '{{reportText}}', recipients: config.REPORT_ISSUE_EMAILS, sendgridTemplateId: config.REPORT_ISSUE_SENDGRID_TEMPLATE_ID From f62169acc4b7c874f69e01ec227b2e52d3204606 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 23 Feb 2021 10:23:57 +0200 Subject: [PATCH 40/46] fix: format email body as HTML no XML ref issue #144 --- config/email_template.config.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/config/email_template.config.js b/config/email_template.config.js index a29a8582..82ec0f1c 100644 --- a/config/email_template.config.js +++ b/config/email_template.config.js @@ -14,10 +14,10 @@ module.exports = { */ 'team-issue-report': { subject: 'Issue Reported on TaaS Team "{{projectName}}" ({{projectId}}).', - body: 'Project Name: {{projectName}}' + '
\n' + - 'Project ID: {{projectId}}' + '
\n' + - `Project URL: ${config.TAAS_APP_URL}/{{projectId}}` + '
\n' + - '
\n' + + body: 'Project Name: {{projectName}}' + '
\n' + + 'Project ID: {{projectId}}' + '
\n' + + `Project URL: ${config.TAAS_APP_URL}/{{projectId}}` + '
\n' + + '
\n' + '{{reportText}}', recipients: config.REPORT_ISSUE_EMAILS, sendgridTemplateId: config.REPORT_ISSUE_SENDGRID_TEMPLATE_ID @@ -31,11 +31,11 @@ module.exports = { */ 'member-issue-report': { subject: 'Issue Reported for member "{{userHandle}}" on TaaS Team "{{projectName}}" ({{projectId}}).', - body: 'User Handle: {{userHandle}}' + '
\n' + - 'Project Name: {{projectName}}' + '
\n' + - 'Project ID: {{projectId}}' + '
\n' + - `Project URL: ${config.TAAS_APP_URL}/{{projectId}}` + '
\n' + - '
\n' + + body: 'User Handle: {{userHandle}}' + '
\n' + + 'Project Name: {{projectName}}' + '
\n' + + 'Project ID: {{projectId}}' + '
\n' + + `Project URL: ${config.TAAS_APP_URL}/{{projectId}}` + '
\n' + + '
\n' + '{{reportText}}', recipients: config.REPORT_ISSUE_EMAILS, sendgridTemplateId: config.REPORT_ISSUE_SENDGRID_TEMPLATE_ID From 79eb11912cc41bfd0e588b803e7cd91482807e9c Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 23 Feb 2021 10:46:18 +0200 Subject: [PATCH 41/46] Revert "fix: format email body as HTML no XML" This reverts commit f62169acc4b7c874f69e01ec227b2e52d3204606. --- config/email_template.config.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/config/email_template.config.js b/config/email_template.config.js index 82ec0f1c..a29a8582 100644 --- a/config/email_template.config.js +++ b/config/email_template.config.js @@ -14,10 +14,10 @@ module.exports = { */ 'team-issue-report': { subject: 'Issue Reported on TaaS Team "{{projectName}}" ({{projectId}}).', - body: 'Project Name: {{projectName}}' + '
\n' + - 'Project ID: {{projectId}}' + '
\n' + - `Project URL: ${config.TAAS_APP_URL}/{{projectId}}` + '
\n' + - '
\n' + + body: 'Project Name: {{projectName}}' + '
\n' + + 'Project ID: {{projectId}}' + '
\n' + + `Project URL: ${config.TAAS_APP_URL}/{{projectId}}` + '
\n' + + '
\n' + '{{reportText}}', recipients: config.REPORT_ISSUE_EMAILS, sendgridTemplateId: config.REPORT_ISSUE_SENDGRID_TEMPLATE_ID @@ -31,11 +31,11 @@ module.exports = { */ 'member-issue-report': { subject: 'Issue Reported for member "{{userHandle}}" on TaaS Team "{{projectName}}" ({{projectId}}).', - body: 'User Handle: {{userHandle}}' + '
\n' + - 'Project Name: {{projectName}}' + '
\n' + - 'Project ID: {{projectId}}' + '
\n' + - `Project URL: ${config.TAAS_APP_URL}/{{projectId}}` + '
\n' + - '
\n' + + body: 'User Handle: {{userHandle}}' + '
\n' + + 'Project Name: {{projectName}}' + '
\n' + + 'Project ID: {{projectId}}' + '
\n' + + `Project URL: ${config.TAAS_APP_URL}/{{projectId}}` + '
\n' + + '
\n' + '{{reportText}}', recipients: config.REPORT_ISSUE_EMAILS, sendgridTemplateId: config.REPORT_ISSUE_SENDGRID_TEMPLATE_ID From 25d313eb9e5a4a4858eaeb3d6533d7fe3c0be6d4 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 23 Feb 2021 10:46:39 +0200 Subject: [PATCH 42/46] Revert "fix: format email body as HTML" This reverts commit 847f79722243f14e8cffe8a0f48e8643246418e0. --- config/email_template.config.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/config/email_template.config.js b/config/email_template.config.js index a29a8582..ea4375f7 100644 --- a/config/email_template.config.js +++ b/config/email_template.config.js @@ -13,11 +13,11 @@ module.exports = { * - reportText: the body of reported issue. Example: "I have issue with ... \n ... Thank you in advance!" */ 'team-issue-report': { - subject: 'Issue Reported on TaaS Team "{{projectName}}" ({{projectId}}).', - body: 'Project Name: {{projectName}}' + '
\n' + - 'Project ID: {{projectId}}' + '
\n' + - `Project URL: ${config.TAAS_APP_URL}/{{projectId}}` + '
\n' + - '
\n' + + subject: 'Issue Reported on TaaS Team {{projectName}} ({{projectId}}).', + body: 'Project Name: {{projectName}}' + '\n' + + 'Project ID: {{projectId}}' + '\n' + + `Project URL: ${config.TAAS_APP_URL}/{{projectId}}` + '\n' + + '\n' + '{{reportText}}', recipients: config.REPORT_ISSUE_EMAILS, sendgridTemplateId: config.REPORT_ISSUE_SENDGRID_TEMPLATE_ID @@ -30,12 +30,12 @@ module.exports = { * - reportText: the body of reported issue. Example: "I have issue with ... \n ... Thank you in advance!" */ 'member-issue-report': { - subject: 'Issue Reported for member "{{userHandle}}" on TaaS Team "{{projectName}}" ({{projectId}}).', - body: 'User Handle: {{userHandle}}' + '
\n' + - 'Project Name: {{projectName}}' + '
\n' + - 'Project ID: {{projectId}}' + '
\n' + - `Project URL: ${config.TAAS_APP_URL}/{{projectId}}` + '
\n' + - '
\n' + + subject: 'Issue Reported for member {{userHandle}} on TaaS Team {{projectName}} ({{projectId}}).', + body: 'User Handle: {{userHandle}}' + '\n' + + 'Project Name: {{projectName}}' + '\n' + + 'Project ID: {{projectId}}' + '\n' + + `Project URL: ${config.TAAS_APP_URL}/{{projectId}}` + '\n' + + '\n' + '{{reportText}}', recipients: config.REPORT_ISSUE_EMAILS, sendgridTemplateId: config.REPORT_ISSUE_SENDGRID_TEMPLATE_ID From 9533a0dffcc89c107f9eb6dcc97c4c11378900bb Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Wed, 24 Feb 2021 14:10:38 +0200 Subject: [PATCH 43/46] debug: removed extra debug ref issue #144 --- src/common/helper.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/common/helper.js b/src/common/helper.js index 313d6504..2ebe7cc6 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -957,13 +957,11 @@ function getAuditM2Muser () { */ async function checkIsMemberOfProject (userId, projectId) { const m2mToken = await getM2MToken() - localLogger.debug({ context: 'checkIsMemberOfProject', message: `m2mToken: ${m2mToken}` }) const res = await request .get(`${config.TC_API}/projects/${projectId}`) .set('Authorization', `Bearer ${m2mToken}`) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - localLogger.debug({ context: 'checkIsMemberOfProject', message: `got project object ${projectId}: ${JSON.stringify(res.body)}` }) const memberIdList = _.map(res.body.members, 'userId') localLogger.debug({ context: 'checkIsMemberOfProject', message: `the members of project ${projectId}: ${JSON.stringify(memberIdList)}, authUserId: ${JSON.stringify(userId)}` }) if (!memberIdList.includes(userId)) { From 80ac1cfa3a471090a58efe9213522eeb72f7db7a Mon Sep 17 00:00:00 2001 From: imcaizheng Date: Wed, 24 Feb 2021 21:22:15 +0800 Subject: [PATCH 44/46] Create 3 proxy endpoints for project members/invites --- app.js | 2 +- ...coder-bookings-api.postman_collection.json | 131 ++++++++++++- docs/swagger.yaml | 182 +++++++++++++++++- src/common/helper.js | 68 ++++++- src/controllers/TeamController.js | 37 +++- src/routes/TeamRoutes.js | 22 +++ src/services/TeamService.js | 84 +++++++- 7 files changed, 512 insertions(+), 14 deletions(-) diff --git a/app.js b/app.js index bc37cd0a..fc52ee99 100644 --- a/app.js +++ b/app.js @@ -76,7 +76,7 @@ app.use((err, req, res, next) => { if (err.response) { // extract error message from V3/V5 API - errorResponse.message = _.get(err, 'response.body.result.content') || _.get(err, 'response.body.message') + errorResponse.message = _.get(err, 'response.body.result.content.message') || _.get(err, 'response.body.message') } if (_.isUndefined(errorResponse.message)) { diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index a3cc1b66..6daa5e1c 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -4527,7 +4527,136 @@ } }, "response": [] - } + }, + { + "name": "GET /taas-teams/:id/members", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "url": { + "raw": "{{URL}}/taas-teams/:id/members?role=customer&fields=id,userId,role,createdAt,updatedAt,createdBy,updatedBy,handle,photoURL,workingHourStart,workingHourEnd,timeZone,email", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + ":id", + "members" + ], + "query": [ + { + "key": "role", + "value": "customer" + }, + { + "key": "fields", + "value": "id,userId,role,createdAt,updatedAt,createdBy,updatedBy,handle,photoURL,workingHourStart,workingHourEnd,timeZone,email" + } + ], + "variable": [ + { + "key": "id", + "value": "16705" + } + ] + } + }, + "response": [] + }, + { + "name": "GET /taas-teams/:id/invites", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "url": { + "raw": "{{URL}}/taas-teams/:id/invites?fields=createdAt,deletedAt,role,updatedBy,createdBy,id,projectId,userId,email,deletedBy,updatedAt,status", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + ":id", + "invites" + ], + "query": [ + { + "key": "fields", + "value": "createdAt,deletedAt,role,updatedBy,createdBy,id,projectId,userId,email,deletedBy,updatedAt,status" + } + ], + "variable": [ + { + "key": "id", + "value": "16705" + } + ] + } + }, + "response": [] + }, + { + "name": "DELETE /taas-teams/:id/members/:projectMemberId", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + }, + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "url": { + "raw": "{{URL}}/taas-teams/:id/members/:projectMemberId", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + ":id", + "members", + ":projectMemberId" + ], + "variable": [ + { + "key": "id", + "value": "16705" + }, + { + "key": "projectMemberId", + "value": "14327" + } + ] + } + }, + "response": [] + } ] }, { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 4c042bab..ad0a3e19 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1580,6 +1580,177 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + get: + tags: + - Teams + description: | + Search members in a team. + Serves as a proxy endpoint for `GET /projects/{projectId}/members`. + security: + - bearerAuth: [] + parameters: + - in: path + name: id + required: true + schema: + type: integer + description: The team/project id. + - in: query + name: fields + required: false + schema: + type: string + description: Fields to be returned. + - in: query + name: role + required: false + schema: + type: string + description: Filtered by a specific role. + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ProjectMember' + '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}/invites: + get: + tags: + - Teams + description: | + Search member invites for a team. + Serves as a proxy endpoint for `GET /projects/{projectId}/invites`. + security: + - bearerAuth: [] + parameters: + - in: path + name: id + required: true + schema: + type: integer + description: The team/project id. + - in: query + name: fields + required: false + schema: + type: string + description: Fields to be returned. + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ProjectMemberInvite' + '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}/members/{projectMemberId}: + delete: + tags: + - Teams + description: | + Remove a member from a team. + Serves as a proxy endpoint for `DELETE /projects/{projectId}/members/{id}`. + security: + - bearerAuth: [] + parameters: + - in: path + name: id + required: true + schema: + type: integer + description: The team/project id. + - in: path + name: projectMemberId + required: true + schema: + type: integer + description: The id of the project member. + responses: + '204': + description: OK + '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: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' /taas-teams/skills: get: tags: @@ -2496,10 +2667,9 @@ components: properties: success: type: array - description: "The handles." + description: "The members created." items: - type: object - example: {"createdAt": "2021-02-18T19:58:50.610Z", "createdBy": -101, "email": "email@domain.com", "handle": "Scud", "id": 14155, "photoURL": "https://topcoder-dev-media.s3.amazonaws.com/member/profile/Scud-1450982908556.png", "role": "customer", "timeZone": null, "updatedAt": "2021-02-18T19:58:50.611Z", "updatedBy": -101, "userId": 1800091, "workingHourEnd": null, "workingHourStart": null} + $ref: '#/components/schemas/ProjectMember' failed: type: array description: "The emails." @@ -2525,6 +2695,12 @@ components: type: string description: "the email of a member" example: 'xxx@xxx.com' + ProjectMember: + type: object + example: {"id": 14329, "userId": 40159097, "role": "customer", "createdAt": "2021-02-24T12:34:45.074Z", "updatedAt": "2021-02-24T12:34:45.075Z", "createdBy": -101, "updatedBy": -101, "handle": "tester1234", "photoURL": null, "workingHourStart": "9:00", "workingHourEnd": "17:00", "timeZone": "Asia/Kolkata", "email": "sathya.jayabal@gmail.com"} + ProjectMemberInvite: + type: object + example: {"createdAt": "2021-02-24T11:02:12.673Z", "deletedAt": null, "role": "customer", "updatedBy": -101, "createdBy": -101, "id": 3686, "projectId": 16705, "userId": 23008602, "email": null, "deletedBy": null, "updatedAt": "2021-02-24T11:02:12.674Z", "status": "pending"} Error: required: - message diff --git a/src/common/helper.js b/src/common/helper.js index 10fc5e19..d144d061 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -1052,6 +1052,69 @@ async function createProjectMember (projectId, data, criteria) { return member } +/** + * List members of a project. + * @param {Object} currentUser the user who perform this operation + * @param {String} projectId the project id + * @param {Object} criteria the search criteria + * @returns {Array} the project members + */ +async function listProjectMembers (currentUser, projectId, criteria = {}) { + const token = (currentUser.hasManagePermission || currentUser.isMachine) + ? `Bearer ${await getM2MToken()}` + : currentUser.jwtToken + const { body: members } = await request + .get(`${config.TC_API}/projects/${projectId}/members`) + .query(criteria) + .set('Authorization', token) + .set('Accept', 'application/json') + localLogger.debug({ context: 'listProjectMembers', message: `response body: ${JSON.stringify(members)}` }) + return members +} + +/** + * List member invites of a project. + * @param {Object} currentUser the user who perform this operation + * @param {String} projectId the project id + * @param {Object} criteria the search criteria + * @returns {Array} the member invites + */ +async function listProjectMemberInvites (currentUser, projectId, criteria = {}) { + const token = (currentUser.hasManagePermission || currentUser.isMachine) + ? `Bearer ${await getM2MToken()}` + : currentUser.jwtToken + const { body: invites } = await request + .get(`${config.TC_API}/projects/${projectId}/invites`) + .query(criteria) + .set('Authorization', token) + .set('Accept', 'application/json') + localLogger.debug({ context: 'listProjectMemberInvites', message: `response body: ${JSON.stringify(invites)}` }) + return invites +} + +/** + * Remove a member from a project. + * @param {Object} currentUser the user who perform this operation + * @param {String} projectId the project id + * @param {String} projectMemberId the id of the project member + * @returns {undefined} + */ +async function deleteProjectMember (currentUser, projectId, projectMemberId) { + const token = (currentUser.hasManagePermission || currentUser.isMachine) + ? `Bearer ${await getM2MToken()}` + : currentUser.jwtToken + try { + await request + .delete(`${config.TC_API}/projects/${projectId}/members/${projectMemberId}`) + .set('Authorization', token) + } catch (err) { + if (err.status === HttpStatus.NOT_FOUND) { + throw new errors.NotFoundError(`projectMemberId: ${projectMemberId} "member" doesn't exist in project ${projectId}`) + } + throw err + } +} + module.exports = { getParamFromCliArgs, promptUser, @@ -1089,5 +1152,8 @@ module.exports = { checkIsMemberOfProject, getMemberDetailsByHandles, getMemberDetailsByEmails, - createProjectMember + createProjectMember, + listProjectMembers, + listProjectMemberInvites, + deleteProjectMember } diff --git a/src/controllers/TeamController.js b/src/controllers/TeamController.js index c60698a2..6cf1a6b4 100644 --- a/src/controllers/TeamController.js +++ b/src/controllers/TeamController.js @@ -50,7 +50,37 @@ async function sendEmail (req, res) { * @param res the response */ async function addMembers (req, res) { - res.send(await service.addMembers(req.authUser, req.params.id, req.body)) + res.send(await service.addMembers(req.authUser, req.params.id, req.query, req.body)) +} + +/** + * Search members in a team. + * @param req the request + * @param res the response + */ +async function searchMembers (req, res) { + const result = await service.searchMembers(req.authUser, req.params.id, req.query) + res.send(result.result) +} + +/** + * Search member invites for a team. + * @param req the request + * @param res the response + */ +async function searchInvites (req, res) { + const result = await service.searchInvites(req.authUser, req.params.id, req.query) + res.send(result.result) +} + +/** + * Remove a member from a team. + * @param req the request + * @param res the response + */ +async function deleteMember (req, res) { + await service.deleteMember(req.authUser, req.params.id, req.params.projectMemberId) + res.status(HttpStatus.NO_CONTENT).end() } module.exports = { @@ -58,5 +88,8 @@ module.exports = { getTeam, getTeamJob, sendEmail, - addMembers + addMembers, + searchMembers, + searchInvites, + deleteMember } diff --git a/src/routes/TeamRoutes.js b/src/routes/TeamRoutes.js index 0fd06402..3df14b0c 100644 --- a/src/routes/TeamRoutes.js +++ b/src/routes/TeamRoutes.js @@ -50,6 +50,28 @@ module.exports = { method: 'addMembers', auth: 'jwt', scopes: [constants.Scopes.READ_TAAS_TEAM] + }, + get: { + controller: 'TeamController', + method: 'searchMembers', + auth: 'jwt', + scopes: [constants.Scopes.READ_TAAS_TEAM] + } + }, + '/taas-teams/:id/invites': { + get: { + controller: 'TeamController', + method: 'searchInvites', + auth: 'jwt', + scopes: [constants.Scopes.READ_TAAS_TEAM] + } + }, + '/taas-teams/:id/members/:projectMemberId': { + delete: { + controller: 'TeamController', + method: 'deleteMember', + auth: 'jwt', + scopes: [constants.Scopes.READ_TAAS_TEAM] } } } diff --git a/src/services/TeamService.js b/src/services/TeamService.js index bc5ed3ff..b8576e62 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -340,14 +340,15 @@ sendEmail.schema = Joi.object().keys({ * * @param {Number} projectId project id * @param {String} userId user id + * @param {String} fields the fields to be returned * @returns {Object} the member added */ -async function _addMemberToProjectAsCustomer (projectId, userId) { +async function _addMemberToProjectAsCustomer (projectId, userId, fields) { try { const member = await helper.createProjectMember( projectId, { userId: userId, role: 'customer' }, - { fields: 'id,userId,role,createdAt,updatedAt,createdBy,updatedBy,handle,photoURL,workingHourStart,workingHourEnd,timeZone,email' } + { fields } ) return member } catch (err) { @@ -368,10 +369,11 @@ async function _addMemberToProjectAsCustomer (projectId, userId) { * Add members to a team by handle or email. * @param {Object} currentUser the user who perform this operation * @param {String} id the team id + * @params {Object} criteria the search criteria * @param {Object} data the object including members with handle/email to be added * @returns {Object} the success/failed added members */ -async function addMembers (currentUser, id, data) { +async function addMembers (currentUser, id, criteria, data) { await helper.getProjectById(currentUser, id) // check whether the user can access the project const result = { @@ -398,7 +400,7 @@ async function addMembers (currentUser, id, data) { result.failed.push({ error: 'User doesn\'t exist', handle }) return } - return _addMemberToProjectAsCustomer(id, membersByHandle[handle][0].userId) + return _addMemberToProjectAsCustomer(id, membersByHandle[handle][0].userId, criteria.fields) .then(member => { result.success.push(({ ...member, handle })) }).catch(err => { @@ -410,7 +412,7 @@ async function addMembers (currentUser, id, data) { result.failed.push({ error: 'User doesn\'t exist', email }) return } - return _addMemberToProjectAsCustomer(id, membersByEmail[email][0].id) + return _addMemberToProjectAsCustomer(id, membersByEmail[email][0].id, criteria.fields) .then(member => { result.success.push(({ ...member, email })) }).catch(err => { @@ -425,16 +427,86 @@ async function addMembers (currentUser, id, data) { addMembers.schema = Joi.object().keys({ currentUser: Joi.object().required(), id: Joi.number().integer().required(), + criteria: Joi.object().keys({ + fields: Joi.string() + }).required(), data: Joi.object().keys({ handles: Joi.array().items(Joi.string()), emails: Joi.array().items(Joi.string().email()) }).or('handles', 'emails').required() }).required() +/** + * Search members in a team. + * Serves as a proxy endpoint for `GET /projects/{projectId}/members`. + * + * @param {Object} currentUser the user who perform this operation. + * @param {String} id the team id + * @params {Object} criteria the search criteria + * @returns {Object} the search result + */ +async function searchMembers (currentUser, id, criteria) { + const result = await helper.listProjectMembers(currentUser, id, criteria) + return { result } +} + +searchMembers.schema = Joi.object().keys({ + currentUser: Joi.object().required(), + id: Joi.number().integer().required(), + criteria: Joi.object().keys({ + role: Joi.string(), + fields: Joi.string() + }).required() +}).required() + +/** + * Search member invites for a team. + * Serves as a proxy endpoint for `GET /projects/{projectId}/invites`. + * + * @param {Object} currentUser the user who perform this operation. + * @param {String} id the team id + * @params {Object} criteria the search criteria + * @returns {Object} the search result + */ +async function searchInvites (currentUser, id, criteria) { + const result = await helper.listProjectMemberInvites(currentUser, id, criteria) + return { result } +} + +searchInvites.schema = Joi.object().keys({ + currentUser: Joi.object().required(), + id: Joi.number().integer().required(), + criteria: Joi.object().keys({ + fields: Joi.string() + }).required() +}).required() + +/** + * Remove a member from a team. + * Serves as a proxy endpoint for `DELETE /projects/{projectId}/members/{id}`. + * + * @param {Object} currentUser the user who perform this operation. + * @param {String} id the team id + * @param {String} projectMemberId the id of the project member + * @returns {undefined} + */ +async function deleteMember (currentUser, id, projectMemberId) { + await helper.deleteProjectMember(currentUser, id, projectMemberId) +} + +deleteMember.schema = Joi.object().keys({ + currentUser: Joi.object().required(), + id: Joi.number().integer().required(), + projectMemberId: Joi.number().integer().required() +}).required() + module.exports = { searchTeams, getTeam, getTeamJob, sendEmail, - addMembers + addMembers, + searchMembers, + searchInvites, + deleteMember } From 187d79dc32e9c4f8386e29149834ea0faa30e7e5 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Wed, 24 Feb 2021 17:19:49 +0200 Subject: [PATCH 45/46] fix: add members case-insesitive way ref issue #148 --- src/services/TeamService.js | 39 ++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/src/services/TeamService.js b/src/services/TeamService.js index b8576e62..792eca7f 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -384,36 +384,49 @@ async function addMembers (currentUser, id, criteria, data) { const handles = data.handles || [] const emails = data.emails || [] - const membersByHandle = await helper.getMemberDetailsByHandles(handles) - .then(members => { - return _.groupBy(members, 'handle') - }) - - const membersByEmail = await helper.getMemberDetailsByEmails(emails) - .then(members => { - return _.groupBy(members, 'email') - }) + const handleMembers = await helper.getMemberDetailsByHandles(handles) + .then((members) => _.map(members, (member) => ({ + ...member, + // populate members with lower-cased handle for case insensitive search + handleLowerCase: member.handle.toLowerCase() + }))) + + const emailMembers = await helper.getMemberDetailsByEmails(emails) + .then((members) => _.map(members, (member) => ({ + ...member, + // populate members with lower-cased email for case insensitive search + emailLowerCase: member.email.toLowerCase() + }))) await Promise.all([ Promise.all(handles.map(handle => { - if (!membersByHandle[handle]) { + const memberDetails = _.find(handleMembers, { handleLowerCase: handle.toLowerCase() }) + + if (!memberDetails) { result.failed.push({ error: 'User doesn\'t exist', handle }) return } - return _addMemberToProjectAsCustomer(id, membersByHandle[handle][0].userId, criteria.fields) + + return _addMemberToProjectAsCustomer(id, memberDetails.userId, criteria.fields) .then(member => { + // note, that we return `handle` in the same case it was in request result.success.push(({ ...member, handle })) }).catch(err => { result.failed.push({ error: err.message, handle }) }) })), + Promise.all(emails.map(email => { - if (!membersByEmail[email]) { + const memberDetails = _.find(emailMembers, { emailLowerCase: email.toLowerCase() }) + + if (!memberDetails) { result.failed.push({ error: 'User doesn\'t exist', email }) return } - return _addMemberToProjectAsCustomer(id, membersByEmail[email][0].id, criteria.fields) + + return _addMemberToProjectAsCustomer(id, memberDetails.id, criteria.fields) .then(member => { + // note, that we return `email` in the same case it was in request result.success.push(({ ...member, email })) }).catch(err => { result.failed.push({ error: err.message, email }) From 3c82b7e247c75277329b0c17018f546ca1be83cf Mon Sep 17 00:00:00 2001 From: imcaizheng Date: Thu, 25 Feb 2021 07:27:06 +0800 Subject: [PATCH 46/46] Fix candidates list in `GET /taas-teams/:teamId/jobs/:jobId` --- src/services/TeamService.js | 54 ++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/services/TeamService.js b/src/services/TeamService.js index 792eca7f..640791a2 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -268,35 +268,35 @@ async function getTeamJob (currentUser, id, jobId) { ) } + // If the job has candidates, the following data for each candidate would be populated: + // + // - the `status`, `resume`, `userId` and `id` of the candidate + // - the `handle`, `firstName` `lastName` and `skills` of the user(from GET /users/:userId) for the candidate + // - the `photoURL` of the member(from GET /members) for the candidate + // if (job && job.candidates && job.candidates.length > 0) { - const usersPromises = [] - _.map(job.candidates, (candidate) => { usersPromises.push(helper.getUserById(candidate.userId, true)) }) - 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(userHandles) - - for (const item of candidates) { - const candidate = _.find(job.candidates, { userId: item.id }) - // TODO this logic should be vice-verse, we should loop trough candidates and populate users data if found, - // not loop through users and populate candidates data if found - if (candidate) { - item.resume = candidate.resume - item.status = candidate.status - // return User id as `userId` and JobCandidate id as `id` - item.userId = item.id - item.id = candidate.id - } - const findMember = _.find(members, { handleLower: item.handle.toLowerCase() }) - if (findMember && findMember.photoURL) { - item.photo_url = findMember.photoURL - } + // find user data for candidates + const users = await Promise.all( + _.map(_.uniq(_.map(job.candidates, 'userId')), userId => helper.getUserById(userId, true)) + ) + const userMap = _.groupBy(users, 'id') + + // find photo URLs for users + const members = await helper.getMembers(_.map(users, 'handle')) + const photoURLMap = _.groupBy(members, 'handleLower') + + result.candidates = _.map(job.candidates, candidate => { + const candidateData = _.pick(candidate, ['status', 'resume', 'userId', 'id']) + const userData = userMap[candidate.userId][0] + // attach user data to the candidate + Object.assign(candidateData, _.pick(userData, ['handle', 'firstName', 'lastName', 'skills'])) + // attach photo URL to the candidate + const handleLower = userData.handle.toLowerCase() + if (photoURLMap[handleLower]) { + candidateData.photo_url = photoURLMap[handleLower][0].photoURL } - } - - result.candidates = candidates + return candidateData + }) } return result