From 6811fc3ea84b8fdbe67df060c797a07d23b30a15 Mon Sep 17 00:00:00 2001 From: Samir Date: Sat, 9 Feb 2019 20:12:32 +0100 Subject: [PATCH 01/48] local development support --- README.md | 29 ++------------- migrations/seedMetadata.js | 72 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 26 deletions(-) create mode 100644 migrations/seedMetadata.js diff --git a/README.md b/README.md index c9efc4e7..a76aa653 100644 --- a/README.md +++ b/README.md @@ -42,32 +42,9 @@ Run `npm run sync:es` from the root of project to execute the script. **NOTE**: In production these dependencies / services are hosted & managed outside tc-projects-service. -#### Kafka -Kafka must be installed and configured prior starting the application. -Following topics must be created: -``` -notifications.connect.project.updated -notifications.connect.project.files.updated -notifications.connect.project.team.updated -notifications.connect.project.plan.updated -notifications.connect.project.topic.created -notifications.connect.project.topic.updated -notifications.connect.project.post.created -notifications.connect.project.post.edited -``` +### Import sample metadata -New Kafka related configuration options has been introduced: -``` -"kafkaConfig": { - "hosts": List of Kafka brokers. Default: localhost: 9092 - "clientCert": SSL certificate - "clientCertKey": Certificate key -} -``` -Environment variables: -- `KAFKA_HOSTS` - same as "hosts" -- `KAFKA_CLIENT_CERT` - same as "clientCert" -- `KAFKA_CLIENT_CERT_KEY` - same as "clientCertKey" +To create sample metadata entries (duplicate what is currently in development environment) run `node migrations/seedMetadata.js` ### Test @@ -103,4 +80,4 @@ You may replace 172.17.0.1 with your docker0 IP. You can paste **swagger.yaml** to [swagger editor](http://editor.swagger.io/) or import **postman.json** and **postman_environment.json** to verify endpoints. #### Deploying without docker -If you don't want to use docker to deploy to localhost. You can simply run `npm run start` from root of project. This should start the server on default port `3000`. +If you don't want to use docker to deploy to localhost. You can simply run `npm run start:dev` from root of project. This should start the server on default port `8001`. diff --git a/migrations/seedMetadata.js b/migrations/seedMetadata.js new file mode 100644 index 00000000..431daa8f --- /dev/null +++ b/migrations/seedMetadata.js @@ -0,0 +1,72 @@ +/* eslint-disable */ +const _ = require('lodash') +const axios = require('axios'); +const Promise = require('bluebird'); + + +var url = 'https://api.topcoder-dev.com/v4/projects/metadata'; +var targetUrl = 'http://localhost:8001/v4/'; +var destUrl = targetUrl + 'projects/'; +var destTimelines = targetUrl; + +axios.get(url) + .then(async function (response) { + let data = response.data; + + var headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw' + } + + + let promises = _(data.result.content.projectTypes).map(pt=>{ + return axios.post(destUrl+'metadata/projectTypes',{param:pt}, {headers:headers}) + }); + try{ + await Promise.all(promises); + }catch(ex){ + //ignore the error + } + + promises = _(data.result.content.projectTemplates).map(pt=>{ + return axios.post(destUrl+'metadata/projectTemplates',{param:pt}, {headers:headers}) + }); + try{ + await Promise.all(promises); + }catch(ex){ + //ignore the error + } + + promises = _(data.result.content.productCategories).map(pt=>{ + return axios.post(destUrl+'metadata/productCategories',{param:pt}, {headers:headers}) + }); + try{ + await Promise.all(promises); + }catch(ex){ + //ignore the error + } + + promises = _(data.result.content.productTemplates).map(pt=>{ + return axios.post(destUrl+'metadata/productTemplates',{param:pt}, {headers:headers}) + }); + try{ + await Promise.all(promises); + }catch(ex){ + //ignore the error + } + + await Promise.each(data.result.content.milestoneTemplates,pt=>{ + return new Promise((resolve,reject)=>{ + axios.post(destTimelines+'timelines/metadata/milestoneTemplates',{param:pt}, {headers:headers}) + .then(r=>resolve()) + .catch(e=>resolve()); //ignore the error + }) + }); + + + + // handle success + console.log('Done'); + }).catch(err=>{ + console.log(err); + }); From 22415e4cc8f28c6d4b4c16f850674b1d463b891d Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Mon, 11 Mar 2019 16:52:46 +0530 Subject: [PATCH 02/48] Fixed the bug where we were not getting the waiting for customer event. Also fixed unit tests around milestone update. --- src/routes/milestones/update.spec.js | 44 ++++++++++++++++++++++++++-- src/util.js | 2 +- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/routes/milestones/update.spec.js b/src/routes/milestones/update.spec.js index ad591287..67cfd0a0 100644 --- a/src/routes/milestones/update.spec.js +++ b/src/routes/milestones/update.spec.js @@ -1102,7 +1102,45 @@ describe('UPDATE Milestone', () => { sandbox.restore(); }); - it('should send message BUS_API_EVENT.TIMELINE_ADJUSTED when milestone duration updated', (done) => { + it('should send message BUS_API_EVENT.MILESTONE_WAITING_CUSTOMER when milestone duration updated', (done) => { + request(server) + .patch('/v4/timelines/1/milestones/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + // duration: 1, + details: { + metadata: { waitingForCustomer : true } + } + }, + }) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + // 5 milestones in total, so it would trigger 5 events + // 4 MILESTONE_UPDATED events are for 4 non deleted milestones + // 1 TIMELINE_ADJUSTED event, because timeline's end date updated + createEventSpy.callCount.should.be.eql(2); + createEventSpy.firstCall.calledWith(BUS_API_EVENT.MILESTONE_UPDATED, sinon.match({ + projectId: 1, + projectName: 'test1', + projectUrl: 'https://local.topcoder-dev.com/projects/1', + userId: 40051332, + initiatorUserId: 40051332, + })).should.be.true; + createEventSpy.lastCall.calledWith(BUS_API_EVENT.MILESTONE_WAITING_CUSTOMER).should.be.true; + done(); + }); + } + }); + }); + + xit('should send message BUS_API_EVENT.TIMELINE_ADJUSTED when milestone duration updated', (done) => { request(server) .patch('/v4/timelines/1/milestones/1') .set({ @@ -1130,14 +1168,14 @@ describe('UPDATE Milestone', () => { userId: 40051332, initiatorUserId: 40051332, })).should.be.true; - createEventSpy.lastCall.calledWith(BUS_API_EVENT.TIMELINE_ADJUSTED); + createEventSpy.lastCall.calledWith(BUS_API_EVENT.TIMELINE_ADJUSTED).should.be.true; done(); }); } }); }); - it('should send message BUS_API_EVENT.MILESTONE_UPDATED when milestone status updated', (done) => { + xit('should send message BUS_API_EVENT.MILESTONE_UPDATED when milestone status updated', (done) => { request(server) .patch('/v4/timelines/1/milestones/1') .set({ diff --git a/src/util.js b/src/util.js index 5231249b..d54d88d8 100644 --- a/src/util.js +++ b/src/util.js @@ -390,7 +390,7 @@ _.assignIn(util, { */ mergeJsonObjects: (targetObj, sourceObj, mergeExceptions) => // eslint-disable-next-line consistent-return - _.mergeWith(targetObj, sourceObj, (target, source, key) => { + _.mergeWith({}, targetObj, sourceObj, (target, source, key) => { // Overwrite the array or merge exception keys if (_.isArray(source) || (mergeExceptions && mergeExceptions.indexOf(key) !== -1)) { return source; From 58aa28719e858192d35d5b6afa244028031f479d Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Mon, 11 Mar 2019 17:42:15 +0530 Subject: [PATCH 03/48] lint fix --- src/routes/milestones/update.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/milestones/update.spec.js b/src/routes/milestones/update.spec.js index 67cfd0a0..9a694e73 100644 --- a/src/routes/milestones/update.spec.js +++ b/src/routes/milestones/update.spec.js @@ -1112,8 +1112,8 @@ describe('UPDATE Milestone', () => { param: { // duration: 1, details: { - metadata: { waitingForCustomer : true } - } + metadata: { waitingForCustomer: true }, + }, }, }) .expect(200) From 0b469f09ce74be9cbc3503826a861ed4c861857c Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Mon, 11 Mar 2019 18:15:16 +0530 Subject: [PATCH 04/48] Fixing unit tests for aliases field of product template --- src/routes/productTemplates/update.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/productTemplates/update.js b/src/routes/productTemplates/update.js index 39456144..60d7f730 100644 --- a/src/routes/productTemplates/update.js +++ b/src/routes/productTemplates/update.js @@ -64,7 +64,7 @@ module.exports = [ } // Merge JSON fields - entityToUpdate.aliases = util.mergeJsonObjects(productTemplate.aliases, entityToUpdate.aliases); + // entityToUpdate.aliases = util.mergeJsonObjects(productTemplate.aliases, entityToUpdate.aliases); entityToUpdate.template = util.mergeJsonObjects(productTemplate.template, entityToUpdate.template); return productTemplate.update(entityToUpdate); From bd0f5997d81a5ddf80228fb33b2581af1fd7ee95 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Wed, 13 Mar 2019 09:36:23 +0800 Subject: [PATCH 05/48] improve local development guide in README --- README.md | 112 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 81 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index cc073a9b..e577765f 100644 --- a/README.md +++ b/README.md @@ -4,48 +4,98 @@ Microservice to manage CRUD operations for all things Projects. ### Note : Steps mentioned below are best to our capability as guide for local deployment, however, we expect from contributor, being a developer, to resolve run-time issues (e.g. OS and node version issues etc), if any. -### Local Development +## Local Development -* We use docker-compose for running dependencies locally. Instructions for Docker compose setup - https://docs.docker.com/compose/install/ +### Requirements + +* [docker-compose](https://docs.docker.com/compose/install/) - We use docker-compose for running dependencies locally. * Nodejs 8.9.4 - consider using [nvm](https://github.com/creationix/nvm) or equivalent to manage your node version * Install [libpg](https://www.npmjs.com/package/pg-native) -* Install node dependencies -`npm install` - -* Start local services -```~/Projects/tc-projects-service -> cd local/ -~/Projects/tc-projects-service/local -> docker-compose up -``` -Copy config/sample.local.js as config/local.js, update the properties and according to your env setup - -#### Database -Once you start your PostgreSQL database through docker, it will create a projectsdb. -*To create tables - note this will drop tables if they already exist* -``` -NODE_ENV=development npm run sync:db -``` -#### Redis -Docker compose command will start a local redis instance as well. You should be able to connect to this instance using url `$(docker-machine ip):6379` +### Steps to run locally +1. Install node dependencies + ```bash + npm install + ``` + +* Run docker with dependant services + ```bash + cd local/ + docker-compose up + ``` + This will run several services locally: + - `postgres` + - `elasticsearch` + - `rabbitmq` + - `mock-services` - mocks some Topcoder API + + *NOTE: In production these dependencies / services are hosted & managed outside tc-projects-service.* + +* Local config + ```bash + # in the tc-project-service root folder, not inside local/ as above + cp config/sample.local.js config/local.js + ``` + Copy `config/sample.local.js` as `config/local.js`.
+ As project service depend on many third-party services we have to config how to access them. Some services are run locally and some services are used from Topcoder DEV environment. `config/local.js` has a prepared configuration which would replace values no matter what `NODE_ENV` value is. + + **IMPORTANT** This configuration file assumes that services run by docker use domain `dockerhost`. Depend on your system you have to make sure that domain `dockerhost` points to the IP address of docker. + For example, you can add a the next line to your `/etc/hosts` file, if docker is run on IP `127.0.0.1`. + ``` + 127.0.0.1 dockerhost + ``` + Alternatively, you may update `config/local.js` and replace `dockerhost` with your docker IP address.
+ You may try using command `docker-machine ip` to get your docker IP, but it works not for all systems. + +* Create tables in DB + ```bash + NODE_ENV=development npm run sync:db + ``` + This command will crate tables in `postgres` db. + + *NOTE: this will drop tables if they already exist.* + +* Sync ES indices + ```bash + NODE_ENV=development npm run sync:es + ``` + Helper script to sync the indices and mappings with the elasticsearch. + + *NOTE: This will first clear all the indices and than recreate them. So use with caution.* + +* Run + ```bash + npm run start:dev + ``` + Runs the Project Service using nodemon, so it would be restarted after any of the files is updated. + The project service will be served on `http://localhost:8001`. -#### Elasticsearch -Docker compose includes elasticsearch instance as well. It will open ports 9200 & 9300 (kibana) - -#### Sync indices and mappings +### Import sample metadata +```bash +node migrations/seedMetadata.js +``` +To create sample metadata entries (duplicate what is currently in development environment). -There is a helper script to sync the indices and mappings with the elasticsearch. +### Run Connect App with Project Service locally -Run `npm run sync:es` from the root of project to execute the script. +To be able to run [Connect App](https://github.com/appirio-tech/connect-app) with the local setup of Project Service we have to do two things: +1. Configurate Connect App to use locally deployed Project service inside `connect-app/config/constants/dev.js` set -> NOTE: This will first clear all the indices and than recreate them. So use with caution. + ```js + PROJECTS_API_URL: 'http://localhost:8001' + ``` +2. Bypass token validation in Project Service. -**NOTE**: In production these dependencies / services are hosted & managed outside tc-projects-service. + In `tc-project-service/node_modules/tc-core-library-js/lib/auth/verifier.js` add this to line 23: + ```js + callback(undefined, decodedToken.payload); + return; + ``` + Connect App when making requests to the Project Service uses token retrieved from the Topcoder service deployed online. Project Service validates the token. For this purpose Project Service have to know the `secret` which has been used to generate the token. But we don't know the `secret` which is used by Topcoder for both DEV and PROD environment. So to bypass token validation we change these lines in the auth library. -### Import sample metadata + *NOTE: this change only let us bypass validation during local development process*. -To create sample metadata entries (duplicate what is currently in development environment) run `node migrations/seedMetadata.js` +3. Restart both Connect App and Project Service if they were running. ### Test From e3410f0737d4daadbaad843dad56948f573ade18 Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Wed, 13 Mar 2019 16:29:06 +0530 Subject: [PATCH 06/48] =?UTF-8?q?Making=20all=20metadata=20endpoints=20to?= =?UTF-8?q?=20require=20authentication=20as=20we=20don=E2=80=99t=20have=20?= =?UTF-8?q?logged=20out=20project=20creation=20wizard=20any=20more?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/index.js | 14 ++++++------ src/routes/metadata/list.spec.js | 27 +++++++++++------------ src/routes/orgConfig/get.spec.js | 4 ++-- src/routes/orgConfig/list.spec.js | 4 ++-- src/routes/productCategories/get.spec.js | 4 ++-- src/routes/productCategories/list.spec.js | 4 ++-- src/routes/productTemplates/get.spec.js | 4 ++-- src/routes/productTemplates/list.spec.js | 10 ++------- src/routes/projectTemplates/get.spec.js | 4 ++-- src/routes/projectTemplates/list.spec.js | 4 ++-- src/routes/projectTypes/get.spec.js | 4 ++-- src/routes/projectTypes/list.spec.js | 4 ++-- 12 files changed, 40 insertions(+), 47 deletions(-) diff --git a/src/routes/index.js b/src/routes/index.js index 110c5ca3..0312b624 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -27,6 +27,13 @@ router.get(`/${apiVersion}/projects/health`, (req, res) => { // All project service endpoints need authentication const jwtAuth = require('tc-core-library-js').middleware.jwtAuthenticator; +router.all( + RegExp(`\\/${apiVersion}\\/(projects|timelines|orgConfig)(?!\\/health).*`), (req, res, next) => ( + // JWT authentication + jwtAuth(config)(req, res, next) + ), +); + router.route('/v4/projects/metadata/projectTemplates') .get(require('./projectTemplates/list')); router.route('/v4/projects/metadata/projectTemplates/:templateId(\\d+)') @@ -58,13 +65,6 @@ router.use('/v4/projects/metadata', compression()); router.route('/v4/projects/metadata') .get(require('./metadata/list')); -router.all( - RegExp(`\\/${apiVersion}\\/(projects|timelines|orgConfig)(?!\\/health).*`), (req, res, next) => ( - // JWT authentication - jwtAuth(config)(req, res, next) - ), -); - // Register all the routes router.use('/v4/projects', compression()); router.route('/v4/projects') diff --git a/src/routes/metadata/list.spec.js b/src/routes/metadata/list.spec.js index 86798d21..48f82532 100644 --- a/src/routes/metadata/list.spec.js +++ b/src/routes/metadata/list.spec.js @@ -95,21 +95,10 @@ describe('GET all metadata', () => { after(testUtil.clearDb); describe('GET /projects/metadata', () => { - it('should return 200 even if user is not authenticated', (done) => { + it('should return 403 if user is not authenticated', (done) => { request(server) .get('/v4/projects/metadata') - .expect(200) - .end((err, res) => { - const resJson = res.body.result.content; - should.exist(resJson); - resJson.projectTemplates.should.have.length(1); - resJson.productTemplates.should.have.length(1); - resJson.milestoneTemplates.should.have.length(1); - resJson.projectTypes.should.have.length(1); - resJson.productCategories.should.have.length(1); - - done(); - }); + .expect(403, done); }); it('should return 200 for admin', (done) => { @@ -129,7 +118,17 @@ describe('GET all metadata', () => { Authorization: `Bearer ${testUtil.jwts.admin}`, }) .expect(200) - .end(done); + .end((err, res) => { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.projectTemplates.should.have.length(1); + resJson.productTemplates.should.have.length(1); + resJson.milestoneTemplates.should.have.length(1); + resJson.projectTypes.should.have.length(1); + resJson.productCategories.should.have.length(1); + + done(); + }); }); it('should return 200 for connect manager', (done) => { diff --git a/src/routes/orgConfig/get.spec.js b/src/routes/orgConfig/get.spec.js index e3f64325..5bafd4c0 100644 --- a/src/routes/orgConfig/get.spec.js +++ b/src/routes/orgConfig/get.spec.js @@ -74,10 +74,10 @@ describe('GET organization config', () => { }); }); - it('should return 200 even if user is not authenticated', (done) => { + it('should return 403 if user is not authenticated', (done) => { request(server) .get(`/v4/projects/metadata/orgConfig/${id}`) - .expect(200, done); + .expect(403, done); }); it('should return 200 for connect admin', (done) => { diff --git a/src/routes/orgConfig/list.spec.js b/src/routes/orgConfig/list.spec.js index 44f42cc7..70a3c093 100644 --- a/src/routes/orgConfig/list.spec.js +++ b/src/routes/orgConfig/list.spec.js @@ -63,10 +63,10 @@ describe('LIST organization config', () => { }); }); - it('should return 200 even if user is not authenticated with filter', (done) => { + it('should return 403 if user is not authenticated with filter', (done) => { request(server) .get(`${orgConfigPath}?filter=orgId%3Din%28${configs[0].orgId}%29%26configName=${configs[0].configName}`) - .expect(200, done); + .expect(403, done); }); it('should return 200 for connect admin with filter', (done) => { diff --git a/src/routes/productCategories/get.spec.js b/src/routes/productCategories/get.spec.js index 78ba07ce..87cf71cb 100644 --- a/src/routes/productCategories/get.spec.js +++ b/src/routes/productCategories/get.spec.js @@ -82,10 +82,10 @@ describe('GET product category', () => { }); }); - it('should return 200 even if user is not authenticated', (done) => { + it('should return 403 if user is not authenticated', (done) => { request(server) .get(`/v4/projects/metadata/productCategories/${key}`) - .expect(200, done); + .expect(403, done); }); it('should return 200 for connect admin', (done) => { diff --git a/src/routes/productCategories/list.spec.js b/src/routes/productCategories/list.spec.js index d055f7df..94114813 100644 --- a/src/routes/productCategories/list.spec.js +++ b/src/routes/productCategories/list.spec.js @@ -77,10 +77,10 @@ describe('LIST product categories', () => { }); }); - it('should return 200 even if user is not authenticated', (done) => { + it('should return 403 if user is not authenticated', (done) => { request(server) .get('/v4/projects/metadata/productCategories') - .expect(200, done); + .expect(403, done); }); it('should return 200 for connect admin', (done) => { diff --git a/src/routes/productTemplates/get.spec.js b/src/routes/productTemplates/get.spec.js index fd7cd7da..0c380dc7 100644 --- a/src/routes/productTemplates/get.spec.js +++ b/src/routes/productTemplates/get.spec.js @@ -109,10 +109,10 @@ describe('GET product template', () => { }); }); - it('should return 200 even if user is not authenticated', (done) => { + it('should return 403 if user is not authenticated', (done) => { request(server) .get(`/v4/projects/metadata/productTemplates/${templateId}`) - .expect(200, done); + .expect(403, done); }); it('should return 200 for connect admin', (done) => { diff --git a/src/routes/productTemplates/list.spec.js b/src/routes/productTemplates/list.spec.js index a9386f66..ae061c24 100644 --- a/src/routes/productTemplates/list.spec.js +++ b/src/routes/productTemplates/list.spec.js @@ -113,16 +113,10 @@ describe('LIST product templates', () => { }); }); - it('should return 200 even if user is not authenticated', (done) => { + it('should return 403 if user is not authenticated', (done) => { request(server) .get('/v4/projects/metadata/productTemplates') - .expect(200) - .end((err, res) => { - const resJson = res.body.result.content; - validateProductTemplates(2, resJson, templates); - resJson[0].id.should.be.eql(templateId); - done(); - }); + .expect(403, done); }); it('should return 200 for connect admin', (done) => { diff --git a/src/routes/projectTemplates/get.spec.js b/src/routes/projectTemplates/get.spec.js index ae6607fa..61fc4da0 100644 --- a/src/routes/projectTemplates/get.spec.js +++ b/src/routes/projectTemplates/get.spec.js @@ -105,10 +105,10 @@ describe('GET project template', () => { }); }); - it('should return 200 even if user is not authenticated', (done) => { + it('should return 403 if user is not authenticated', (done) => { request(server) .get(`/v4/projects/metadata/projectTemplates/${templateId}`) - .expect(200, done); + .expect(403, done); }); it('should return 200 for connect admin', (done) => { diff --git a/src/routes/projectTemplates/list.spec.js b/src/routes/projectTemplates/list.spec.js index 9c3008e8..2da743c5 100644 --- a/src/routes/projectTemplates/list.spec.js +++ b/src/routes/projectTemplates/list.spec.js @@ -104,10 +104,10 @@ describe('LIST project templates', () => { }); }); - it('should return 200 for anonymous user', (done) => { + it('should return 403 for anonymous user', (done) => { request(server) .get('/v4/projects/metadata/projectTemplates') - .expect(200, done); + .expect(403, done); }); it('should return 200 for connect admin', (done) => { diff --git a/src/routes/projectTypes/get.spec.js b/src/routes/projectTypes/get.spec.js index 099b4c66..621b30d6 100644 --- a/src/routes/projectTypes/get.spec.js +++ b/src/routes/projectTypes/get.spec.js @@ -84,10 +84,10 @@ describe('GET project type', () => { }); }); - it('should return 200 even if user is not authenticated', (done) => { + it('should return 403 if user is not authenticated', (done) => { request(server) .get(`/v4/projects/metadata/projectTypes/${key}`) - .expect(200, done); + .expect(403, done); }); it('should return 200 for connect admin', (done) => { diff --git a/src/routes/projectTypes/list.spec.js b/src/routes/projectTypes/list.spec.js index f941fe02..fa2a1c35 100644 --- a/src/routes/projectTypes/list.spec.js +++ b/src/routes/projectTypes/list.spec.js @@ -80,10 +80,10 @@ describe('LIST project types', () => { }); }); - it('should return 200 even if user is not authenticated', (done) => { + it('should return 403 if user is not authenticated', (done) => { request(server) .get('/v4/projects/metadata/projectTypes') - .expect(200, done); + .expect(403, done); }); it('should return 200 for connect admin', (done) => { From 23d85930ad6e7bb5b439996b54af66b66a70890c Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Wed, 13 Mar 2019 16:34:44 +0530 Subject: [PATCH 07/48] fixing unit tests for orgconfig --- src/routes/orgConfig/list.spec.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/routes/orgConfig/list.spec.js b/src/routes/orgConfig/list.spec.js index 70a3c093..060a1a79 100644 --- a/src/routes/orgConfig/list.spec.js +++ b/src/routes/orgConfig/list.spec.js @@ -110,12 +110,18 @@ describe('LIST organization config', () => { it('should return 422 without filter query param', (done) => { request(server) .get(`${orgConfigPath}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) .expect(422, done); }); it('should return 422 with filter query param but without orgId defined', (done) => { request(server) .get(`${orgConfigPath}?filter=configName=${configs[0].configName}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) .expect(422, done); }); }); From a5a52ff3a66189f9635c830a10ab90951fc6d675 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Thu, 14 Mar 2019 10:21:56 +0800 Subject: [PATCH 08/48] test are now running using a separate instance of PostgreSQL in docker-compose for tests, so we don't have to rebuid docker containers every time we want to switch from app running to tests running. --- README.md | 17 ++++------------- config/test.json | 2 +- local/docker-compose.yml | 8 ++++++++ 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index e577765f..efbd4daf 100644 --- a/README.md +++ b/README.md @@ -98,22 +98,13 @@ To be able to run [Connect App](https://github.com/appirio-tech/connect-app) wit 3. Restart both Connect App and Project Service if they were running. ### Test +```bash +npm run test +``` +Tests are being executed with the `NODE_ENV` environment variable has a value `test` and `config/test.js` configuration is loaded. Each of the individual modules/services are unit tested. -To run unit tests run `npm run test` from root of project. - -While tests are being executed the `NODE_ENV` environment variable has a value `test` and `config/test.js` configuration is loaded. The default test configuration refers to `projectsdb_test` postgres database. So make sure that this database exists before running the tests. Since we are using docker-compose for local deployment change `local/docker-compose.yaml` postgres service with updated database name and re-create the containers. - -``` -// stop already executing containers if any -docker-compose stop -t 1 -// clear the containers -docker-compose rm -f -// re-run the services with build flag -docker-compose up --build -``` - #### JWT Authentication Authentication is handled via Authorization (Bearer) token header field. Token is a JWT token. Here is a sample token that is valid for a very long time for a user with administrator role. ``` diff --git a/config/test.json b/config/test.json index 23ca972e..4e94d1a1 100644 --- a/config/test.json +++ b/config/test.json @@ -14,7 +14,7 @@ "rabbitmqUrl": "amqp://localhost:5672", "connectProjectsUrl": "https://local.topcoder-dev.com/projects/", "dbConfig": { - "masterUrl": "postgres://coder:mysecretpassword@localhost:5432/projectsdb_test", + "masterUrl": "postgres://coder:mysecretpassword@localhost:5433/projectsdb_test", "maxPoolSize": 50, "minPoolSize": 4, "idleTimeout": 1000 diff --git a/local/docker-compose.yml b/local/docker-compose.yml index 73a9edcc..72e6985f 100644 --- a/local/docker-compose.yml +++ b/local/docker-compose.yml @@ -12,6 +12,14 @@ services: - POSTGRES_PASSWORD=mysecretpassword - POSTGRES_USER=coder - POSTGRES_DB=projectsdb + db_test: + image: "postgres:9.5" + ports: + - "5433:5432" + environment: + - POSTGRES_PASSWORD=mysecretpassword + - POSTGRES_USER=coder + - POSTGRES_DB=projectsdb_test esearch: image: "elasticsearch:2.3" ports: From 0af9407101ef02f0de5b5409126e668a162fc405 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Thu, 14 Mar 2019 11:14:56 +0800 Subject: [PATCH 09/48] README update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index efbd4daf..f0417f5e 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Microservice to manage CRUD operations for all things Projects. docker-compose up ``` This will run several services locally: - - `postgres` + - `postgres` - two instances: for app and for unit tests - `elasticsearch` - `rabbitmq` - `mock-services` - mocks some Topcoder API From 023818ac85316c6ec6784903f6a0e9547549f306 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Thu, 14 Mar 2019 16:37:10 +0800 Subject: [PATCH 10/48] fix seed metadata script --- README.md | 10 ++++++---- migrations/seedMetadata.js | 20 ++++++++++++++++---- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f0417f5e..9e1cab7c 100644 --- a/README.md +++ b/README.md @@ -51,9 +51,9 @@ Microservice to manage CRUD operations for all things Projects. ```bash NODE_ENV=development npm run sync:db ``` - This command will crate tables in `postgres` db. + This command will crate tables in `postgres` db. - *NOTE: this will drop tables if they already exist.* + *NOTE: this will drop tables if they already exist.* * Sync ES indices ```bash @@ -72,9 +72,11 @@ Microservice to manage CRUD operations for all things Projects. ### Import sample metadata ```bash -node migrations/seedMetadata.js +CONNECT_USER_TOKEN= node migrations/seedMetadata.js ``` -To create sample metadata entries (duplicate what is currently in development environment). +This command will create sample metadata entries in the DB (duplicate what is currently in development environment). + +To retrieve data from DEV env we need to provide a valid user token. You may login to http://connect.topcoder-dev.com and find the Bearer token in the request headers using browser dev tools. ### Run Connect App with Project Service locally diff --git a/migrations/seedMetadata.js b/migrations/seedMetadata.js index 431daa8f..6fd4b143 100644 --- a/migrations/seedMetadata.js +++ b/migrations/seedMetadata.js @@ -3,19 +3,31 @@ const _ = require('lodash') const axios = require('axios'); const Promise = require('bluebird'); +if (!process.env.CONNECT_USER_TOKEN) { + console.error('This script requires environment variable CONNECT_USER_TOKEN to be defined. Login to http://connect.topcoder-dev.com and get your user token from the requests headers.') + exit(1); +} + +// we need to know any logged in Connect user token to retrieve data from DEV +const CONNECT_USER_TOKEN = process.env.CONNECT_USER_TOKEN; var url = 'https://api.topcoder-dev.com/v4/projects/metadata'; var targetUrl = 'http://localhost:8001/v4/'; var destUrl = targetUrl + 'projects/'; var destTimelines = targetUrl; -axios.get(url) +axios.get(url, { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${CONNECT_USER_TOKEN}` + } +}) .then(async function (response) { let data = response.data; var headers = { 'Content-Type': 'application/json', - 'Authorization': 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw' + 'Authorization': 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw' } @@ -62,11 +74,11 @@ axios.get(url) .catch(e=>resolve()); //ignore the error }) }); - + // handle success console.log('Done'); }).catch(err=>{ - console.log(err); + console.log(err.response.data); }); From 8ce5e6bfdd65264cedb0b10a7d4fb38f8faf1d79 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Thu, 14 Mar 2019 16:51:54 +0800 Subject: [PATCH 11/48] improve error handling --- migrations/seedMetadata.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/seedMetadata.js b/migrations/seedMetadata.js index 6fd4b143..fc2236de 100644 --- a/migrations/seedMetadata.js +++ b/migrations/seedMetadata.js @@ -80,5 +80,5 @@ axios.get(url, { // handle success console.log('Done'); }).catch(err=>{ - console.log(err.response.data); + console.log(err && err.response ? err.response.data : err); }); From 19f0767c84e02fdbca48e3ef051558194c67f489 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Fri, 15 Mar 2019 11:47:47 +0800 Subject: [PATCH 12/48] use M2M token for Identity and Member services improved README to cover two possible ways of running locally with and without M2M token provided --- .eslintignore | 3 +- README.md | 22 ++++++++++---- config/m2m.local.js | 35 +++++++++++++++++++++++ config/{sample.local.js => mock.local.js} | 0 src/tests/serviceMocks.js | 2 ++ src/util.js | 6 ++-- 6 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 config/m2m.local.js rename config/{sample.local.js => mock.local.js} (100%) diff --git a/.eslintignore b/.eslintignore index 5eb52385..7674d60e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,6 @@ config/local.js -config/sample.local.js +config/mock.local.js +config/m2m.local.js node_modules dist .ebextensions diff --git a/README.md b/README.md index f0417f5e..3b5b3f6f 100644 --- a/README.md +++ b/README.md @@ -32,14 +32,22 @@ Microservice to manage CRUD operations for all things Projects. *NOTE: In production these dependencies / services are hosted & managed outside tc-projects-service.* * Local config + + There are two prepared configs: + - if you have M2M environment variables provided: `AUTH0_CLIENT_ID`, `AUTH0_CLIENT_SECRET`, `AUTH0_URL`, `AUTH0_AUDIENCE` then use `config/m2m.local.js` + - otherwise use `config/mock.local.js`. + + To apply any of these config copy it to `config/local.js`: + ```bash - # in the tc-project-service root folder, not inside local/ as above - cp config/sample.local.js config/local.js + cp config/mock.local.js config/local.js + # or + cp config/m2m.local.js config/local.js ``` - Copy `config/sample.local.js` as `config/local.js`.
- As project service depend on many third-party services we have to config how to access them. Some services are run locally and some services are used from Topcoder DEV environment. `config/local.js` has a prepared configuration which would replace values no matter what `NODE_ENV` value is. - **IMPORTANT** This configuration file assumes that services run by docker use domain `dockerhost`. Depend on your system you have to make sure that domain `dockerhost` points to the IP address of docker. + `config/local.js` has a prepared configuration which would replace values no matter what `NODE_ENV` value is. + + **IMPORTANT** These configuration files assume that docker containers are run on domain `dockerhost`. Depend on your system you have to make sure that domain `dockerhost` points to the IP address of docker. For example, you can add a the next line to your `/etc/hosts` file, if docker is run on IP `127.0.0.1`. ``` 127.0.0.1 dockerhost @@ -47,6 +55,10 @@ Microservice to manage CRUD operations for all things Projects. Alternatively, you may update `config/local.js` and replace `dockerhost` with your docker IP address.
You may try using command `docker-machine ip` to get your docker IP, but it works not for all systems. + Explanation of configs: + - `config/mock.local.js` - Use local `mock-services` from docker to mock Identity and Member services instead of using deployed at Topcoder dev environment. + - `config/m2m.local.js` - Use Identity and Member services deployed at Topcoder dev environment. This can be used only if you have M2M environment variables (`AUTH0_CLIENT_ID`, `AUTH0_CLIENT_SECRET`, `AUTH0_URL`, `AUTH0_AUDIENCE`) provided to access Topcoder DEV environment services. + * Create tables in DB ```bash NODE_ENV=development npm run sync:db diff --git a/config/m2m.local.js b/config/m2m.local.js new file mode 100644 index 00000000..9aef91f2 --- /dev/null +++ b/config/m2m.local.js @@ -0,0 +1,35 @@ +// force using test.json for unit tests + +let config; +if (process.env.NODE_ENV === 'test') { + config = require('./test.json'); +} else { + config = { + identityServiceEndpoint: "https://api.topcoder-dev.com/", + authSecret: 'secret', + authDomain: 'topcoder-dev.com', + logLevel: 'debug', + captureLogs: 'false', + logentriesToken: '', + rabbitmqURL: 'amqp://dockerhost:5672', + fileServiceEndpoint: 'https://api.topcoder-dev.com/v3/files/', + directProjectServiceEndpoint: 'https://api.topcoder-dev.com/v3/direct', + connectProjectsUrl: 'https://connect.topcoder-dev.com/projects/', + memberServiceEndpoint: 'https://api.topcoder-dev.com/v3/members', + dbConfig: { + masterUrl: 'postgres://coder:mysecretpassword@dockerhost:5432/projectsdb', + maxPoolSize: 50, + minPoolSize: 4, + idleTimeout: 1000, + }, + elasticsearchConfig: { + host: 'dockerhost:9200', + // target elasticsearch 2.3 version + apiVersion: '2.3', + indexName: 'projects', + docType: 'projectV4' + }, + whitelistedOriginsForUserIdAuth: "[\"\"]", + }; +} +module.exports = config; diff --git a/config/sample.local.js b/config/mock.local.js similarity index 100% rename from config/sample.local.js rename to config/mock.local.js diff --git a/src/tests/serviceMocks.js b/src/tests/serviceMocks.js index 70a2e9aa..662bd2c4 100644 --- a/src/tests/serviceMocks.js +++ b/src/tests/serviceMocks.js @@ -6,6 +6,7 @@ import _ from 'lodash'; // we do need to test elasticsearch indexing import config from 'config'; import elasticsearch from 'elasticsearch'; +import util from '../util'; module.exports = (app) => { _.assign(app.services, { @@ -17,4 +18,5 @@ module.exports = (app) => { }); sinon.stub(app.services.pubsub, 'init', () => Promise.resolve(true)); sinon.stub(app.services.pubsub, 'publish', () => Promise.resolve(true)); + sinon.stub(util, 'getM2MToken', () => Promise.resolve('MOCK_TOKEN')); }; diff --git a/src/util.js b/src/util.js index d54d88d8..853e71c4 100644 --- a/src/util.js +++ b/src/util.js @@ -339,7 +339,7 @@ _.assignIn(util, { */ getMemberDetailsByUserIds: Promise.coroutine(function* (userIds, logger, requestId) { // eslint-disable-line func-names try { - const token = yield this.getSystemUserToken(logger); + const token = yield this.getM2MToken(); const httpClient = this.getHttpClient({ id: requestId, log: logger }); if (logger) { logger.trace(userIds); @@ -364,7 +364,7 @@ _.assignIn(util, { */ getUserRoles: Promise.coroutine(function* (userId, logger, requestId) { // eslint-disable-line func-names try { - const token = yield this.getSystemUserToken(logger); + const token = yield this.getM2MToken(); const httpClient = this.getHttpClient({ id: requestId, log: logger }); return httpClient.get(`${config.identityServiceEndpoint}roles`, { params: { @@ -449,7 +449,7 @@ _.assignIn(util, { filter += '&like=true'; } req.log.trace('filter for users api call', filter); - return util.getSystemUserToken(req.log) + return util.getM2MToken() .then((token) => { req.log.debug(`Bearer ${token}`); const httpClient = util.getHttpClient({ id: req.id, log: req.log }); From 03645d1da7ceefc566f6a25e58f2dab137b4e80b Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Fri, 15 Mar 2019 11:49:01 +0800 Subject: [PATCH 13/48] trigger running tests again --- migrations/seedMetadata.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/migrations/seedMetadata.js b/migrations/seedMetadata.js index fc2236de..6309fe21 100644 --- a/migrations/seedMetadata.js +++ b/migrations/seedMetadata.js @@ -75,8 +75,6 @@ axios.get(url, { }) }); - - // handle success console.log('Done'); }).catch(err=>{ From ac0a14697015d3b64ec749f624e813aad7791b31 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Fri, 15 Mar 2019 12:30:12 +0800 Subject: [PATCH 14/48] mock for identity service /users ednpoint --- local/mock-services/server.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/local/mock-services/server.js b/local/mock-services/server.js index c1f0069f..f43fd1c7 100644 --- a/local/mock-services/server.js +++ b/local/mock-services/server.js @@ -73,6 +73,27 @@ server.get('/v3/members/_search', (req, res) => { res.status(200).json(response); }); + +// add filter route for project members +server.get('/users', (req, res) => { + const filter = req.query.filter.replace('%2520', ' ').replace('%20', ' ').replace('%3D', ' '); + const allEmails = filter.split('=')[1]; + const emails = allEmails.split('OR'); + const cloned = _.cloneDeep(members); + const response = { + id: 'res1', + result: { + success: true, + status: 200, + }, + }; + const users = _.filter(cloned, single => _.includes(emails, single.result.content.email)); + response.result.content = _.map(users, + single => _.assign(single.result.content, { id: single.result.content.userId })); + response.result.metadata = { totalCount: response.result.content.length }; + res.status(200).json(response); +}); + // add additional search route for project members server.get('/roles', (req, res) => { const filter = _.isString(req.query.filter) ? From 8b9c715bed63a617c858941ed09c3cbbf2466965 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Fri, 15 Mar 2019 17:11:35 +0800 Subject: [PATCH 15/48] fully removed system user token usage --- config/custom-environment-variables.json | 2 -- config/default.json | 2 -- src/events/projectMembers/index.js | 4 ++-- src/services/messageService.js | 4 ---- src/util.js | 14 -------------- 5 files changed, 2 insertions(+), 24 deletions(-) diff --git a/config/custom-environment-variables.json b/config/custom-environment-variables.json index c807e407..4260d83e 100644 --- a/config/custom-environment-variables.json +++ b/config/custom-environment-variables.json @@ -22,8 +22,6 @@ "fileServiceEndpoint": "FILE_SERVICE_ENDPOINT", "identityServiceEndpoint": "IDENTITY_SERVICE_ENDPOINT", "memberServiceEndpoint": "MEMBER_SERVICE_ENDPOINT", - "systemUserClientId": "SYSTEM_USER_CLIENT_ID", - "systemUserClientSecret": "SYSTEM_USER_CLIENT_SECRET", "connectProjectsUrl": "CONNECT_PROJECTS_URL", "dbConfig": { "masterUrl": "DB_MASTER_URL", diff --git a/config/default.json b/config/default.json index 4da2970e..fa994859 100644 --- a/config/default.json +++ b/config/default.json @@ -25,8 +25,6 @@ "timelineIndexName": "timelines", "timelineDocType": "timelineV4" }, - "systemUserClientId": "", - "systemUserClientSecret": "", "connectProjectUrl":"", "dbConfig": { "masterUrl": "", diff --git a/src/events/projectMembers/index.js b/src/events/projectMembers/index.js index bab7e486..5dfca77c 100644 --- a/src/events/projectMembers/index.js +++ b/src/events/projectMembers/index.js @@ -48,7 +48,7 @@ const projectMemberAddedHandler = Promise.coroutine(function* a(logger, msg, cha // add copilot/update manager permissions operation promise const directProjectId = yield models.Project.getDirectProjectId(projectId); if (directProjectId) { - const token = yield util.getSystemUserToken(logger); + const token = yield util.getM2MToken(); const req = { id: origRequestId, log: logger, @@ -119,7 +119,7 @@ const projectMemberRemovedHandler = Promise.coroutine(function* (logger, msg, ch if (_.indexOf([PROJECT_MEMBER_ROLE.COPILOT, PROJECT_MEMBER_ROLE.MANAGER], member.role) > -1) { const directProjectId = yield models.Project.getDirectProjectId(projectId); if (directProjectId) { - const token = yield util.getSystemUserToken(logger); + const token = yield util.getM2MToken(); const req = { id: origRequestId, log: logger, diff --git a/src/services/messageService.js b/src/services/messageService.js index 4ea5f04b..949c0134 100644 --- a/src/services/messageService.js +++ b/src/services/messageService.js @@ -65,12 +65,8 @@ async function getClient(logger) { function createTopic(topic, logger) { logger.debug(`createTopic for topic: ${JSON.stringify(topic)}`); return getClient(logger).then((msgClient) => { - // return util.getSystemUserToken(logger).then((adminToken) => { logger.debug('calling message service'); return msgClient.post('/topics/create', topic) - // const httpClient = util.getHttpClient({ id: `topic#create#${topic.referenceId}`, log: logger }); - // httpClient.defaults.headers.common.Authorization = `Bearer ${adminToken}`; - // return httpClient.post(`${config.get('messageApiUrl')}/topics/create`, topic) .then((resp) => { logger.debug('Topic created successfully'); logger.debug(`Topic created successfully [status]: ${resp.status}`); diff --git a/src/util.js b/src/util.js index 853e71c4..b706f7f8 100644 --- a/src/util.js +++ b/src/util.js @@ -261,20 +261,6 @@ _.assignIn(util, { }); }, - getSystemUserToken: (logger, id = 'system') => { - const httpClient = util.getHttpClient({ id, log: logger }); - const url = `${config.get('identityServiceEndpoint')}authorizations`; - const formData = `clientId=${config.get('systemUserClientId')}&` + - `secret=${encodeURIComponent(config.get('systemUserClientSecret'))}`; - return httpClient.post(url, formData, - { - timeout: 4000, - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - }, - ) - .then(res => res.data.result.content.token); - }, - /** * Get machine to machine token. * @returns {Promise} promise which resolves to the m2m token From 7c206b9d435047a9117d25ae3142cf0286680443 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Fri, 15 Mar 2019 17:26:56 +0800 Subject: [PATCH 16/48] handle errors in seed metadata script so we are aware if it was successful or not improved README --- README.md | 4 +- migrations/seedMetadata.js | 88 ++++++++++++++++++++++---------------- 2 files changed, 54 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 85b3b4f5..6d964185 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Microservice to manage CRUD operations for all things Projects. * Local config There are two prepared configs: - - if you have M2M environment variables provided: `AUTH0_CLIENT_ID`, `AUTH0_CLIENT_SECRET`, `AUTH0_URL`, `AUTH0_AUDIENCE` then use `config/m2m.local.js` + - if you have M2M environment variables provided: `AUTH0_CLIENT_ID`, `AUTH0_CLIENT_SECRET`, `AUTH0_URL`, `AUTH0_AUDIENCE`, `AUTH0_PROXY_SERVER_URL` then use `config/m2m.local.js` - otherwise use `config/mock.local.js`. To apply any of these config copy it to `config/local.js`: @@ -57,7 +57,7 @@ Microservice to manage CRUD operations for all things Projects. Explanation of configs: - `config/mock.local.js` - Use local `mock-services` from docker to mock Identity and Member services instead of using deployed at Topcoder dev environment. - - `config/m2m.local.js` - Use Identity and Member services deployed at Topcoder dev environment. This can be used only if you have M2M environment variables (`AUTH0_CLIENT_ID`, `AUTH0_CLIENT_SECRET`, `AUTH0_URL`, `AUTH0_AUDIENCE`) provided to access Topcoder DEV environment services. + - `config/m2m.local.js` - Use Identity and Member services deployed at Topcoder dev environment. This can be used only if you have M2M environment variables (`AUTH0_CLIENT_ID`, `AUTH0_CLIENT_SECRET`, `AUTH0_URL`, `AUTH0_AUDIENCE`, `AUTH0_PROXY_SERVER_URL`) provided to access Topcoder DEV environment services. * Create tables in DB ```bash diff --git a/migrations/seedMetadata.js b/migrations/seedMetadata.js index 6309fe21..790a1e55 100644 --- a/migrations/seedMetadata.js +++ b/migrations/seedMetadata.js @@ -5,7 +5,7 @@ const Promise = require('bluebird'); if (!process.env.CONNECT_USER_TOKEN) { console.error('This script requires environment variable CONNECT_USER_TOKEN to be defined. Login to http://connect.topcoder-dev.com and get your user token from the requests headers.') - exit(1); + process.exit(1); } // we need to know any logged in Connect user token to retrieve data from DEV @@ -16,67 +16,83 @@ var targetUrl = 'http://localhost:8001/v4/'; var destUrl = targetUrl + 'projects/'; var destTimelines = targetUrl; +console.log('Getting metadata from DEV environment...'); + axios.get(url, { headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${CONNECT_USER_TOKEN}` } }) + .catch((err) => { + const errMessage = _.get(err, 'response.data.result.content.message'); + throw errMessage ? new Error('Error during obtaining data from DEV: ' + errMessage) : err + }) .then(async function (response) { let data = response.data; + console.log('Creating metadata objects locally...'); + var headers = { 'Content-Type': 'application/json', 'Authorization': 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw' } - let promises = _(data.result.content.projectTypes).map(pt=>{ - return axios.post(destUrl+'metadata/projectTypes',{param:pt}, {headers:headers}) + return axios + .post(destUrl+'metadata/projectTypes',{param:pt}, {headers:headers}) + .catch((err) => { + const errMessage = _.get(err, 'response.data.result.content.message', ''); + console.log(`Failed to create projectType with key=${pt.key}.`, errMessage) + }) }); - try{ - await Promise.all(promises); - }catch(ex){ - //ignore the error - } - promises = _(data.result.content.projectTemplates).map(pt=>{ - return axios.post(destUrl+'metadata/projectTemplates',{param:pt}, {headers:headers}) - }); - try{ - await Promise.all(promises); - }catch(ex){ - //ignore the error - } + await Promise.all(promises); promises = _(data.result.content.productCategories).map(pt=>{ - return axios.post(destUrl+'metadata/productCategories',{param:pt}, {headers:headers}) + return axios + .post(destUrl+'metadata/productCategories',{param:pt}, {headers:headers}) + .catch((err) => { + const errMessage = _.get(err, 'response.data.result.content.message', ''); + console.log(`Failed to create productCategory with key=${pt.key}.`, errMessage) + }) }); - try{ - await Promise.all(promises); - }catch(ex){ - //ignore the error - } - promises = _(data.result.content.productTemplates).map(pt=>{ - return axios.post(destUrl+'metadata/productTemplates',{param:pt}, {headers:headers}) + await Promise.all(promises); + + promises = _(data.result.content.projectTemplates).map(pt=>{ + return axios + .post(destUrl+'metadata/projectTemplates',{param:pt}, {headers:headers}) + .catch((err) => { + const errMessage = _.get(err, 'response.data.result.content.message', ''); + console.log(`Failed to create projectTemplate with id=${pt.id}.`, errMessage) + }) }); - try{ - await Promise.all(promises); - }catch(ex){ - //ignore the error - } - await Promise.each(data.result.content.milestoneTemplates,pt=>{ - return new Promise((resolve,reject)=>{ - axios.post(destTimelines+'timelines/metadata/milestoneTemplates',{param:pt}, {headers:headers}) - .then(r=>resolve()) - .catch(e=>resolve()); //ignore the error - }) + await Promise.all(promises); + + promises = _(data.result.content.productTemplates).map(pt=>{ + return axios + .post(destUrl+'metadata/productTemplates',{param:pt}, {headers:headers}) + .catch((err) => { + const errMessage = _.get(err, 'response.data.result.content.message', ''); + console.log(`Failed to create productTemplate with id=${pt.id}.`, errMessage) + }) }); + await Promise.all(promises); + + await Promise.each(data.result.content.milestoneTemplates,pt=> ( + axios + .post(destTimelines+'timelines/metadata/milestoneTemplates',{param:pt}, {headers:headers}) + .catch((err) => { + const errMessage = _.get(err, 'response.data.result.content.message', ''); + console.log(`Failed to create milestoneTemplate with id=${pt.id}.`, errMessage) + }) + )); + // handle success console.log('Done'); }).catch(err=>{ - console.log(err && err.response ? err.response.data : err); + console.error(err && err.response ? err.response : err); }); From 5438fb19df021fc94a82dd808ab076e9fabbac73 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Sat, 16 Mar 2019 17:34:04 +0800 Subject: [PATCH 17/48] winning submission from challenge 30086428 - Topcoder Project Service - Create demo data --- README.md | 4 +- local/seed/index.js | 13 ++ local/seed/projects.json | 178 +++++++++++++++++++++ {migrations => local/seed}/seedMetadata.js | 29 ++-- local/seed/seedProjects.js | 74 +++++++++ package.json | 3 +- 6 files changed, 284 insertions(+), 17 deletions(-) create mode 100644 local/seed/index.js create mode 100644 local/seed/projects.json rename {migrations => local/seed}/seedMetadata.js (80%) create mode 100644 local/seed/seedProjects.js diff --git a/README.md b/README.md index 6d964185..c8356d59 100644 --- a/README.md +++ b/README.md @@ -82,9 +82,9 @@ Microservice to manage CRUD operations for all things Projects. Runs the Project Service using nodemon, so it would be restarted after any of the files is updated. The project service will be served on `http://localhost:8001`. -### Import sample metadata +### Import sample metadata & projects ```bash -CONNECT_USER_TOKEN= node migrations/seedMetadata.js +CONNECT_USER_TOKEN= npm run demo-data ``` This command will create sample metadata entries in the DB (duplicate what is currently in development environment). diff --git a/local/seed/index.js b/local/seed/index.js new file mode 100644 index 00000000..c13edcce --- /dev/null +++ b/local/seed/index.js @@ -0,0 +1,13 @@ +const seedMetadata = require('./seedMetadata'); +const seedProjects = require('./seedProjects'); + +const targetUrl = 'http://localhost:8001/v4/'; +const token = + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw'; + +async function seed() { + await seedMetadata(targetUrl, token); + await seedProjects(targetUrl, token); +} + +seed(); diff --git a/local/seed/projects.json b/local/seed/projects.json new file mode 100644 index 00000000..8d4fb65f --- /dev/null +++ b/local/seed/projects.json @@ -0,0 +1,178 @@ +[ + { + "param": { + "name": "Develop app", + "details": { + "utm": { + "code": "R&D" + }, + "appDefinition": { + "primaryTarget": "phone", + "goal": { + "value": "Nothing" + }, + "users": { + "value": "No one" + }, + "notes": "" + }, + "hideDiscussions": true + }, + "description": "Hello this is a sample description... This requires at least 160 characters. I'm trying to satisfy this condition. But I could n't if I don't type this unnecessary message", + "templateId": 1, + "type": "app", + "status": "draft" + } + }, + { + "param": { + "name": "Develop website", + "details": { + "utm": { + "code": "" + }, + "appDefinition": { + "primaryTarget": "phone", + "goal": { + "value": "Nothing" + }, + "users": { + "value": "No one" + }, + "notes": "" + }, + "hideDiscussions": true + }, + "description": "Hello this is a sample description... This requires at least 160 characters. I'm trying to satisfy this condition. But I could n't if I don't type this unnecessary message", + "templateId": 2, + "type": "chatbot", + "status": "in_review" + } + }, + { + "param": { + "name": "Develop website 2", + "details": { + "utm": { + "code": "" + }, + "appDefinition": { + "primaryTarget": "phone", + "goal": { + "value": "Nothing" + }, + "users": { + "value": "No one" + }, + "notes": "" + }, + "hideDiscussions": true + }, + "description": "Hello this is a sample description... This requires at least 160 characters. I'm trying to satisfy this condition. But I could n't if I don't type this unnecessary message", + "templateId": 3, + "type": "website", + "status": "reviewed" + } + }, + { + "param": { + "name": "Develop chatbot", + "details": { + "utm": { + "code": "" + }, + "appDefinition": { + "primaryTarget": "phone", + "goal": { + "value": "Nothing" + }, + "users": { + "value": "No one" + }, + "notes": "" + }, + "hideDiscussions": true + }, + "description": "Hello this is a sample description... This requires at least 160 characters. I'm trying to satisfy this condition. But I could n't if I don't type this unnecessary message", + "templateId": 4, + "type": "chatbot", + "status": "active" + } + }, + { + "param": { + "name": "Develop app 2", + "details": { + "utm": { + "code": "" + }, + "appDefinition": { + "primaryTarget": "phone", + "goal": { + "value": "Nothing" + }, + "users": { + "value": "No one" + }, + "notes": "" + }, + "hideDiscussions": true + }, + "description": "Hello this is a sample description... This requires at least 160 characters. I'm trying to satisfy this condition. But I could n't if I don't type this unnecessary message", + "templateId": 1, + "type": "app", + "status": "completed" + } + }, + { + "param": { + "name": "Develop website 3", + "details": { + "utm": { + "code": "" + }, + "appDefinition": { + "primaryTarget": "phone", + "goal": { + "value": "Nothing" + }, + "users": { + "value": "No one" + }, + "notes": "" + }, + "hideDiscussions": true + }, + "description": "Hello this is a sample description... This requires at least 160 characters. I'm trying to satisfy this condition. But I could n't if I don't type this unnecessary message", + "templateId": 2, + "type": "website", + "status": "paused" + } + }, + { + "param": { + "name": "Develop app 3", + "details": { + "utm": { + "code": "" + }, + "appDefinition": { + "primaryTarget": "phone", + "goal": { + "value": "Nothing" + }, + "users": { + "value": "No one" + }, + "notes": "" + }, + "hideDiscussions": true + }, + "description": "Hello this is a sample description... This requires at least 160 characters. I'm trying to satisfy this condition. But I could n't if I don't type this unnecessary message", + "templateId": 1, + "type": "app", + "status": "cancelled", + "cancelReason": "Test cancel" + } + } +] diff --git a/migrations/seedMetadata.js b/local/seed/seedMetadata.js similarity index 80% rename from migrations/seedMetadata.js rename to local/seed/seedMetadata.js index 790a1e55..5cfef2a3 100644 --- a/migrations/seedMetadata.js +++ b/local/seed/seedMetadata.js @@ -12,18 +12,18 @@ if (!process.env.CONNECT_USER_TOKEN) { const CONNECT_USER_TOKEN = process.env.CONNECT_USER_TOKEN; var url = 'https://api.topcoder-dev.com/v4/projects/metadata'; -var targetUrl = 'http://localhost:8001/v4/'; -var destUrl = targetUrl + 'projects/'; -var destTimelines = targetUrl; - -console.log('Getting metadata from DEV environment...'); - -axios.get(url, { - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${CONNECT_USER_TOKEN}` - } -}) + +module.exports = (targetUrl, token) => { + var destUrl = targetUrl + 'projects/'; + var destTimelines = targetUrl; + + console.log('Getting metadata from DEV environment...'); + return axios.get(url, { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${CONNECT_USER_TOKEN}` + } + }) .catch((err) => { const errMessage = _.get(err, 'response.data.result.content.message'); throw errMessage ? new Error('Error during obtaining data from DEV: ' + errMessage) : err @@ -35,7 +35,7 @@ axios.get(url, { var headers = { 'Content-Type': 'application/json', - 'Authorization': 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw' + 'Authorization': 'Bearer ' + token } let promises = _(data.result.content.projectTypes).map(pt=>{ @@ -92,7 +92,8 @@ axios.get(url, { )); // handle success - console.log('Done'); + console.log('Done metadata seed'); }).catch(err=>{ console.error(err && err.response ? err.response : err); }); +} diff --git a/local/seed/seedProjects.js b/local/seed/seedProjects.js new file mode 100644 index 00000000..36cc3d79 --- /dev/null +++ b/local/seed/seedProjects.js @@ -0,0 +1,74 @@ +const axios = require('axios'); +const Promise = require('bluebird'); +const _ = require('lodash'); + +const projects = require('./projects.json'); + +/** + * Create projects and update their statuses. + */ +module.exports = (targetUrl, token) => { + let projectPromises; + + const projectsUrl = `${targetUrl}projects`; + const headers = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }; + + console.log('Creating projects'); + projectPromises = projects.map((project, i) => { + const status = _.get(project, 'param.status'); + const cancelReason = _.get(project, 'param.cancelReason'); + delete project.param.status; + delete project.param.cancelReason; + + return axios + .post(projectsUrl, project, { headers }) + .catch((err) => { + console.log(`Failed to create project ${i}: ${err.message}`); + }) + .then((response) => { + const projectId = _.get(response, 'data.result.content.id'); + + return { + projectId, + status, + cancelReason, + }; + }); + }); + + return Promise.all(projectPromises) + .then((createdProjects) => { + console.log('Updating statuses'); + return Promise.all( + createdProjects.map(({ projectId, status, cancelReason }) => + updateProjectStatus(projectId, { status, cancelReason }, targetUrl, headers).catch((ex) => { + console.log(`Failed to update project status of project with id ${projectId}: ${ex.message}`); + }), + ), + ); + }) + .then(() => console.log('Done project seed.')) + .catch(ex => console.error(ex)); +}; + +function updateProjectStatus(project, updateParams, targetUrl, headers) { + const projectUpdateUrl = `${targetUrl}projects/${project}`; + + // only cancelled status requires cancelReason + if (updateParams.status !== 'cancelled') { + delete updateParams.cancelReason; + } + + return axios.patch( + projectUpdateUrl, + { + param: updateParams, + }, + { + headers, + }, + ); +} diff --git a/package.json b/package.json index d96b1164..c1921857 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "start:dev": "NODE_ENV=development PORT=8001 nodemon -w src --exec \"babel-node src --presets es2015\" | ./node_modules/.bin/bunyan", "test": "NODE_ENV=test npm run lint && NODE_ENV=test npm run sync:es && NODE_ENV=test npm run sync:db && NODE_ENV=test ./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha -- --timeout 10000 --compilers js:babel-core/register $(find src -path '*spec.js*')", "test:watch": "NODE_ENV=test ./node_modules/.bin/mocha -w --compilers js:babel-core/register $(find src -path '*spec.js*')", - "seed": "babel-node src/tests/seed.js --presets es2015" + "seed": "babel-node src/tests/seed.js --presets es2015", + "demo-data": "babel-node local/seed" }, "repository": { "type": "git", From ed3e6254eb0a8ab3530a3f68673e9ca6f9ad9a51 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Sat, 16 Mar 2019 17:37:20 +0800 Subject: [PATCH 18/48] skip linting for seed scripts - this should be fixed --- .eslintignore | 1 + local/seed/seedMetadata.js | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintignore b/.eslintignore index 7674d60e..87d8ad2d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,6 +1,7 @@ config/local.js config/mock.local.js config/m2m.local.js +local/seed/ node_modules dist .ebextensions diff --git a/local/seed/seedMetadata.js b/local/seed/seedMetadata.js index 5cfef2a3..eca3aa20 100644 --- a/local/seed/seedMetadata.js +++ b/local/seed/seedMetadata.js @@ -1,4 +1,3 @@ -/* eslint-disable */ const _ = require('lodash') const axios = require('axios'); const Promise = require('bluebird'); From 7d5f1b2212df790fb7c472cc595e4756a6ff48a8 Mon Sep 17 00:00:00 2001 From: Vikas Agarwal Date: Mon, 18 Mar 2019 16:02:12 +0530 Subject: [PATCH 19/48] Github issue#277,Changes required for project member invite update endpoint Potential fix --- src/routes/projectMemberInvites/update.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/routes/projectMemberInvites/update.js b/src/routes/projectMemberInvites/update.js index 6e1e0945..e71ddeda 100644 --- a/src/routes/projectMemberInvites/update.js +++ b/src/routes/projectMemberInvites/update.js @@ -113,10 +113,23 @@ module.exports = [ .then((members) => { req.context = req.context || {}; req.context.currentProjectMembers = members; + let userId = updatedInvite.userId; + // if the requesting user is updating his/her own invite + if (!userId && req.authUser.email === updatedInvite.email) { + userId = req.authUser.userId; + } + // if we are not able to identify the user yet, it must be something wrong and we should not create + // project member + if (!userId) { + const err = new Error( + `Unable to find userId for the invite. ${updatedInvite.email} has not joined topcoder yet.`); + err.status = 400; + return next(err); + } const member = { projectId, role: updatedInvite.role, - userId: _.get(updatedInvite, 'userId', req.authUser.userId), + userId, createdBy: req.authUser.userId, updatedBy: req.authUser.userId, }; From 7cf5810af226a8b66ea862345a3dc7eda08da059 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 19 Mar 2019 11:35:52 +0800 Subject: [PATCH 20/48] winning submission from challenge 30086547 - Topcoder Connect - Projects for copilots --- local/seed/index.js | 2 +- local/seed/projects.json | 86 ++++++++++++++ local/seed/seedProjects.js | 58 ++++++++- src/models/project.js | 12 +- src/permissions/project.view.js | 5 +- .../projectMemberInvites/create.spec.js | 27 +++-- src/routes/projectMembers/create.spec.js | 110 ++++++++++++++---- src/routes/projects/list-db.js | 3 +- src/routes/projects/list-db.spec.js | 7 +- src/routes/projects/list.js | 2 +- src/routes/projects/list.spec.js | 7 +- 11 files changed, 265 insertions(+), 54 deletions(-) diff --git a/local/seed/index.js b/local/seed/index.js index c13edcce..a98fcc1c 100644 --- a/local/seed/index.js +++ b/local/seed/index.js @@ -10,4 +10,4 @@ async function seed() { await seedProjects(targetUrl, token); } -seed(); +seed().then(() => process.exit()); diff --git a/local/seed/projects.json b/local/seed/projects.json index 8d4fb65f..7e9cb032 100644 --- a/local/seed/projects.json +++ b/local/seed/projects.json @@ -174,5 +174,91 @@ "status": "cancelled", "cancelReason": "Test cancel" } + }, + { + "param": { + "name": "Reviewed project with copilot invited", + "details": { + "utm": { + "code": "" + }, + "appDefinition": { + "primaryTarget": "phone", + "goal": { + "value": "Nothing" + }, + "users": { + "value": "No one" + }, + "notes": "" + }, + "hideDiscussions": true + }, + "description": "Hello this is a sample description... This requires at least 160 characters. I'm trying to satisfy this condition. But I could n't if I don't type this unnecessary message", + "templateId": 3, + "type": "website", + "status": "reviewed", + "invites": [{ + "param": { + "userIds": [40051332], + "role": "copilot" + }}] + } + }, + { + "param": { + "name": "Reviewed project with copilot as a member with copilot role", + "details": { + "utm": { + "code": "" + }, + "appDefinition": { + "primaryTarget": "phone", + "goal": { + "value": "Nothing" + }, + "users": { + "value": "No one" + }, + "notes": "" + }, + "hideDiscussions": true + }, + "description": "Hello this is a sample description... This requires at least 160 characters. I'm trying to satisfy this condition. But I could n't if I don't type this unnecessary message", + "templateId": 3, + "type": "website", + "status": "reviewed", + "invites": [{ + "param": { + "userIds": [40051332], + "role": "copilot" + }}], + "acceptInvitation": true + } + }, + { + "param": { + "name": "Reviewed project when copilot is not a member and not invited", + "details": { + "utm": { + "code": "" + }, + "appDefinition": { + "primaryTarget": "phone", + "goal": { + "value": "Nothing" + }, + "users": { + "value": "No one" + }, + "notes": "" + }, + "hideDiscussions": true + }, + "description": "Hello this is a sample description... This requires at least 160 characters. I'm trying to satisfy this condition. But I could n't if I don't type this unnecessary message", + "templateId": 3, + "type": "website", + "status": "reviewed" + } } ] diff --git a/local/seed/seedProjects.js b/local/seed/seedProjects.js index 36cc3d79..9352ff66 100644 --- a/local/seed/seedProjects.js +++ b/local/seed/seedProjects.js @@ -1,7 +1,8 @@ +import util from '../../src/tests/util'; + const axios = require('axios'); const Promise = require('bluebird'); const _ = require('lodash'); - const projects = require('./projects.json'); /** @@ -16,21 +17,53 @@ module.exports = (targetUrl, token) => { Authorization: `Bearer ${token}`, }; + const adminHeaders = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${util.jwts.connectAdmin}`, + }; + console.log('Creating projects'); projectPromises = projects.map((project, i) => { const status = _.get(project, 'param.status'); const cancelReason = _.get(project, 'param.cancelReason'); + const invites = _.cloneDeep(_.get(project, 'param.invites')); + const acceptInvitation = _.get(project, 'param.acceptInvitation'); + delete project.param.status; delete project.param.cancelReason; + delete project.param.invites; + delete project.param.acceptInvitation; return axios .post(projectsUrl, project, { headers }) .catch((err) => { console.log(`Failed to create project ${i}: ${err.message}`); }) - .then((response) => { + .then(async (response) => { const projectId = _.get(response, 'data.result.content.id'); + if (Array.isArray(invites)) { + let promises = [] + invites.forEach(invite => { + promises.push(createProjectMemberInvite(projectId, invite, targetUrl, headers)) + }) + const responses = await Promise.all(promises) + if (acceptInvitation) { + let acceptInvitationPromises = [] + responses.forEach(response => { + const userId = _.get(response, 'data.result.content.success[0].userId') + acceptInvitationPromises.push(updateProjectMemberInvite(projectId, { + param: { + userId, + status: 'accepted' + } + }, targetUrl, adminHeaders)) + }) + + await Promise.all(acceptInvitationPromises) + } + } + return { projectId, status, @@ -72,3 +105,24 @@ function updateProjectStatus(project, updateParams, targetUrl, headers) { }, ); } + +function createProjectMemberInvite(projectId, params, targetUrl, headers) { + const projectMemberInviteUrl = `${targetUrl}projects/${projectId}/members/invite`; + + return axios + .post(projectMemberInviteUrl, params, { headers }) + .catch((err) => { + console.log(`Failed to create project member invites ${projectId}: ${err.message}`); + }) +} + +function updateProjectMemberInvite(projectId, params, targetUrl, headers) { + const updateProjectMemberInviteUrl = `${targetUrl}projects/${projectId}/members/invite`; + + return axios + .put(updateProjectMemberInviteUrl, params, { headers }) + .catch((err) => { + console.log(`Failed to update project member invites ${projectId}: ${err.message}`); + }) +} + diff --git a/src/models/project.js b/src/models/project.js index 5339b5b4..4e23efc7 100644 --- a/src/models/project.js +++ b/src/models/project.js @@ -1,7 +1,7 @@ /* eslint-disable valid-jsdoc */ import _ from 'lodash'; -import { PROJECT_STATUS, PROJECT_MEMBER_ROLE } from '../constants'; +import { PROJECT_STATUS } from '../constants'; module.exports = function defineProject(sequelize, DataTypes) { const Project = sequelize.define('Project', { @@ -64,18 +64,18 @@ module.exports = function defineProject(sequelize, DataTypes) { /* * @Co-pilots should be able to view projects any of the following conditions are met: * a. they are registered active project members on the project - * b. any project that is in 'reviewed' state AND does not yet have a co-pilot assigned + * b. any project that is in 'reviewed' state AND copilot is invited * @param userId the id of user */ getProjectIdsForCopilot(userId) { return this.findAll({ where: { $or: [ + ['"Project".status=? AND EXISTS(SELECT * FROM "project_member_invites" WHERE "deletedAt" ' + + 'IS NULL AND "projectId" = "Project".id ' + + 'AND "status" IN (\'requested\', \'pending\') AND "userId" = ? )', PROJECT_STATUS.REVIEWED, userId], ['EXISTS(SELECT * FROM "project_members" WHERE "deletedAt" ' + - 'IS NULL AND "projectId" = "Project".id AND "userId" = ? )', userId], - ['"Project".status=? AND NOT EXISTS(SELECT * FROM "project_members" WHERE ' + - ' "deletedAt" IS NULL AND "projectId" = "Project".id AND "role" = ? )', - PROJECT_STATUS.REVIEWED, PROJECT_MEMBER_ROLE.COPILOT], + 'IS NULL AND "projectId" = "Project".id AND "userId" = ? )', userId], ], }, attributes: ['id'], diff --git a/src/permissions/project.view.js b/src/permissions/project.view.js index 61e4ebed..3701e059 100644 --- a/src/permissions/project.view.js +++ b/src/permissions/project.view.js @@ -24,8 +24,7 @@ module.exports = freq => new Promise((resolve, reject) => { || util.hasRoles(req, MANAGER_ROLES) || !_.isUndefined(_.find(members, m => m.userId === currentUserId)); - // if user is co-pilot and the project doesn't have any copilots then - // user can access the project + // if user is co-pilot and he is a member or if project is in "reviewed" status and he is invited if (!hasAccess && util.hasRole(req, USER_ROLE.COPILOT)) { return models.Project.getProjectIdsForCopilot(currentUserId) .then((ids) => { @@ -53,7 +52,7 @@ module.exports = freq => new Promise((resolve, reject) => { .then((project) => { if (!project || [PROJECT_STATUS.DRAFT, PROJECT_STATUS.IN_REVIEW].indexOf(project.status) >= 0) { errorMessage = 'Copilot: Project is not yet available to copilots'; - } else { + } else if (project.status !== PROJECT_STATUS.REVIEWED) { // project status is 'active' or higher so it's not available to copilots errorMessage = 'Copilot: Project has already started'; } diff --git a/src/routes/projectMemberInvites/create.spec.js b/src/routes/projectMemberInvites/create.spec.js index 74f6be9c..e9701f99 100644 --- a/src/routes/projectMemberInvites/create.spec.js +++ b/src/routes/projectMemberInvites/create.spec.js @@ -65,18 +65,27 @@ describe('Project Member Invite create', () => { lastActivityUserId: '1', }).then((p2) => { project2 = p2; - models.ProjectMemberInvite.create({ - projectId: project1.id, - userId: 40051335, - email: null, - role: PROJECT_MEMBER_ROLE.MANAGER, - status: INVITE_STATUS.PENDING, + models.ProjectMember.create({ + userId: 40051332, + projectId: project2.id, + role: 'copilot', + isPrimary: true, createdBy: 1, updatedBy: 1, - createdAt: '2016-06-30 00:33:07+00', - updatedAt: '2016-06-30 00:33:07+00', }).then(() => { - done(); + models.ProjectMemberInvite.create({ + projectId: project1.id, + userId: 40051335, + email: null, + role: PROJECT_MEMBER_ROLE.MANAGER, + status: INVITE_STATUS.PENDING, + createdBy: 1, + updatedBy: 1, + createdAt: '2016-06-30 00:33:07+00', + updatedAt: '2016-06-30 00:33:07+00', + }).then(() => { + done(); + }); }); })); }); diff --git a/src/routes/projectMembers/create.spec.js b/src/routes/projectMembers/create.spec.js index f55ad90a..f6c85633 100644 --- a/src/routes/projectMembers/create.spec.js +++ b/src/routes/projectMembers/create.spec.js @@ -60,7 +60,7 @@ describe('Project Members create', () => { .expect(403, done); }); - it('should return 201 and then 400 if user is already registered', (done) => { + it('should return 201 when invited then accepted and then 404 if user is already as a member', (done) => { const mockHttpClient = _.merge(testUtil.mockHttpClient, { get: () => Promise.resolve({ status: 200, @@ -79,9 +79,15 @@ describe('Project Members create', () => { }); sandbox.stub(util, 'getHttpClient', () => mockHttpClient); request(server) - .post(`/v4/projects/${project1.id}/members/`) + .post(`/v4/projects/${project1.id}/members/invite`) .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send({ + param: { + userIds: [40051332], + role: 'copilot', + }, }) .expect('Content-Type', /json/) .expect(201) @@ -89,26 +95,59 @@ describe('Project Members create', () => { if (err) { done(err); } else { - const resJson = res.body.result.content; + const resJson = res.body.result.content.success[0]; should.exist(resJson); resJson.role.should.equal('copilot'); resJson.projectId.should.equal(project1.id); resJson.userId.should.equal(40051332); - server.services.pubsub.publish.calledWith('project.member.added').should.be.true; + server.services.pubsub.publish.calledWith('project.member.invite.created').should.be.true; request(server) - .post(`/v4/projects/${project1.id}/members/`) + .put(`/v4/projects/${project1.id}/members/invite`) .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .send({ + param: { + userId: 40051332, + status: 'accepted', + }, }) .expect('Content-Type', /json/) - .expect(400) + .expect(200) .end((err2, res2) => { if (err2) { - done(err); + done(err2); } else { - res2.body.result.status.should.equal(400); - done(); + const resJson2 = res2.body.result.content; + should.exist(resJson2); + resJson2.role.should.equal('copilot'); + resJson2.projectId.should.equal(project1.id); + resJson2.userId.should.equal(40051332); + server.services.pubsub.publish.calledWith('project.member.invite.updated').should.be.true; + server.services.pubsub.publish.calledWith('project.member.added').should.be.true; + + request(server) + .put(`/v4/projects/${project1.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .send({ + param: { + userId: 40051332, + status: 'accepted', + }, + }) + .expect('Content-Type', /json/) + .expect(404) + .end((err3, res3) => { + if (err3) { + done(err3); + } else { + res3.body.result.status.should.equal(404); + done(); + } + }); } }); } @@ -238,28 +277,53 @@ describe('Project Members create', () => { it('sends single BUS_API_EVENT.PROJECT_TEAM_UPDATED message when copilot added', (done) => { request(server) - .post(`/v4/projects/${project1.id}/members/`) + .post(`/v4/projects/${project1.id}/members/invite`) .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, + Authorization: `Bearer ${testUtil.jwts.admin}`, }) .send({ + param: { + userIds: [40051332], + role: 'copilot', + }, }) .expect(201) .end((err) => { if (err) { done(err); } else { - testUtil.wait(() => { - createEventSpy.calledTwice.should.be.true; - createEventSpy.firstCall.calledWith(BUS_API_EVENT.MEMBER_JOINED_COPILOT); - createEventSpy.secondCall.calledWith(BUS_API_EVENT.PROJECT_TEAM_UPDATED, sinon.match({ - projectId: project1.id, - projectName: project1.name, - projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`, + request(server) + .put(`/v4/projects/${project1.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .send({ + param: { userId: 40051332, - initiatorUserId: 40051332, - })).should.be.true; - done(); + status: 'accepted', + }, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err2) => { + if (err2) { + done(err2); + } else { + testUtil.wait(() => { + createEventSpy.callCount.should.equal(4); + createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_MEMBER_INVITE_REQUESTED); + createEventSpy.secondCall.calledWith(BUS_API_EVENT.PROJECT_MEMBER_INVITE_UPDATED); + createEventSpy.thirdCall.calledWith(BUS_API_EVENT.MEMBER_JOINED_COPILOT); + createEventSpy.lastCall.calledWith(BUS_API_EVENT.PROJECT_TEAM_UPDATED, sinon.match({ + projectId: project1.id, + projectName: project1.name, + projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`, + userId: 40051336, + initiatorUserId: 40051336, + })).should.be.true; + done(); + }); + } }); } }); diff --git a/src/routes/projects/list-db.js b/src/routes/projects/list-db.js index 3e10736f..6d19428f 100644 --- a/src/routes/projects/list-db.js +++ b/src/routes/projects/list-db.js @@ -133,11 +133,12 @@ module.exports = [ } // If user requested projects where he/she is a member or // if they are not a copilot then return projects that they are members in. - // Copilots can view projects that they are members in or they have + // Copilots can view projects that they are members in or they are invited // const getProjectIds = !memberOnly && util.hasRole(req, USER_ROLE.COPILOT) ? models.Project.getProjectIdsForCopilot(req.authUser.userId) : models.ProjectMember.getProjectIdsForUser(req.authUser.userId); + return getProjectIds .then((accessibleProjectIds) => { let allowedProjectIds = accessibleProjectIds; diff --git a/src/routes/projects/list-db.spec.js b/src/routes/projects/list-db.spec.js index 08f4d14c..f7022e51 100644 --- a/src/routes/projects/list-db.spec.js +++ b/src/routes/projects/list-db.spec.js @@ -184,8 +184,7 @@ describe('LIST Project db', () => { }); }); - it('should return the project when project that is in reviewed state AND does not yet' + - 'have a co-pilot assigned', (done) => { + it('should return the project when project that is in reviewed state in which the copilot is its member or has been invited', (done) => { request(server) .get('/v4/projects/db/') .set({ @@ -198,9 +197,9 @@ describe('LIST Project db', () => { done(err); } else { const resJson = res.body.result.content; - res.body.result.metadata.totalCount.should.equal(3); + res.body.result.metadata.totalCount.should.equal(2); should.exist(resJson); - resJson.should.have.lengthOf(3); + resJson.should.have.lengthOf(2); done(); } }); diff --git a/src/routes/projects/list.js b/src/routes/projects/list.js index d9be9f66..88325822 100755 --- a/src/routes/projects/list.js +++ b/src/routes/projects/list.js @@ -438,7 +438,7 @@ module.exports = [ } // If user requested projects where he/she is a member or // if they are not a copilot then return projects that they are members in. - // Copilots can view projects that they are members in or they have + // Copilots can view projects that they are members in or they are invited // const getProjectIds = !memberOnly && util.hasRole(req, USER_ROLE.COPILOT) ? models.Project.getProjectIdsForCopilot(req.authUser.userId) : diff --git a/src/routes/projects/list.spec.js b/src/routes/projects/list.spec.js index dfecec90..3edc9f4c 100644 --- a/src/routes/projects/list.spec.js +++ b/src/routes/projects/list.spec.js @@ -303,8 +303,7 @@ describe('LIST Project', () => { }); }); - it('should return the project when project that is in reviewed state AND does not yet ' + - 'have a co-pilot assigned', (done) => { + it('should return the project when project that is in reviewed state in which the copilot is its member or has been invited', (done) => { request(server) .get('/v4/projects') .set({ @@ -317,9 +316,9 @@ describe('LIST Project', () => { done(err); } else { const resJson = res.body.result.content; - res.body.result.metadata.totalCount.should.equal(3); + res.body.result.metadata.totalCount.should.equal(2); should.exist(resJson); - resJson.should.have.lengthOf(3); + resJson.should.have.lengthOf(2); done(); } }); From 5b99768506e3d786aefe5bbb7c57a3993560f3cc Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 19 Mar 2019 11:51:25 +0800 Subject: [PATCH 21/48] use pshah_copilot as demo user for project with copilot --- local/seed/projects.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/local/seed/projects.json b/local/seed/projects.json index 7e9cb032..9fb184af 100644 --- a/local/seed/projects.json +++ b/local/seed/projects.json @@ -200,7 +200,7 @@ "status": "reviewed", "invites": [{ "param": { - "userIds": [40051332], + "userIds": [40152855], "role": "copilot" }}] } @@ -230,7 +230,7 @@ "status": "reviewed", "invites": [{ "param": { - "userIds": [40051332], + "userIds": [40152855], "role": "copilot" }}], "acceptInvitation": true From 63c7d65534586daa678e574b26df91725334a072 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 19 Mar 2019 12:30:24 +0800 Subject: [PATCH 22/48] fix seed script to wait until created projects are indexed in ES before starting updating statuses --- local/seed/seedProjects.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/local/seed/seedProjects.js b/local/seed/seedProjects.js index 9352ff66..d503a630 100644 --- a/local/seed/seedProjects.js +++ b/local/seed/seedProjects.js @@ -74,14 +74,18 @@ module.exports = (targetUrl, token) => { return Promise.all(projectPromises) .then((createdProjects) => { - console.log('Updating statuses'); - return Promise.all( - createdProjects.map(({ projectId, status, cancelReason }) => - updateProjectStatus(projectId, { status, cancelReason }, targetUrl, headers).catch((ex) => { - console.log(`Failed to update project status of project with id ${projectId}: ${ex.message}`); - }), - ), - ); + console.log('Wait 5 seconds to give time ES to index created projects...'); + return Promise.delay(5000).then(() => { + console.log('Updating statuses...'); + + return Promise.all( + createdProjects.map(({ projectId, status, cancelReason }) => + updateProjectStatus(projectId, { status, cancelReason }, targetUrl, headers).catch((ex) => { + console.log(`Failed to update project status of project with id ${projectId}: ${ex.message}`); + }), + ), + ) + }); }) .then(() => console.log('Done project seed.')) .catch(ex => console.error(ex)); From b3d1cb84a93977a7d6d2f638720e033c515e6c35 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 19 Mar 2019 12:30:33 +0800 Subject: [PATCH 23/48] fix m2m config --- config/m2m.local.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/m2m.local.js b/config/m2m.local.js index 9aef91f2..8fb26130 100644 --- a/config/m2m.local.js +++ b/config/m2m.local.js @@ -5,7 +5,7 @@ if (process.env.NODE_ENV === 'test') { config = require('./test.json'); } else { config = { - identityServiceEndpoint: "https://api.topcoder-dev.com/", + identityServiceEndpoint: "https://api.topcoder-dev.com/v3/", authSecret: 'secret', authDomain: 'topcoder-dev.com', logLevel: 'debug', From 8a3c1feb8e27302e57b6a0c6d296d6f9a446f295 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 19 Mar 2019 15:23:18 +0800 Subject: [PATCH 24/48] updated so copilots cannot access projects until they accept invitation - so basically copilots have the same rights to see list of project and project details as other regular non-privilage users --- src/models/project.js | 3 --- src/routes/projects/list-db.js | 12 ++++-------- src/routes/projects/list.js | 12 ++++-------- 3 files changed, 8 insertions(+), 19 deletions(-) diff --git a/src/models/project.js b/src/models/project.js index 4e23efc7..759c8f98 100644 --- a/src/models/project.js +++ b/src/models/project.js @@ -71,9 +71,6 @@ module.exports = function defineProject(sequelize, DataTypes) { return this.findAll({ where: { $or: [ - ['"Project".status=? AND EXISTS(SELECT * FROM "project_member_invites" WHERE "deletedAt" ' + - 'IS NULL AND "projectId" = "Project".id ' + - 'AND "status" IN (\'requested\', \'pending\') AND "userId" = ? )', PROJECT_STATUS.REVIEWED, userId], ['EXISTS(SELECT * FROM "project_members" WHERE "deletedAt" ' + 'IS NULL AND "projectId" = "Project".id AND "userId" = ? )', userId], ], diff --git a/src/routes/projects/list-db.js b/src/routes/projects/list-db.js index 6d19428f..c01b138f 100644 --- a/src/routes/projects/list-db.js +++ b/src/routes/projects/list-db.js @@ -1,7 +1,7 @@ import _ from 'lodash'; import Promise from 'bluebird'; import models from '../../models'; -import { USER_ROLE, MANAGER_ROLES } from '../../constants'; +import { MANAGER_ROLES } from '../../constants'; import util from '../../util'; /** @@ -131,13 +131,9 @@ module.exports = [ .then(result => res.json(util.wrapResponse(req.id, result.rows, result.count))) .catch(err => next(err)); } - // If user requested projects where he/she is a member or - // if they are not a copilot then return projects that they are members in. - // Copilots can view projects that they are members in or they are invited - // - const getProjectIds = !memberOnly && util.hasRole(req, USER_ROLE.COPILOT) ? - models.Project.getProjectIdsForCopilot(req.authUser.userId) : - models.ProjectMember.getProjectIdsForUser(req.authUser.userId); + + // regular users can only see projects they are members of (or invited, handled bellow) + const getProjectIds = models.ProjectMember.getProjectIdsForUser(req.authUser.userId); return getProjectIds .then((accessibleProjectIds) => { diff --git a/src/routes/projects/list.js b/src/routes/projects/list.js index 88325822..73cd3f5d 100755 --- a/src/routes/projects/list.js +++ b/src/routes/projects/list.js @@ -5,7 +5,7 @@ import _ from 'lodash'; import config from 'config'; import models from '../../models'; -import { USER_ROLE, MANAGER_ROLES } from '../../constants'; +import { MANAGER_ROLES } from '../../constants'; import util from '../../util'; const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName'); @@ -436,13 +436,9 @@ module.exports = [ .then(result => res.json(util.wrapResponse(req.id, result.rows, result.count))) .catch(err => next(err)); } - // If user requested projects where he/she is a member or - // if they are not a copilot then return projects that they are members in. - // Copilots can view projects that they are members in or they are invited - // - const getProjectIds = !memberOnly && util.hasRole(req, USER_ROLE.COPILOT) ? - models.Project.getProjectIdsForCopilot(req.authUser.userId) : - models.ProjectMember.getProjectIdsForUser(req.authUser.userId); + + // regular users can only see projects they are members of (or invited, handled bellow) + const getProjectIds = models.ProjectMember.getProjectIdsForUser(req.authUser.userId); return getProjectIds .then((accessibleProjectIds) => { From 6df79f8dac710264691f99872c87a299632905c1 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 19 Mar 2019 15:24:08 +0800 Subject: [PATCH 25/48] fix seed projects script so it waits until ES index is done for previous request --- local/seed/seedProjects.js | 43 +++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/local/seed/seedProjects.js b/local/seed/seedProjects.js index d503a630..e1d50417 100644 --- a/local/seed/seedProjects.js +++ b/local/seed/seedProjects.js @@ -5,6 +5,9 @@ const Promise = require('bluebird'); const _ = require('lodash'); const projects = require('./projects.json'); +// we make delay after requests which has to be indexed in ES asynchronous +const ES_INDEX_DELAY = 3000; + /** * Create projects and update their statuses. */ @@ -12,12 +15,12 @@ module.exports = (targetUrl, token) => { let projectPromises; const projectsUrl = `${targetUrl}projects`; - const headers = { + const adminHeaders = { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }; - const adminHeaders = { + const connectAdminHeaders = { 'Content-Type': 'application/json', Authorization: `Bearer ${util.jwts.connectAdmin}`, }; @@ -35,18 +38,32 @@ module.exports = (targetUrl, token) => { delete project.param.acceptInvitation; return axios - .post(projectsUrl, project, { headers }) + .post(projectsUrl, project, { headers: adminHeaders }) .catch((err) => { console.log(`Failed to create project ${i}: ${err.message}`); }) .then(async (response) => { const projectId = _.get(response, 'data.result.content.id'); + // updating status + if (status !== _.get(response, 'data.result.content.status')) { + console.log(`Project #${projectId}: Wait a bit to give time ES to index before updating status...`); + await Promise.delay(ES_INDEX_DELAY); + await updateProjectStatus(projectId, { status, cancelReason }, targetUrl, adminHeaders).catch((ex) => { + console.error(`Project #${projectId}: Failed to update project status: ${ex.message}`); + }); + } + + // creating invitations if (Array.isArray(invites)) { let promises = [] invites.forEach(invite => { - promises.push(createProjectMemberInvite(projectId, invite, targetUrl, headers)) + promises.push(createProjectMemberInvite(projectId, invite, targetUrl, connectAdminHeaders)) }) + + // accepting invitations + console.log(`Project #${projectId}: Wait a bit to give time ES to index before creating invitation...`); + await Promise.delay(ES_INDEX_DELAY); const responses = await Promise.all(promises) if (acceptInvitation) { let acceptInvitationPromises = [] @@ -57,9 +74,11 @@ module.exports = (targetUrl, token) => { userId, status: 'accepted' } - }, targetUrl, adminHeaders)) + }, targetUrl, connectAdminHeaders)) }) + console.log(`Project #${projectId}: Wait a bit to give time ES to index before accepting invitation...`); + await Promise.delay(ES_INDEX_DELAY); await Promise.all(acceptInvitationPromises) } } @@ -73,20 +92,6 @@ module.exports = (targetUrl, token) => { }); return Promise.all(projectPromises) - .then((createdProjects) => { - console.log('Wait 5 seconds to give time ES to index created projects...'); - return Promise.delay(5000).then(() => { - console.log('Updating statuses...'); - - return Promise.all( - createdProjects.map(({ projectId, status, cancelReason }) => - updateProjectStatus(projectId, { status, cancelReason }, targetUrl, headers).catch((ex) => { - console.log(`Failed to update project status of project with id ${projectId}: ${ex.message}`); - }), - ), - ) - }); - }) .then(() => console.log('Done project seed.')) .catch(ex => console.error(ex)); }; From d2dad3f5ccc5806db5e3c21be795082abca071ec Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 19 Mar 2019 15:33:27 +0800 Subject: [PATCH 26/48] update README --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index c8356d59..e2790ac4 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,8 @@ Microservice to manage CRUD operations for all things Projects. *NOTE: This will first clear all the indices and than recreate them. So use with caution.* * Run + + **NOTE** If you use `config/m2m.local.js` config, you should set M2M environment variables before running the next command. ```bash npm run start:dev ``` @@ -127,6 +129,9 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI It's been signed with the secret 'secret'. This secret should match your entry in config/local.js. You can generate your own token using https://jwt.io ### Local Deployment + +**NOTE: This part of README may contain inconsistencies and requires update. Don't follow it unless you know how to properly make configuration for these steps. It's not needed for regular development process.** + Build image: `docker build -t tc_projects_services .` Run image: From 4424e700bd07cfba7b2dab8b217e62597bed2324 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 19 Mar 2019 15:48:59 +0800 Subject: [PATCH 27/48] removed special permissions for copilots to view projects and remove customer error messages for copilots as they cannot be shown anymore --- src/permissions/project.view.js | 40 ++------------------------------- 1 file changed, 2 insertions(+), 38 deletions(-) diff --git a/src/permissions/project.view.js b/src/permissions/project.view.js index 3701e059..e14049ea 100644 --- a/src/permissions/project.view.js +++ b/src/permissions/project.view.js @@ -2,7 +2,7 @@ import _ from 'lodash'; import util from '../util'; import models from '../models'; -import { USER_ROLE, PROJECT_STATUS, PROJECT_MEMBER_ROLE, MANAGER_ROLES } from '../constants'; +import { MANAGER_ROLES } from '../constants'; /** * Super admin, Topcoder Managers are allowed to view any projects @@ -24,47 +24,11 @@ module.exports = freq => new Promise((resolve, reject) => { || util.hasRoles(req, MANAGER_ROLES) || !_.isUndefined(_.find(members, m => m.userId === currentUserId)); - // if user is co-pilot and he is a member or if project is in "reviewed" status and he is invited - if (!hasAccess && util.hasRole(req, USER_ROLE.COPILOT)) { - return models.Project.getProjectIdsForCopilot(currentUserId) - .then((ids) => { - req.context.accessibleProjectIds = ids; - return Promise.resolve(_.indexOf(ids, projectId) > -1); - }); - } return Promise.resolve(hasAccess); }) .then((hasAccess) => { if (!hasAccess) { - let errorMessage = 'You do not have permissions to perform this action'; - // customize error message for copilots - if (util.hasRole(freq, USER_ROLE.COPILOT)) { - if (_.findIndex(freq.context.currentProjectMembers, m => m.role === PROJECT_MEMBER_ROLE.COPILOT) >= 0) { - errorMessage = 'Copilot: Project is already claimed by another copilot'; - return Promise.resolve(errorMessage); - } - return models.Project - .find({ - where: { id: projectId }, - attributes: ['status'], - raw: true, - }) - .then((project) => { - if (!project || [PROJECT_STATUS.DRAFT, PROJECT_STATUS.IN_REVIEW].indexOf(project.status) >= 0) { - errorMessage = 'Copilot: Project is not yet available to copilots'; - } else if (project.status !== PROJECT_STATUS.REVIEWED) { - // project status is 'active' or higher so it's not available to copilots - errorMessage = 'Copilot: Project has already started'; - } - return Promise.resolve(errorMessage); - }); - } - // user is not an admin nor is a registered project member - return Promise.resolve(errorMessage); - } - return Promise.resolve(null); - }).then((errorMessage) => { - if (errorMessage) { + const errorMessage = 'You do not have permissions to perform this action'; // user is not an admin nor is a registered project member return reject(new Error(errorMessage)); } From b5f3e0f12ee893ea34b6769e0e591fbc40aa6b23 Mon Sep 17 00:00:00 2001 From: Samir Date: Mon, 25 Mar 2019 11:10:07 +0100 Subject: [PATCH 28/48] check user roles on user update --- src/constants.js | 6 +++- src/routes/projectMembers/update.js | 47 +++++++++++++++++------------ 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/constants.js b/src/constants.js index 583cfce1..d2b7ee41 100644 --- a/src/constants.js +++ b/src/constants.js @@ -21,7 +21,11 @@ export const PROJECT_MEMBER_ROLE = { ACCOUNT_MANAGER: 'account_manager', }; -export const PROJECT_MEMBER_MANAGER_ROLES = [PROJECT_MEMBER_ROLE.MANAGER, PROJECT_MEMBER_ROLE.OBSERVER]; +export const PROJECT_MEMBER_MANAGER_ROLES = [ + PROJECT_MEMBER_ROLE.MANAGER, + PROJECT_MEMBER_ROLE.OBSERVER, + PROJECT_MEMBER_ROLE.ACCOUNT_MANAGER, +]; export const USER_ROLE = { TOPCODER_ADMIN: 'administrator', diff --git a/src/routes/projectMembers/update.js b/src/routes/projectMembers/update.js index 7001133f..d187814b 100644 --- a/src/routes/projectMembers/update.js +++ b/src/routes/projectMembers/update.js @@ -5,7 +5,7 @@ import Joi from 'joi'; import { middleware as tcMiddleware } from 'tc-core-library-js'; import models from '../../models'; import util from '../../util'; -import { EVENT, PROJECT_MEMBER_ROLE } from '../../constants'; +import { EVENT, PROJECT_MEMBER_ROLE, PROJECT_MEMBER_MANAGER_ROLES, MANAGER_ROLES } from '../../constants'; /** * API to update a project member. @@ -62,26 +62,35 @@ module.exports = [ return Promise.resolve(); } - projectMember.updatedBy = req.authUser.userId; - const operations = []; - operations.push(projectMember.save()); + return util.getUserRoles(projectMember.userId, req.log, req.id).then((roles) => { + if (_.includes(PROJECT_MEMBER_MANAGER_ROLES, updatedProps.role) + && !util.hasIntersection(MANAGER_ROLES, roles)) { + const err = new Error('User role can not be updated to Manager role'); + err.status = 401; + return Promise.reject(err); + } - if (updatedProps.isPrimary) { - // if set as primary, other users with same role should no longer be primary - operations.push(models.ProjectMember.update({ isPrimary: false, - updatedBy: req.authUser.userId }, - { - where: { - projectId, - isPrimary: true, - role: updatedProps.role, - id: { - $ne: projectMember.id, + projectMember.updatedBy = req.authUser.userId; + const operations = []; + operations.push(projectMember.save()); + + if (updatedProps.isPrimary) { + // if set as primary, other users with same role should no longer be primary + operations.push(models.ProjectMember.update({ isPrimary: false, + updatedBy: req.authUser.userId }, + { + where: { + projectId, + isPrimary: true, + role: updatedProps.role, + id: { + $ne: projectMember.id, + }, }, - }, - })); - } - return Promise.all(operations); + })); + } + return Promise.all(operations); + }); }) // .then(() => { // // TODO move this to an event From 73495fc0bae0bf3098b5d0df08339b5e35458122 Mon Sep 17 00:00:00 2001 From: Samir Date: Mon, 25 Mar 2019 16:28:33 +0100 Subject: [PATCH 29/48] update tests --- src/routes/projectMembers/update.spec.js | 45 ++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/routes/projectMembers/update.spec.js b/src/routes/projectMembers/update.spec.js index 4db550cd..5b39f2dc 100644 --- a/src/routes/projectMembers/update.spec.js +++ b/src/routes/projectMembers/update.spec.js @@ -176,6 +176,21 @@ describe('Project members update', () => { }); it('should return 200 if valid user and data(no isPrimary and no updates)', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + get: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: [{ roleName: 'administrator' }], + }, + }, + }), + }); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); request(server) .patch(`/v4/projects/${project1.id}/members/${member2.id}`) .set({ @@ -204,6 +219,21 @@ describe('Project members update', () => { }); it('should return 200 if valid user(not copilot any more) for project without direct project id', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + get: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: [{ roleName: 'administrator' }], + }, + }, + }), + }); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); models.Project.update({ directProjectId: null, }, { @@ -467,6 +497,21 @@ describe('Project members update', () => { }); it('sends single BUS_API_EVENT.PROJECT_TEAM_UPDATED message when user role updated', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + get: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: [{ roleName: 'administrator' }], + }, + }, + }), + }); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); request(server) .patch(`/v4/projects/${project1.id}/members/${member2.id}`) .set({ From c917f1db3029ea44b742f56c5f416ce160f75e5b Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Tue, 26 Mar 2019 17:31:31 +0800 Subject: [PATCH 30/48] removed unused method --- src/models/project.js | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/models/project.js b/src/models/project.js index 759c8f98..946289f2 100644 --- a/src/models/project.js +++ b/src/models/project.js @@ -61,25 +61,6 @@ module.exports = function defineProject(sequelize, DataTypes) { { fields: ['directProjectId'] }, ], classMethods: { - /* - * @Co-pilots should be able to view projects any of the following conditions are met: - * a. they are registered active project members on the project - * b. any project that is in 'reviewed' state AND copilot is invited - * @param userId the id of user - */ - getProjectIdsForCopilot(userId) { - return this.findAll({ - where: { - $or: [ - ['EXISTS(SELECT * FROM "project_members" WHERE "deletedAt" ' + - 'IS NULL AND "projectId" = "Project".id AND "userId" = ? )', userId], - ], - }, - attributes: ['id'], - raw: true, - }) - .then(res => _.map(res, 'id')); - }, /** * Get direct project id * @param id the id of project From 847c297ce17ee3cf0aec3484f73810723a6e82be Mon Sep 17 00:00:00 2001 From: RishiRaj Date: Tue, 26 Mar 2019 16:42:52 +0530 Subject: [PATCH 31/48] -Added testcases for account manager invites. --- .../projectMemberInvites/create.spec.js | 89 ++++++++++++++----- 1 file changed, 66 insertions(+), 23 deletions(-) diff --git a/src/routes/projectMemberInvites/create.spec.js b/src/routes/projectMemberInvites/create.spec.js index 74f6be9c..543962b7 100644 --- a/src/routes/projectMemberInvites/create.spec.js +++ b/src/routes/projectMemberInvites/create.spec.js @@ -500,25 +500,8 @@ describe('Project Member Invite create', () => { }); it('should return 201 if try to create manager with MANAGER_ROLES', (done) => { - const mockHttpClient = _.merge(testUtil.mockHttpClient, { - get: () => Promise.resolve({ - status: 403, - data: { - id: 'requesterId', - version: 'v3', - result: { - success: true, - status: 403, - content: { - failed: [{ - message: 'cannot be added with a Manager role to the project', - }], - }, - }, - }, - }), - }); - sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + util.getUserRoles.restore(); + sandbox.stub(util, 'getUserRoles', () => Promise.resolve([USER_ROLE.MANAGER])); request(server) .post(`/v4/projects/${project1.id}/members/invite`) .set({ @@ -531,15 +514,75 @@ describe('Project Member Invite create', () => { }, }) .expect('Content-Type', /json/) - .expect(403) + .expect(201) + .end((err, res) => { + const resJson = res.body.result.content.success[0]; + should.exist(resJson); + resJson.role.should.equal('manager'); + resJson.projectId.should.equal(project1.id); + resJson.userId.should.equal(40152855); + server.services.pubsub.publish.calledWith('project.member.invite.created').should.be.true; + done(); + }); + }); + + it('should return 201 if try to create account_manager with MANAGER_ROLES', (done) => { + util.getUserRoles.restore(); + sandbox.stub(util, 'getUserRoles', () => Promise.resolve([USER_ROLE.MANAGER])); + request(server) + .post(`/v4/projects/${project1.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send({ + param: { + userIds: [40152855], + role: 'account_manager', + }, + }) + .expect('Content-Type', /json/) + .expect(201) .end((err, res) => { - const failed = res.body.result.content.failed[0]; - should.exist(failed); - failed.message.should.equal('cannot be added with a Manager role to the project'); + const resJson = res.body.result.content.success[0]; + should.exist(resJson); + resJson.role.should.equal('account_manager'); + resJson.projectId.should.equal(project1.id); + resJson.userId.should.equal(40152855); + server.services.pubsub.publish.calledWith('project.member.invite.created').should.be.true; done(); }); }); + it('should return 403 if try to create account_manager with CUSTOMER_ROLE', (done) => { + util.getUserRoles.restore(); + sandbox.stub(util, 'getUserRoles', () => Promise.resolve(['Topcoder User'])); + request(server) + .post(`/v4/projects/${project1.id}/members/invite`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send({ + param: { + userIds: [40152855], + role: 'account_manager', + }, + }) + .expect('Content-Type', /json/) + .expect(403) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content.failed[0]; + should.exist(resJson); + res.body.result.status.should.equal(403); + const errorMessage = _.get(resJson, 'message', ''); + sinon.assert.match(errorMessage, /.*cannot be added with a Manager role to the project/); + done(); + } + }); + }); + it('should return 201 if try to create customer with COPILOT', (done) => { const mockHttpClient = _.merge(testUtil.mockHttpClient, { get: () => Promise.resolve({ From d11122370b33ff2009a411e93a35c9468b1f384b Mon Sep 17 00:00:00 2001 From: Gunasekar-K Date: Tue, 26 Mar 2019 18:02:44 +0530 Subject: [PATCH 32/48] Update Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index a59f1239..f02615be 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM node:8.2.1 LABEL version="1.2" LABEL description="Projects microservice" - +RUN sed -i '/jessie-updates/d' /etc/apt/sources.list RUN apt-get update && \ apt-get upgrade -y From a59ca81d875762b1ed61561fa39f2cd83bac5221 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Wed, 27 Mar 2019 14:52:25 +0800 Subject: [PATCH 33/48] submission from the final fix challenge 30087019 - Project template refactoring (submission + final fixes) also includes small error message fixes and supporting 'null' for scope, phases, form, planConfig, priceConfig for create and update endpoints of projectTemplates --- config/default.json | 4 +- config/test.json | 1 + ...6_extract_scope_from_project_templates.sql | 89 + .../20190317_refactor_project_templates.sql | 9 + postman.json | 3362 +++++++++++------ src/models/form.js | 47 + src/models/planConfig.js | 47 + src/models/priceConfig.js | 46 + src/models/projectTemplate.js | 7 +- src/models/versionModelClassMethods.js | 124 + src/permissions/index.js | 16 + src/routes/form/revision/create.js | 66 + src/routes/form/revision/create.spec.js | 136 + src/routes/form/revision/delete.js | 48 + src/routes/form/revision/delete.spec.js | 153 + src/routes/form/revision/get.js | 44 + src/routes/form/revision/get.spec.js | 113 + src/routes/form/revision/list.js | 41 + src/routes/form/revision/list.spec.js | 115 + src/routes/form/version/create.js | 64 + src/routes/form/version/create.spec.js | 114 + src/routes/form/version/delete.js | 54 + src/routes/form/version/delete.spec.js | 139 + src/routes/form/version/get.js | 34 + src/routes/form/version/get.spec.js | 133 + src/routes/form/version/getVersion.js | 42 + src/routes/form/version/getVersion.spec.js | 124 + src/routes/form/version/list.js | 47 + src/routes/form/version/list.spec.js | 115 + src/routes/form/version/update.js | 74 + src/routes/form/version/update.spec.js | 113 + src/routes/index.js | 68 + src/routes/metadata/list.js | 117 +- src/routes/metadata/list.spec.js | 103 +- src/routes/planConfig/revision/create.js | 66 + src/routes/planConfig/revision/create.spec.js | 136 + src/routes/planConfig/revision/delete.js | 48 + src/routes/planConfig/revision/delete.spec.js | 153 + src/routes/planConfig/revision/get.js | 44 + src/routes/planConfig/revision/get.spec.js | 113 + src/routes/planConfig/revision/list.js | 41 + src/routes/planConfig/revision/list.spec.js | 115 + src/routes/planConfig/version/create.js | 64 + src/routes/planConfig/version/create.spec.js | 114 + src/routes/planConfig/version/delete.js | 54 + src/routes/planConfig/version/delete.spec.js | 139 + src/routes/planConfig/version/get.js | 33 + src/routes/planConfig/version/get.spec.js | 134 + src/routes/planConfig/version/getVersion.js | 42 + .../planConfig/version/getVersion.spec.js | 124 + src/routes/planConfig/version/list.js | 47 + src/routes/planConfig/version/list.spec.js | 115 + src/routes/planConfig/version/update.js | 74 + src/routes/planConfig/version/update.spec.js | 113 + src/routes/priceConfig/revision/create.js | 66 + .../priceConfig/revision/create.spec.js | 136 + src/routes/priceConfig/revision/delete.js | 48 + .../priceConfig/revision/delete.spec.js | 153 + src/routes/priceConfig/revision/get.js | 44 + src/routes/priceConfig/revision/get.spec.js | 113 + src/routes/priceConfig/revision/list.js | 41 + src/routes/priceConfig/revision/list.spec.js | 115 + src/routes/priceConfig/version/create.js | 64 + src/routes/priceConfig/version/create.spec.js | 114 + src/routes/priceConfig/version/delete.js | 54 + src/routes/priceConfig/version/delete.spec.js | 139 + src/routes/priceConfig/version/get.js | 33 + src/routes/priceConfig/version/get.spec.js | 134 + src/routes/priceConfig/version/getVersion.js | 42 + .../priceConfig/version/getVersion.spec.js | 124 + src/routes/priceConfig/version/list.js | 47 + src/routes/priceConfig/version/list.spec.js | 115 + src/routes/priceConfig/version/update.js | 74 + src/routes/priceConfig/version/update.spec.js | 113 + src/routes/projectTemplates/create.js | 80 +- src/routes/projectTemplates/create.spec.js | 74 + src/routes/projectTemplates/update.js | 126 +- src/routes/projectTemplates/update.spec.js | 72 + src/routes/projectTemplates/upgrade.js | 151 + src/routes/projectTemplates/upgrade.spec.js | 287 ++ src/routes/timelines/create.js | 2 +- swagger.yaml | 3066 ++++++++++----- 82 files changed, 11045 insertions(+), 2160 deletions(-) create mode 100644 migrations/20190316_extract_scope_from_project_templates.sql create mode 100644 migrations/20190317_refactor_project_templates.sql create mode 100644 src/models/form.js create mode 100644 src/models/planConfig.js create mode 100644 src/models/priceConfig.js create mode 100644 src/models/versionModelClassMethods.js create mode 100644 src/routes/form/revision/create.js create mode 100644 src/routes/form/revision/create.spec.js create mode 100644 src/routes/form/revision/delete.js create mode 100644 src/routes/form/revision/delete.spec.js create mode 100644 src/routes/form/revision/get.js create mode 100644 src/routes/form/revision/get.spec.js create mode 100644 src/routes/form/revision/list.js create mode 100644 src/routes/form/revision/list.spec.js create mode 100644 src/routes/form/version/create.js create mode 100644 src/routes/form/version/create.spec.js create mode 100644 src/routes/form/version/delete.js create mode 100644 src/routes/form/version/delete.spec.js create mode 100644 src/routes/form/version/get.js create mode 100644 src/routes/form/version/get.spec.js create mode 100644 src/routes/form/version/getVersion.js create mode 100644 src/routes/form/version/getVersion.spec.js create mode 100644 src/routes/form/version/list.js create mode 100644 src/routes/form/version/list.spec.js create mode 100644 src/routes/form/version/update.js create mode 100644 src/routes/form/version/update.spec.js create mode 100644 src/routes/planConfig/revision/create.js create mode 100644 src/routes/planConfig/revision/create.spec.js create mode 100644 src/routes/planConfig/revision/delete.js create mode 100644 src/routes/planConfig/revision/delete.spec.js create mode 100644 src/routes/planConfig/revision/get.js create mode 100644 src/routes/planConfig/revision/get.spec.js create mode 100644 src/routes/planConfig/revision/list.js create mode 100644 src/routes/planConfig/revision/list.spec.js create mode 100644 src/routes/planConfig/version/create.js create mode 100644 src/routes/planConfig/version/create.spec.js create mode 100644 src/routes/planConfig/version/delete.js create mode 100644 src/routes/planConfig/version/delete.spec.js create mode 100644 src/routes/planConfig/version/get.js create mode 100644 src/routes/planConfig/version/get.spec.js create mode 100644 src/routes/planConfig/version/getVersion.js create mode 100644 src/routes/planConfig/version/getVersion.spec.js create mode 100644 src/routes/planConfig/version/list.js create mode 100644 src/routes/planConfig/version/list.spec.js create mode 100644 src/routes/planConfig/version/update.js create mode 100644 src/routes/planConfig/version/update.spec.js create mode 100644 src/routes/priceConfig/revision/create.js create mode 100644 src/routes/priceConfig/revision/create.spec.js create mode 100644 src/routes/priceConfig/revision/delete.js create mode 100644 src/routes/priceConfig/revision/delete.spec.js create mode 100644 src/routes/priceConfig/revision/get.js create mode 100644 src/routes/priceConfig/revision/get.spec.js create mode 100644 src/routes/priceConfig/revision/list.js create mode 100644 src/routes/priceConfig/revision/list.spec.js create mode 100644 src/routes/priceConfig/version/create.js create mode 100644 src/routes/priceConfig/version/create.spec.js create mode 100644 src/routes/priceConfig/version/delete.js create mode 100644 src/routes/priceConfig/version/delete.spec.js create mode 100644 src/routes/priceConfig/version/get.js create mode 100644 src/routes/priceConfig/version/get.spec.js create mode 100644 src/routes/priceConfig/version/getVersion.js create mode 100644 src/routes/priceConfig/version/getVersion.spec.js create mode 100644 src/routes/priceConfig/version/list.js create mode 100644 src/routes/priceConfig/version/list.spec.js create mode 100644 src/routes/priceConfig/version/update.js create mode 100644 src/routes/priceConfig/version/update.spec.js create mode 100644 src/routes/projectTemplates/upgrade.js create mode 100644 src/routes/projectTemplates/upgrade.spec.js diff --git a/config/default.json b/config/default.json index fa994859..7dd5f6bc 100644 --- a/config/default.json +++ b/config/default.json @@ -56,6 +56,6 @@ "inviteEmailSubject": "You are invited to Topcoder", "inviteEmailSectionTitle": "Project Invitation", "connectUrl":"https://connect.topcoder-dev.com", - "accountsAppUrl": "https://accounts.topcoder-dev.com" - + "accountsAppUrl": "https://accounts.topcoder-dev.com", + "MAX_REVISION_NUMBER": 100 } diff --git a/config/test.json b/config/test.json index 4e94d1a1..73b0fe82 100644 --- a/config/test.json +++ b/config/test.json @@ -1,5 +1,6 @@ { "AUTH_SECRET": "secret", + "MAX_REVISION_NUMBER": 2, "logLevel": "debug", "captureLogs": "false", "logentriesToken": "", diff --git a/migrations/20190316_extract_scope_from_project_templates.sql b/migrations/20190316_extract_scope_from_project_templates.sql new file mode 100644 index 00000000..6f5e34e9 --- /dev/null +++ b/migrations/20190316_extract_scope_from_project_templates.sql @@ -0,0 +1,89 @@ +-- +-- form +-- + +CREATE TABLE form ( + id bigint NOT NULL, + "key" character varying(45) NOT NULL, + "version" bigint DEFAULT 1 NOT NULL, + "revision" bigint DEFAULT 1 NOT NULL, + "scope" json DEFAULT '{}'::json NOT NULL, + "deletedAt" timestamp with time zone, + "createdAt" timestamp with time zone, + "updatedAt" timestamp with time zone, + "deletedBy" bigint, + "createdBy" bigint NOT NULL, + "updatedBy" bigint NOT NULL +); + +ALTER TABLE form + ADD CONSTRAINT form_key_version_revision UNIQUE (key, version, revision); + +CREATE SEQUENCE form_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE form_id_seq OWNED BY form.id; + +-- +-- price_config +-- + +CREATE TABLE price_config ( + id bigint NOT NULL, + "key" character varying(45) NOT NULL, + "version" bigint DEFAULT 1 NOT NULL, + "revision" bigint DEFAULT 1 NOT NULL, + "config" json DEFAULT '{}'::json NOT NULL, + "deletedAt" timestamp with time zone, + "createdAt" timestamp with time zone, + "updatedAt" timestamp with time zone, + "deletedBy" bigint, + "createdBy" bigint NOT NULL, + "updatedBy" bigint NOT NULL +); + +ALTER TABLE price_config + ADD CONSTRAINT price_config_key_version_revision UNIQUE (key, version, revision); + +CREATE SEQUENCE price_config_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE price_config_id_seq OWNED BY price_config.id; + +-- +-- plan_config +-- + +CREATE TABLE plan_config ( + id bigint NOT NULL, + "key" character varying(45) NOT NULL, + "version" bigint DEFAULT 1 NOT NULL, + "revision" bigint DEFAULT 1 NOT NULL, + "phases" json DEFAULT '{}'::json NOT NULL, + "deletedAt" timestamp with time zone, + "createdAt" timestamp with time zone, + "updatedAt" timestamp with time zone, + "deletedBy" bigint, + "createdBy" bigint NOT NULL, + "updatedBy" bigint NOT NULL +); + +ALTER TABLE plan_config + ADD CONSTRAINT plan_config_key_version_revision UNIQUE (key, version, revision); + +CREATE SEQUENCE plan_config_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE plan_config_id_seq OWNED BY plan_config.id; diff --git a/migrations/20190317_refactor_project_templates.sql b/migrations/20190317_refactor_project_templates.sql new file mode 100644 index 00000000..b65da92b --- /dev/null +++ b/migrations/20190317_refactor_project_templates.sql @@ -0,0 +1,9 @@ +-- +-- project_templates +-- +ALTER TABLE project_templates ALTER COLUMN "scope" DROP NOT NULL; +ALTER TABLE project_templates ALTER COLUMN "phases" DROP NOT NULL; + +ALTER TABLE project_templates ADD COLUMN "planConfig" json; +ALTER TABLE project_templates ADD COLUMN "priceConfig" json; +ALTER TABLE project_templates ADD COLUMN "form" json; diff --git a/postman.json b/postman.json index 8c74a585..536c3124 100644 --- a/postman.json +++ b/postman.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "4fc2b7cf-404a-4fd7-b6d2-4828a3994859", + "_postman_id": "b2fedaf2-e077-4351-ac5b-72be7f46e3ed", "name": "tc-project-service", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -278,17 +278,17 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"title\": \"first attachment submission\",\n\t\t\"filePath\": \"asdjshdasdas/asdsadj/asdasd.png\",\n\t\t\"s3Bucket\": \"topcoder-project-service\",\n\t\t\"contentType\": \"application/png\",\n\t\t\"allowedUsers\": [40051331]\n\t}\n}" + "raw": "{\n\t\"param\": {\n\t\t\"title\": \"first attachment submission\",\n\t\t\"filePath\": \"asdjshdasdas/asdsadj/asdasd.png\",\n\t\t\"s3Bucket\": \"topcoder-project-service\",\n\t\t\"contentType\": \"application/png\"\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/1/attachments", + "raw": "{{api-url}}/v4/projects/7/attachments", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "1", + "7", "attachments" ] }, @@ -297,9 +297,9 @@ "response": [] }, { - "name": "Download attachment", + "name": "Update attachment", "request": { - "method": "GET", + "method": "PATCH", "header": [ { "key": "Authorization", @@ -312,33 +312,33 @@ ], "body": { "mode": "raw", - "raw": "" + "raw": "{\n\t\"param\": {\n\t\t\"title\": \"first attachment submission updated\",\n\t\t\"description\": \"updated project attachment\"\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/1/attachments/2", + "raw": "{{api-url}}/v4/projects/7/attachments/2", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "1", + "7", "attachments", "2" ] }, - "description": "Create an project attachment" + "description": "Update project attachment" }, "response": [] }, { - "name": "Download attachment admin", + "name": "Delete attachment", "request": { - "method": "GET", + "method": "DELETE", "header": [ { "key": "Authorization", - "value": "Bearer {{jwt-token-admin-40051333}}" + "value": "Bearer {{jwt-token}}" }, { "key": "Content-Type", @@ -350,30 +350,35 @@ "raw": "" }, "url": { - "raw": "{{api-url}}/v4/projects/1/attachments/2", + "raw": "{{api-url}}/v4/projects/7/attachments/2", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "1", + "7", "attachments", "2" ] }, - "description": "Create an project attachment" + "description": "Delete a project attachment" }, "response": [] - }, + } + ] + }, + { + "name": "Project With TemplateId issue", + "item": [ { - "name": "Download attachment - No access", + "name": "Create project with templateId (not existed)", "request": { - "method": "GET", + "method": "POST", "header": [ { "key": "Authorization", - "value": "Bearer {{jwt-token-copilot-40051332}}" + "value": "Bearer {{jwt-token}}" }, { "key": "Content-Type", @@ -382,29 +387,25 @@ ], "body": { "mode": "raw", - "raw": "" + "raw": "{\n\t\"param\": {\n\t\t\"name\": \"test project with templateId\",\n\t\t\"description\": \"Hello I am a test project with templateId\",\n\t\t\"type\": \"generic\",\n\t\t\"templateId\": 3000\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/1/attachments/2", + "raw": "{{api-url}}/v4/projects", "host": [ "{{api-url}}" ], "path": [ "v4", - "projects", - "1", - "attachments", - "2" + "projects" ] - }, - "description": "Create an project attachment" + } }, "response": [] }, { - "name": "Update attachment", + "name": "Create project with templateId", "request": { - "method": "PATCH", + "method": "POST", "header": [ { "key": "Authorization", @@ -417,33 +418,34 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"title\": \"first attachment submission updated\",\n\t\t\"description\": \"updated project attachment\"\n\t}\n}" + "raw": "{\n \"param\": {\n \"name\": \"test project with templateId\",\n \"description\": \"Hello I am a test project with templateId\",\n \"type\": \"generic\",\n \"templateId\": 3\n }\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/1/attachments/2", + "raw": "{{api-url}}/v4/projects", "host": [ "{{api-url}}" ], "path": [ "v4", - "projects", - "1", - "attachments", - "2" + "projects" ] - }, - "description": "Update project attachment" + } }, "response": [] - }, + } + ] + }, + { + "name": "Project Members", + "item": [ { - "name": "Update attachment - No access", + "name": "Create project member with no payload", "request": { - "method": "PATCH", + "method": "POST", "header": [ { "key": "Authorization", - "value": "Bearer {{jwt-token-copilot-40051332}}" + "value": "Bearer {{jwt-token}}" }, { "key": "Content-Type", @@ -452,10 +454,10 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"title\": \"first attachment submission updated\",\n\t\t\"description\": \"updated project attachment\",\n\t\t\"allowedUsers\": null\n\t}\n}" + "raw": "" }, "url": { - "raw": "{{api-url}}/v4/projects/1/attachments/2", + "raw": "{{api-url}}/v4/projects/1/members", "host": [ "{{api-url}}" ], @@ -463,18 +465,17 @@ "v4", "projects", "1", - "attachments", - "2" + "members" ] }, - "description": "Update project attachment" + "description": "Request payload is mandatory while creating project. If no request payload is specified this should result in 422 status code." }, "response": [] }, { - "name": "Delete attachment", + "name": "Create project copilot with invalid userId", "request": { - "method": "DELETE", + "method": "POST", "header": [ { "key": "Authorization", @@ -487,33 +488,32 @@ ], "body": { "mode": "raw", - "raw": "" + "raw": "{\n\"param\":{\n\t\"role\": \"copilot\"\n}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/7/attachments/2", + "raw": "{{api-url}}/v4/projects/1/members", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "7", - "attachments", - "2" + "1", + "members" ] }, - "description": "Delete a project attachment" + "description": "Certain fields are mandatory while creating project. If invalid fields are specified this should result in 422 status code." }, "response": [] }, { - "name": "Delete attachment - No access", + "name": "Create project copilot with valid values", "request": { - "method": "DELETE", + "method": "POST", "header": [ { "key": "Authorization", - "value": "Bearer {{jwt-token-copilot-40051332}}" + "value": "Bearer {{jwt-token}}" }, { "key": "Content-Type", @@ -522,32 +522,26 @@ ], "body": { "mode": "raw", - "raw": "" + "raw": "{\n\t\"param\": {\n\t\t\"role\": \"copilot\",\n\t\t\"userId\": 40051331,\n\t\t\"isPrimary\": true\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/1/attachments/2", + "raw": "{{api-url}}/v4/projects/7/members", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "1", - "attachments", - "2" + "7", + "members" ] }, - "description": "Delete a project attachment" + "description": "If the request payload is valid, than project member should be created." }, "response": [] - } - ] - }, - { - "name": "Project With TemplateId issue", - "item": [ + }, { - "name": "Create project with templateId (not existed)", + "name": "Create project member, if user already registered", "request": { "method": "POST", "header": [ @@ -562,23 +556,26 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"name\": \"test project with templateId\",\n\t\t\"description\": \"Hello I am a test project with templateId\",\n\t\t\"type\": \"generic\",\n\t\t\"templateId\": 3000\n\t}\n}" + "raw": "{\n\t\"param\": {\n\t\t\"role\": \"copilot\",\n\t\t\"userId\": 40051331,\n\t\t\"isPrimary\": true\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects", + "raw": "{{api-url}}/v4/projects/1/members", "host": [ "{{api-url}}" ], "path": [ "v4", - "projects" + "projects", + "1", + "members" ] - } + }, + "description": "If the request payload is valid and user is already registered with the specified role than this should result in 400." }, "response": [] }, { - "name": "Create project with templateId", + "name": "Create project manager with valid values", "request": { "method": "POST", "header": [ @@ -593,34 +590,32 @@ ], "body": { "mode": "raw", - "raw": "{\n \"param\": {\n \"name\": \"test project with templateId\",\n \"description\": \"Hello I am a test project with templateId\",\n \"type\": \"generic\",\n \"templateId\": 3\n }\n}" + "raw": "{\n\t\"param\": {\n\t\t\"role\": \"manager\",\n\t\t\"userId\": 40051330,\n\t\t\"isPrimary\": true\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects", + "raw": "{{api-url}}/v4/projects/7/members", "host": [ "{{api-url}}" ], "path": [ "v4", - "projects" + "projects", + "7", + "members" ] - } + }, + "description": "If the request payload is valid, than project manager should be added. This should sync with the direct project is project is associated with direct project." }, "response": [] - } - ] - }, - { - "name": "Project Members", - "item": [ + }, { - "name": "Create project manager with valid values", + "name": "Create project customer with valid values", "request": { "method": "POST", "header": [ { "key": "Authorization", - "value": "Bearer {{jwt-token-manager-40051334}}" + "value": "Bearer {{jwt-token}}" }, { "key": "Content-Type", @@ -629,21 +624,21 @@ ], "body": { "mode": "raw", - "raw": "" + "raw": "{\n\t\"param\": {\n\t\t\"role\": \"customer\",\n\t\t\"userId\": 40051332,\n\t\t\"isPrimary\": true\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/1/members", + "raw": "{{api-url}}/v4/projects/7/members", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "1", + "7", "members" ] }, - "description": "If the request payload is valid, than project member should be created." + "description": "If the request payload is valid, than project customer should be added. This should sync with the direct project is project is associated with direct project." }, "response": [] }, @@ -770,16 +765,16 @@ "raw": "" }, "url": { - "raw": "{{api-url}}/v4/projects/1/members/40051331", + "raw": "{{api-url}}/v4/projects/3/members/5", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "1", + "3", "members", - "40051331" + "5" ] }, "description": "Delete a project's member" @@ -823,10 +818,10 @@ ] }, { - "name": "Project Member Invites", + "name": "Projects", "item": [ { - "name": "Invite valid userIds", + "name": "Create project without payload", "request": { "method": "POST", "header": [ @@ -836,33 +831,29 @@ }, { "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" + "value": "application/json" } ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"userIds\": [40051331],\n\t\t\"role\": \"customer\"\n\t}\n}" + "raw": "{\n\t\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/1/members/invite", + "raw": "{{api-url}}/v4/projects", "host": [ "{{api-url}}" ], "path": [ "v4", - "projects", - "1", - "members", - "invite" + "projects" ] - } + }, + "description": "Request body is mandatory while creating project. If invalid request body is supplied this should return 422 status code." }, "response": [] }, { - "name": "Invite valid emails", + "name": "Create project without valid name", "request": { "method": "POST", "header": [ @@ -872,33 +863,29 @@ }, { "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" + "value": "application/json" } ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"emails\": [\"hello@world.com\"],\n\t\t\"role\": \"customer\"\n\t}\n}" + "raw": "{\n\t\"param\": {\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/1/members/invite", + "raw": "{{api-url}}/v4/projects", "host": [ "{{api-url}}" ], "path": [ "v4", - "projects", - "1", - "members", - "invite" + "projects" ] - } + }, + "description": "Certain fields are mandatory while creating project. If invalid request body is supplied this should return 422 status code." }, "response": [] }, { - "name": "Invite email with manager role", + "name": "Create project with valid values", "request": { "method": "POST", "header": [ @@ -908,289 +895,265 @@ }, { "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" + "value": "application/json" } ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"emails\": [\"hello@world.com\"],\n\t\t\"role\": \"manager\"\n\t}\n}" + "raw": "{\n\t\"param\": {\n\t\t\"name\": \"test project\",\n\t\t\"description\": \"Hello I am a test project\",\n\t\t\"type\": \"generic\"\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/1/members/invite", + "raw": "{{api-url}}/v4/projects", "host": [ "{{api-url}}" ], "path": [ "v4", - "projects", - "1", - "members", - "invite" + "projects" ] - } + }, + "description": "Valid request body. Project should be created successfully." }, "response": [] }, { - "name": "Invite manager and target has no MANAGER_ROLES", + "name": "Create project by inactive user", "request": { "method": "POST", "header": [ { "key": "Authorization", - "value": "Bearer {{jwt-token}}" + "value": "Bearer userId_{{inactive-userId}}" }, { "key": "Content-Type", - "name": "Content-Type", - "type": "text", "value": "application/json" } ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"userIds\": [40051331],\n\t\t\"role\": \"manager\"\n\t}\n}" + "raw": "{\n\t\"param\": {\n\t\t\"name\": \"test project\",\n\t\t\"description\": \"Hello I am a test project\",\n\t\t\"type\": \"generic\"\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/1/members/invite", + "raw": "{{api-url}}/v4/projects", "host": [ "{{api-url}}" ], "path": [ "v4", - "projects", - "1", - "members", - "invite" + "projects" ] - } + }, + "description": "Valid request body. Project should be created successfully." }, "response": [] }, { - "name": "Invite manager and requester has no MANAGER_ROLES", + "name": "Get project by id", "request": { - "method": "POST", + "method": "GET", "header": [ { "key": "Authorization", - "value": "Bearer {{jwt-token-member2-40051335}}" - }, - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" + "value": "Bearer {{jwt-token}}" } ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"userIds\": [40051331],\n\t\t\"role\": \"manager\"\n\t}\n}" + "raw": "" }, "url": { - "raw": "{{api-url}}/v4/projects/1/members/invite", + "raw": "{{api-url}}/v4/projects/7", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "1", - "members", - "invite" + "7" ] - } + }, + "description": "Get a project by id. project members and attachments should also be returned." }, "response": [] }, { - "name": "Invite with both userIds and emails", + "name": "Get project by id and request specific fields", "request": { - "method": "POST", + "method": "GET", "header": [ { "key": "Authorization", "value": "Bearer {{jwt-token}}" - }, - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" } ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"userIds\": [40051331],\n\t\t\"emails\": [\"hello@world.com\"],\n\t\t\"role\": \"manager\"\n\t}\n}" + "raw": "" }, "url": { - "raw": "{{api-url}}/v4/projects/1/members/invite", + "raw": "{{api-url}}/v4/projects/1?fields=id,name,description,members.id,members.projectId", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "1", - "members", - "invite" + "1" + ], + "query": [ + { + "key": "fields", + "value": "id,name,description,members.id,members.projectId" + } ] - } + }, + "description": "Get a project by id. project members and attachments should also be returned. Only those fields which are specified should be returned." }, "response": [] }, { - "name": "Invite with userIds and emails - both success and failed", + "name": "List projects", "request": { - "url": "{{api-url}}/v4/projects/1/members/invite", - "method": "POST", + "method": "GET", "header": [ { "key": "Authorization", - "value": "Bearer {{jwt-token-manager-40051334}}", - "description": "" - }, - { - "key": "Content-Type", - "value": "application/json", - "description": "" + "value": "Bearer {{jwt-token}}" } ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"userIds\": [40051331, 40051334],\n\t\t\"emails\": [\"divyalife526@gmail.com\"],\n\t\t\"role\": \"manager\"\n\t}\n}" + "raw": "" }, - "description": "" - }, - "response": [] - }, + "url": { + "raw": "{{api-url}}/v4/projects", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects" + ] + }, + "description": "List all the project with no filter. Default sort and limits are applied." + }, + "response": [] + }, { - "name": "Update invite status with userId", + "name": "List projects with limit and offset", "request": { - "method": "PUT", + "method": "GET", "header": [ { "key": "Authorization", "value": "Bearer {{jwt-token}}" - }, - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" } ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"userId\": \"40051331\",\n\t\t\"status\": \"accepted\"\n\t}\n}" + "raw": "" }, "url": { - "raw": "{{api-url}}/v4/projects/1/members/invite", + "raw": "{{api-url}}/v4/projects?limit=1&offset=1", "host": [ "{{api-url}}" ], "path": [ "v4", - "projects", - "1", - "members", - "invite" + "projects" + ], + "query": [ + { + "key": "limit", + "value": "1" + }, + { + "key": "offset", + "value": "1" + } ] - } + }, + "description": "List all the project with no filter. Limit of 1 and offset of 1 is applied" }, "response": [] }, { - "name": "Update invite status with email", + "name": "List projects with filters applied", "request": { - "method": "PUT", + "method": "GET", "header": [ { "key": "Authorization", "value": "Bearer {{jwt-token}}" - }, - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" } ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"email\": \"hello@world.com\",\n\t\t\"status\": \"canceled\"\n\t}\n}" + "raw": "" }, "url": { - "raw": "{{api-url}}/v4/projects/1/members/invite", + "raw": "{{api-url}}/v4/projects?filter=type%3Dgeneric", "host": [ "{{api-url}}" ], "path": [ "v4", - "projects", - "1", - "members", - "invite" + "projects" + ], + "query": [ + { + "key": "filter", + "value": "type%3Dgeneric" + } ] - } + }, + "description": "List all the project with filters applied. The filter string should be url encoded. Default limit and offset is applicable" }, "response": [] }, { - "name": "Update invite with both userId and email", + "name": "List projects with sort applied", "request": { - "method": "PUT", + "method": "GET", "header": [ { "key": "Authorization", "value": "Bearer {{jwt-token}}" - }, - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" } ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"userId\": \"40051331\",\n\t\t\"email\": \"hello@world.com\",\n\t\t\"status\": \"accepted\"\n\t}\n}" + "raw": "" }, "url": { - "raw": "{{api-url}}/v4/projects/1/members/invite", + "raw": "{{api-url}}/v4/projects?sort=type%20desc", "host": [ "{{api-url}}" ], "path": [ "v4", - "projects", - "1", - "members", - "invite" + "projects" + ], + "query": [ + { + "key": "sort", + "value": "type%20desc" + } ] - } + }, + "description": "List all the project with custom sort and no filter. Default sort and limits are applied. The sort string has to be url encoded. Sort is of type `key asc|desc`" }, "response": [] }, { - "name": "Retrieve current user invite", - "protocolProfileBehavior": { - "disableBodyPruning": true - }, + "name": "List projects and return specific fields", "request": { "method": "GET", "header": [ { "key": "Authorization", - "value": "Bearer {{jwt-token-member-40051331}}" - }, - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" + "value": "Bearer {{jwt-token}}" } ], "body": { @@ -1198,43 +1161,38 @@ "raw": "" }, "url": { - "raw": "{{api-url}}/v4/projects/1/members/invite", + "raw": "{{api-url}}/v4/projects?fields=id,name,description", "host": [ "{{api-url}}" ], "path": [ "v4", - "projects", - "1", - "members", - "invite" + "projects" + ], + "query": [ + { + "key": "fields", + "value": "id,name,description" + } ] - } + }, + "description": "List all the project with no filter. Default sort and limits are applied. The fields to return is specified as comma separated list. Only those fields should be returned." }, "response": [] - } - ] - }, - { - "name": "Projects", - "item": [ + }, { - "name": "Create project without payload", + "name": "get projects with copilot token", "request": { - "method": "POST", + "method": "GET", "header": [ { "key": "Authorization", - "value": "Bearer {{jwt-token}}" - }, - { - "key": "Content-Type", - "value": "application/json" + "value": "Bearer {{jwt-token-copilot-40051332}}" } ], "body": { "mode": "raw", - "raw": "{\n\t\n}" + "raw": "" }, "url": { "raw": "{{api-url}}/v4/projects", @@ -1245,47 +1203,43 @@ "v4", "projects" ] - }, - "description": "Request body is mandatory while creating project. If invalid request body is supplied this should return 422 status code." + } }, "response": [] }, { - "name": "Create project without valid name", + "name": "DELETE project by id", "request": { - "method": "POST", + "method": "DELETE", "header": [ { "key": "Authorization", "value": "Bearer {{jwt-token}}" - }, - { - "key": "Content-Type", - "value": "application/json" } ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t}\n}" + "raw": "" }, "url": { - "raw": "{{api-url}}/v4/projects", + "raw": "{{api-url}}/v4/projects/3", "host": [ "{{api-url}}" ], "path": [ "v4", - "projects" + "projects", + "3" ] }, - "description": "Certain fields are mandatory while creating project. If invalid request body is supplied this should return 422 status code." + "description": "Delete a project by id" }, "response": [] }, { - "name": "Create project with valid values", + "name": "Update project", "request": { - "method": "POST", + "method": "PATCH", "header": [ { "key": "Authorization", @@ -1298,30 +1252,31 @@ ], "body": { "mode": "raw", - "raw": "{\n \"param\": {\n \"name\": \"Test 3\",\n \"details\": {\n \"utm\": {\n \"code\": \"\"\n },\n \"appDefinition\": {\n \"primaryTarget\": \"phone\",\n \"goal\": {\n \"value\": \"Nothing\"\n },\n \"users\": {\n \"value\": \"No one\"\n },\n \"notes\": \"\"\n },\n \"hideDiscussions\": true\n },\n \"description\": \"Hello this is a sample description... This requires at least 160 characters. I'm trying to satisfy this condition. But I could n't if I don't type this unnecessary message\",\n \"templateId\": 3,\n \"type\": \"app\"\n }\n}" + "raw": "{\n \"param\": {\n \"name\": \"project name updated\"\n }\n}" }, "url": { - "raw": "{{api-url}}/v4/projects", + "raw": "{{api-url}}/v4/projects/1", "host": [ "{{api-url}}" ], "path": [ "v4", - "projects" + "projects", + "1" ] }, - "description": "Valid request body. Project should be created successfully." + "description": "Update the project name. Name should be updated successfully." }, "response": [] }, { - "name": "Create project by inactive user", + "name": "Update project 403", "request": { - "method": "POST", + "method": "PATCH", "header": [ { "key": "Authorization", - "value": "Bearer userId_{{inactive-userId}}" + "value": "Bearer {{jwt-token}}" }, { "key": "Content-Type", @@ -1330,67 +1285,76 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"name\": \"test project\",\n\t\t\"description\": \"Hello I am a test project\",\n\t\t\"type\": \"generic\"\n\t}\n}" + "raw": "{\n\t\"param\": {\n\t\t\"name\": \"project name updated\"\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects", + "raw": "{{api-url}}/v4/projects/2", "host": [ "{{api-url}}" ], "path": [ "v4", - "projects" + "projects", + "2" ] }, - "description": "Valid request body. Project should be created successfully." + "description": "Update the project name. If user don't have permission to the project than it should return 403." }, "response": [] }, { - "name": "Get project by id", + "name": "Update project 404", "request": { - "method": "GET", + "method": "PATCH", "header": [ { "key": "Authorization", "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" } ], "body": { "mode": "raw", - "raw": "" + "raw": "{\n\t\"param\": {\n\t\t\"name\": \"project name updated\"\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/1", + "raw": "{{api-url}}/v4/projects/10", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "1" + "10" ] }, - "description": "Get a project by id. project members and attachments should also be returned." + "description": "Update the project name. If project is not found than this result in 404 status code." }, "response": [] }, { - "name": "Get project by id and request specific fields", + "name": "Update project status to in review", "request": { - "method": "GET", + "method": "PATCH", "header": [ { "key": "Authorization", "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" } ], "body": { "mode": "raw", - "raw": "" + "raw": "{\n \"param\": {\n \"status\": \"in_review\"\n }\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/1?fields=id,name,description,members.id,members.projectId", + "raw": "{{api-url}}/v4/projects/1", "host": [ "{{api-url}}" ], @@ -1398,346 +1362,343 @@ "v4", "projects", "1" - ], - "query": [ - { - "key": "fields", - "value": "id,name,description,members.id,members.projectId" - } ] }, - "description": "Get a project by id. project members and attachments should also be returned. Only those fields which are specified should be returned." + "description": "Update the project status." }, "response": [] }, { - "name": "List projects", + "name": "Update project status to reviewed", "request": { - "method": "GET", + "method": "PATCH", "header": [ { "key": "Authorization", "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" } ], "body": { "mode": "raw", - "raw": "" + "raw": "{\n \"param\": {\n \"status\": \"reviewed\"\n }\n}" }, "url": { - "raw": "{{api-url}}/v4/projects", + "raw": "{{api-url}}/v4/projects/7", "host": [ "{{api-url}}" ], "path": [ "v4", - "projects" + "projects", + "7" ] }, - "description": "List all the project with no filter. Default sort and limits are applied." + "description": "Update the project status." }, "response": [] }, { - "name": "List projects with limit and offset", + "name": "Update project status to paused", "request": { - "method": "GET", + "method": "PATCH", "header": [ { "key": "Authorization", "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" } ], "body": { "mode": "raw", - "raw": "" + "raw": "{\n \"param\": {\n \"status\": \"paused\"\n }\n}" }, "url": { - "raw": "{{api-url}}/v4/projects?limit=1&offset=1", + "raw": "{{api-url}}/v4/projects/7", "host": [ "{{api-url}}" ], "path": [ "v4", - "projects" - ], - "query": [ - { - "key": "limit", - "value": "1" - }, - { - "key": "offset", - "value": "1" - } + "projects", + "7" ] }, - "description": "List all the project with no filter. Limit of 1 and offset of 1 is applied" + "description": "Update the project status." }, "response": [] }, { - "name": "List projects with filters - type (exact)", + "name": "Update project status to cancelled with cancelReason", "request": { - "method": "GET", + "method": "PATCH", "header": [ { "key": "Authorization", "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" } ], "body": { "mode": "raw", - "raw": "" + "raw": "{\n \"param\": {\n \"status\": \"cancelled\",\n \"cancelReason\": \"price/cost\"\n }\n}" }, "url": { - "raw": "{{api-url}}/v4/projects?filter=type%3Dapp", + "raw": "{{api-url}}/v4/projects/7", "host": [ "{{api-url}}" ], "path": [ "v4", - "projects" - ], - "query": [ - { - "key": "filter", - "value": "type%3Dapp" - } + "projects", + "7" ] }, - "description": "List all the project with filters applied. The filter string should be url encoded. Default limit and offset is applicable" + "description": "Update the project status. While cancelling the project `cancelReason` is mandatory." }, "response": [] }, { - "name": "List projects with filters - id (exact)", + "name": "Update project status to cancelled", "request": { - "method": "GET", + "method": "PATCH", "header": [ { "key": "Authorization", "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" } ], "body": { "mode": "raw", - "raw": "" + "raw": "{\n\t\"param\": {\n\t\t\"status\": \"cancelled\"\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects?filter=id%3D1", + "raw": "{{api-url}}/v4/projects/1", "host": [ "{{api-url}}" ], "path": [ "v4", - "projects" - ], - "query": [ - { - "key": "filter", - "value": "id%3D1" - } + "projects", + "1" ] }, - "description": "List all the project with filters applied. The filter string should be url encoded. Default limit and offset is applicable" + "description": "Update the project status. While cancelling the project `cancelReason` is mandatory. If no `cancelReason` is supplied this should result in 422 status code." }, "response": [] }, { - "name": "List projects with filters - name, code, customer, manager", + "name": "Update project status to completed", "request": { - "method": "GET", + "method": "PATCH", "header": [ { "key": "Authorization", "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" } ], "body": { "mode": "raw", - "raw": "" + "raw": "{\n \"param\": {\n \"status\": \"completed\"\n }\n}" }, "url": { - "raw": "{{api-url}}/v4/projects?filter=id%3D1*%26name%3Dtes*%26code=test*%26customer%3DDiya*%26manager=first*", + "raw": "{{api-url}}/v4/projects/7", "host": [ "{{api-url}}" ], "path": [ "v4", - "projects" - ], - "query": [ - { - "key": "filter", - "value": "id%3D1*%26name%3Dtes*%26code=test*%26customer%3DDiya*%26manager=first*" - } + "projects", + "7" ] }, - "description": "List all the project with filters applied. The filter string should be url encoded. Default limit and offset is applicable" + "description": "Update the project status." }, "response": [] }, { - "name": "List projects with filters - code", + "name": "Move project out of cancel state.", "request": { - "method": "GET", + "method": "PATCH", "header": [ { "key": "Authorization", "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" } ], "body": { "mode": "raw", - "raw": "" + "raw": "{\n\t\"param\": {\n\t\t\"status\": \"active\"\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects?filter=code%3Dtest*", + "raw": "{{api-url}}/v4/projects/1", "host": [ "{{api-url}}" ], "path": [ "v4", - "projects" - ], - "query": [ - { - "key": "filter", - "value": "code%3Dtest*" - } + "projects", + "1" ] }, - "description": "List all the project with filters applied. The filter string should be url encoded. Default limit and offset is applicable" + "description": "Move a project out of cancel state. Only admin and manager is allowed to do so." }, "response": [] }, { - "name": "List projects with sort applied", + "name": "Move project out of cancel state 403", "request": { - "method": "GET", + "method": "PATCH", "header": [ { "key": "Authorization", "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" } ], "body": { "mode": "raw", - "raw": "" + "raw": "{\n\t\"param\": {\n\t\t\"status\": \"active\"\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects?sort=type%20desc", + "raw": "{{api-url}}/v4/projects/1", "host": [ "{{api-url}}" ], "path": [ "v4", - "projects" - ], - "query": [ - { - "key": "sort", - "value": "type%20desc" - } + "projects", + "1" ] }, - "description": "List all the project with custom sort and no filter. Default sort and limits are applied. The sort string has to be url encoded. Sort is of type `key asc|desc`" + "description": "Move a project out of cancel state. Only admin and manager is allowed to do so." }, "response": [] }, { - "name": "List projects and return specific fields", + "name": "Update project details", "request": { - "method": "GET", + "method": "PATCH", "header": [ { "key": "Authorization", "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" } ], "body": { "mode": "raw", - "raw": "" + "raw": "{\n \"param\": {\n \"details\": {\n \"summary\": \"project name updated\"\n }\n }\n}" }, "url": { - "raw": "{{api-url}}/v4/projects?fields=id,name,description", + "raw": "{{api-url}}/v4/projects/8", "host": [ "{{api-url}}" ], "path": [ "v4", - "projects" - ], - "query": [ - { - "key": "fields", - "value": "id,name,description" - } + "projects", + "8" ] }, - "description": "List all the project with no filter. Default sort and limits are applied. The fields to return is specified as comma separated list. Only those fields should be returned." + "description": "Update the project details. This should fire specification modified event" }, "response": [] }, { - "name": "get projects with copilot token", + "name": "Update project bookmarks", "request": { - "method": "GET", + "method": "PATCH", "header": [ { "key": "Authorization", - "value": "Bearer {{jwt-token-copilot-40051332}}" + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" } ], "body": { "mode": "raw", - "raw": "" + "raw": "{\n \"param\": {\n \"bookmarks\": [\n {\n \"title\": \"test\",\n \"address\": \"http://topcoder.com\"\n }\n \n ]\n }\n}" }, "url": { - "raw": "{{api-url}}/v4/projects", + "raw": "{{api-url}}/v4/projects/8", "host": [ "{{api-url}}" ], "path": [ "v4", - "projects" + "projects", + "8" ] - } + }, + "description": "Update the project bookmarks. This should fire project link created event" }, "response": [] }, { - "name": "DELETE project by id", + "name": "launch a project by topcoder managers ", "request": { - "method": "DELETE", + "method": "PATCH", "header": [ { "key": "Authorization", "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" } ], "body": { "mode": "raw", - "raw": "" + "raw": "{\n \n \"param\":{\n \"name\": \"updatedProject name\",\n \"status\": \"active\"\n }\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/3", + "raw": "{{api-url}}/v4/projects/1", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "3" + "1" ] - }, - "description": "Delete a project by id" + } }, "response": [] }, { - "name": "Update project", + "name": "launch a project by member", "request": { "method": "PATCH", "header": [ @@ -1752,7 +1713,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"param\": {\n \"name\": \"project name updated\"\n }\n}" + "raw": "{\n \n \"param\":{\n \"name\": \"updatedProject name\",\n \"status\": \"active\"\n }\n}" }, "url": { "raw": "{{api-url}}/v4/projects/1", @@ -1764,13 +1725,12 @@ "projects", "1" ] - }, - "description": "Update the project name. Name should be updated successfully." + } }, "response": [] }, { - "name": "Update project 403", + "name": "launch a project by copilot", "request": { "method": "PATCH", "header": [ @@ -1785,27 +1745,32 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"name\": \"project name updated\"\n\t}\n}" + "raw": "{\n \n \"param\":{\n \"name\": \"updatedProject name\",\n \"status\": \"active\"\n }\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/2", + "raw": "{{api-url}}/v4/projects/1", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "2" + "1" ] - }, - "description": "Update the project name. If user don't have permission to the project than it should return 403." + } }, "response": [] - }, + } + ], + "description": "Requests for all things projects." + }, + { + "name": "EventHandling and Integration with Direct Project API", + "item": [ { - "name": "Update project 404", + "name": "mock direct projects", "request": { - "method": "PATCH", + "method": "GET", "header": [ { "key": "Authorization", @@ -1818,27 +1783,29 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"name\": \"project name updated\"\n\t}\n}" + "raw": "" }, "url": { - "raw": "{{api-url}}/v4/projects/10", + "raw": "https://api.topcoder-dev.com/v3/direct/projects", + "protocol": "https", "host": [ - "{{api-url}}" + "api", + "topcoder-dev", + "com" ], "path": [ - "v4", - "projects", - "10" + "v3", + "direct", + "projects" ] - }, - "description": "Update the project name. If project is not found than this result in 404 status code." + } }, "response": [] }, { - "name": "Update project status to in review", + "name": " Create direct project when a new project is successfully created", "request": { - "method": "PATCH", + "method": "POST", "header": [ { "key": "Authorization", @@ -1851,27 +1818,25 @@ ], "body": { "mode": "raw", - "raw": "{\n \"param\": {\n \"status\": \"in_review\"\n }\n}" + "raw": "{\n \"param\": {\n \"type\": \"generic\",\n \"description\": \"test project\",\n \"details\": {},\n \"billingAccountId\": 123,\n \"name\": \"test project1\"\n }\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/1", + "raw": "{{api-url}}/v4/projects", "host": [ "{{api-url}}" ], "path": [ "v4", - "projects", - "1" + "projects" ] - }, - "description": "Update the project status." + } }, "response": [] }, { - "name": "Update project status to reviewed", + "name": "Response error from direct project service", "request": { - "method": "PATCH", + "method": "POST", "header": [ { "key": "Authorization", @@ -1884,27 +1849,27 @@ ], "body": { "mode": "raw", - "raw": "{\n \"param\": {\n \"status\": \"reviewed\"\n }\n}" + "raw": "{\n \"param\": {\n \"userId\": 2, \n \"role\": \"copilot\"\n }\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/7", + "raw": "{{api-url}}/v4/projects/1/members", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "7" + "1", + "members" ] - }, - "description": "Update the project status." + } }, "response": [] }, { - "name": "Update project status to paused", + "name": " Add co-pilot when a co-pilot is added to a project", "request": { - "method": "PATCH", + "method": "POST", "header": [ { "key": "Authorization", @@ -1917,25 +1882,25 @@ ], "body": { "mode": "raw", - "raw": "{\n \"param\": {\n \"status\": \"paused\"\n }\n}" + "raw": "{\n \"param\": {\n \"userId\": 2, \n \"role\": \"copilot\"\n }\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/7", + "raw": "{{api-url}}/v4/projects/2/members", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "7" + "2", + "members" ] - }, - "description": "Update the project status." + } }, "response": [] }, { - "name": "Update project status to cancelled with cancelReason", + "name": "remove copilot from direct project when editing project member role", "request": { "method": "PATCH", "header": [ @@ -1950,25 +1915,26 @@ ], "body": { "mode": "raw", - "raw": "{\n \"param\": {\n \"status\": \"cancelled\",\n \"cancelReason\": \"price/cost\"\n }\n}" + "raw": " {\n \"param\": {\n \"role\": \"customer\",\n \"isPrimary\": true\n }\n } " }, "url": { - "raw": "{{api-url}}/v4/projects/7", + "raw": "{{api-url}}/v4/projects/2/members/4", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "7" + "2", + "members", + "4" ] - }, - "description": "Update the project status. While cancelling the project `cancelReason` is mandatory." + } }, "response": [] }, { - "name": "Update project status to cancelled", + "name": " Sync billing account id with direct", "request": { "method": "PATCH", "header": [ @@ -1983,27 +1949,26 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"status\": \"cancelled\"\n\t}\n}" + "raw": "{\n \"param\": {\n \"billingAccountId\": 9999, \n \"name\": \"new project name\"\n }\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/1", + "raw": "{{api-url}}/v4/projects/2", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "1" + "2" ] - }, - "description": "Update the project status. While cancelling the project `cancelReason` is mandatory. If no `cancelReason` is supplied this should result in 422 status code." + } }, "response": [] }, { - "name": "Update project status to completed", + "name": "Delete co-pilot when a co-pilot is removed from a project", "request": { - "method": "PATCH", + "method": "DELETE", "header": [ { "key": "Authorization", @@ -2016,27 +1981,55 @@ ], "body": { "mode": "raw", - "raw": "{\n \"param\": {\n \"status\": \"completed\"\n }\n}" + "raw": "" }, "url": { - "raw": "{{api-url}}/v4/projects/7", + "raw": "{{api-url}}/v4/projects/2/members/4", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "7" + "2", + "members", + "4" ] - }, - "description": "Update the project status." + } }, "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "id": "ef96ac6a-0fc0-4a64-a4fe-5390e17afe67", + "type": "text/javascript", + "exec": [ + "" + ] + } }, { - "name": "Move project out of cancel state.", + "listen": "test", + "script": { + "id": "12f9d794-0872-4058-aafa-77b89e72025b", + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + }, + { + "name": "Project Phase", + "item": [ + { + "name": "Create Phase", "request": { - "method": "PATCH", + "method": "POST", "header": [ { "key": "Authorization", @@ -2049,27 +2042,27 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"status\": \"active\"\n\t}\n}" + "raw": "{\n\t\"param\": {\n\t\t\"name\": \"test project phase\",\n\t\t\"status\": \"active\",\n\t\t\"startDate\": \"2018-05-15T00:00:00\",\n\t\t\"endDate\": \"2018-05-16T00:00:00\",\n\t\t\"budget\": 20,\n\t\t\"details\": {\n\t\t\t\"aDetails\": \"a details\"\n\t\t}\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/1", + "raw": "{{api-url}}/v4/projects/1/phases", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "1" + "1", + "phases" ] - }, - "description": "Move a project out of cancel state. Only admin and manager is allowed to do so." + } }, "response": [] }, { - "name": "Move project out of cancel state 403", + "name": "Create Phase with order", "request": { - "method": "PATCH", + "method": "POST", "header": [ { "key": "Authorization", @@ -2082,27 +2075,27 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"status\": \"active\"\n\t}\n}" + "raw": "{\n\t\"param\": {\n\t\t\"name\": \"test project phase\",\n\t\t\"status\": \"active\",\n\t\t\"startDate\": \"2018-05-15T00:00:00\",\n\t\t\"endDate\": \"2018-05-16T00:00:00\",\n\t\t\"budget\": 20,\n\t\t\"details\": {\n\t\t\t\"aDetails\": \"a details\"\n\t\t},\n\t\t\"order\": 1\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/1", + "raw": "{{api-url}}/v4/projects/1/phases", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "1" + "1", + "phases" ] - }, - "description": "Move a project out of cancel state. Only admin and manager is allowed to do so." + } }, "response": [] }, { - "name": "Update project details", + "name": "Create Phase with productTemplateId", "request": { - "method": "PATCH", + "method": "POST", "header": [ { "key": "Authorization", @@ -2115,27 +2108,27 @@ ], "body": { "mode": "raw", - "raw": "{\n \"param\": {\n \"details\": {\n \"summary\": \"project name updated\"\n }\n }\n}" + "raw": "{\n\t\"param\": {\n\t\t\"name\": \"test project phase\",\n\t\t\"status\": \"active\",\n\t\t\"startDate\": \"2018-05-15T00:00:00\",\n\t\t\"endDate\": \"2018-05-16T00:00:00\",\n\t\t\"budget\": 20,\n\t\t\"details\": {\n\t\t\t\"aDetails\": \"a details\"\n\t\t},\n\t\t\"order\": 1,\n\t\t\"productTemplateId\": 1\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/8", + "raw": "{{api-url}}/v4/projects/1/phases", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "8" + "1", + "phases" ] - }, - "description": "Update the project details. This should fire specification modified event" + } }, "response": [] }, { - "name": "Update project bookmarks", + "name": "List Phase", "request": { - "method": "PATCH", + "method": "GET", "header": [ { "key": "Authorization", @@ -2148,27 +2141,27 @@ ], "body": { "mode": "raw", - "raw": "{\n \"param\": {\n \"bookmarks\": [\n {\n \"title\": \"test\",\n \"address\": \"http://topcoder.com\"\n }\n \n ]\n }\n}" + "raw": "" }, "url": { - "raw": "{{api-url}}/v4/projects/8", + "raw": "{{api-url}}/v4/projects/1/phases", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "8" + "1", + "phases" ] - }, - "description": "Update the project bookmarks. This should fire project link created event" + } }, "response": [] }, { - "name": "launch a project by topcoder managers ", + "name": "List Phase with fields", "request": { - "method": "PATCH", + "method": "GET", "header": [ { "key": "Authorization", @@ -2181,26 +2174,33 @@ ], "body": { "mode": "raw", - "raw": "{\n \n \"param\":{\n \"name\": \"updatedProject name\",\n \"status\": \"active\"\n }\n}" + "raw": "" }, "url": { - "raw": "{{api-url}}/v4/projects/1", + "raw": "{{api-url}}/v4/projects/1/phases?fields=status,name,budget", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "1" + "1", + "phases" + ], + "query": [ + { + "key": "fields", + "value": "status,name,budget" + } ] } }, "response": [] }, { - "name": "launch a project by member", + "name": "List Phase with sort", "request": { - "method": "PATCH", + "method": "GET", "header": [ { "key": "Authorization", @@ -2213,26 +2213,33 @@ ], "body": { "mode": "raw", - "raw": "{\n \n \"param\":{\n \"name\": \"updatedProject name\",\n \"status\": \"active\"\n }\n}" + "raw": "" }, "url": { - "raw": "{{api-url}}/v4/projects/1", + "raw": "{{api-url}}/v4/projects/1/phases?sort=status desc", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "1" + "1", + "phases" + ], + "query": [ + { + "key": "sort", + "value": "status desc" + } ] } }, "response": [] }, { - "name": "launch a project by copilot", + "name": "List Phase with sort by order", "request": { - "method": "PATCH", + "method": "GET", "header": [ { "key": "Authorization", @@ -2245,30 +2252,31 @@ ], "body": { "mode": "raw", - "raw": "{\n \n \"param\":{\n \"name\": \"updatedProject name\",\n \"status\": \"active\"\n }\n}" + "raw": "" }, "url": { - "raw": "{{api-url}}/v4/projects/1", + "raw": "{{api-url}}/v4/projects/1/phases?sort=order desc", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "1" + "1", + "phases" + ], + "query": [ + { + "key": "sort", + "value": "order desc" + } ] } }, "response": [] - } - ], - "description": "Requests for all things projects." - }, - { - "name": "EventHandling and Integration with Direct Project API", - "item": [ + }, { - "name": "mock direct projects", + "name": "Get Phase", "request": { "method": "GET", "header": [ @@ -2286,26 +2294,25 @@ "raw": "" }, "url": { - "raw": "https://api.topcoder-dev.com/v3/direct/projects", - "protocol": "https", + "raw": "{{api-url}}/v4/projects/1/phases/1", "host": [ - "api", - "topcoder-dev", - "com" + "{{api-url}}" ], "path": [ - "v3", - "direct", - "projects" + "v4", + "projects", + "1", + "phases", + "1" ] } }, "response": [] }, { - "name": " Create direct project when a new project is successfully created", + "name": "Update Phase", "request": { - "method": "POST", + "method": "PATCH", "header": [ { "key": "Authorization", @@ -2318,25 +2325,28 @@ ], "body": { "mode": "raw", - "raw": "{\n \"param\": {\n \"type\": \"generic\",\n \"description\": \"test project\",\n \"details\": {},\n \"billingAccountId\": 123,\n \"name\": \"test project1\"\n }\n}" + "raw": "{\n\t\"param\": {\n\t\t\"name\": \"test project phase xxx\",\n\t\t\"status\": \"inactive\",\n\t\t\"startDate\": \"2018-05-14T00:00:00\",\n\t\t\"endDate\": \"2018-05-15T00:00:00\",\n\t\t\"budget\": 30,\n\t\t\"progress\": 15,\n\t\t\"details\": {\n\t\t\t\"message\": \"phase details\"\n\t\t}\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects", + "raw": "{{api-url}}/v4/projects/1/phases/1", "host": [ "{{api-url}}" ], "path": [ "v4", - "projects" + "projects", + "1", + "phases", + "1" ] } }, "response": [] }, { - "name": "Response error from direct project service", + "name": "Update Phase with order", "request": { - "method": "POST", + "method": "PATCH", "header": [ { "key": "Authorization", @@ -2349,10 +2359,10 @@ ], "body": { "mode": "raw", - "raw": "{\n \"param\": {\n \"userId\": 2, \n \"role\": \"copilot\"\n }\n}" + "raw": "{\n\t\"param\": {\n\t\t\"name\": \"test project phase xxx\",\n\t\t\"status\": \"inactive\",\n\t\t\"startDate\": \"2018-05-14T00:00:00\",\n\t\t\"endDate\": \"2018-05-15T00:00:00\",\n\t\t\"budget\": 30,\n\t\t\"progress\": 15,\n\t\t\"details\": {\n\t\t\t\"message\": \"phase details\"\n\t\t},\n\t\t\"order\": 1\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/1/members", + "raw": "{{api-url}}/v4/projects/1/phases/1", "host": [ "{{api-url}}" ], @@ -2360,16 +2370,17 @@ "v4", "projects", "1", - "members" + "phases", + "1" ] } }, "response": [] }, { - "name": " Add co-pilot when a co-pilot is added to a project", + "name": "Delete Phase", "request": { - "method": "POST", + "method": "DELETE", "header": [ { "key": "Authorization", @@ -2382,27 +2393,33 @@ ], "body": { "mode": "raw", - "raw": "{\n \"param\": {\n \"userId\": 2, \n \"role\": \"copilot\"\n }\n}" + "raw": "" }, "url": { - "raw": "{{api-url}}/v4/projects/2/members", + "raw": "{{api-url}}/v4/projects/1/phases/3", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "2", - "members" + "1", + "phases", + "3" ] } }, "response": [] - }, + } + ] + }, + { + "name": "Phase Products", + "item": [ { - "name": "remove copilot from direct project when editing project member role", + "name": "Create Phase Product", "request": { - "method": "PATCH", + "method": "POST", "header": [ { "key": "Authorization", @@ -2415,68 +2432,64 @@ ], "body": { "mode": "raw", - "raw": " {\n \"param\": {\n \"role\": \"customer\",\n \"isPrimary\": true\n }\n } " + "raw": "{\n\t\"param\": {\n\t\t\"name\": \"test phase product\",\n\t\t\"type\": \"type 1\",\n\t\t\"estimatedPrice\": 10\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/2/members/4", + "raw": "{{api-url}}/v4/projects/1/phases/1/products", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "2", - "members", - "4" + "1", + "phases", + "1", + "products" ] } }, "response": [] }, { - "name": " Sync billing account id with direct", + "name": "List Phase Products", "request": { - "method": "PATCH", + "method": "GET", "header": [ { "key": "Authorization", "value": "Bearer {{jwt-token}}" - }, - { - "key": "Content-Type", - "value": "application/json" } ], "body": { "mode": "raw", - "raw": "{\n \"param\": {\n \"billingAccountId\": 9999, \n \"name\": \"new project name\"\n }\n}" + "raw": "" }, "url": { - "raw": "{{api-url}}/v4/projects/2", + "raw": "{{api-url}}/v4/projects/1/phases/1/products", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "2" + "1", + "phases", + "1", + "products" ] } }, "response": [] }, { - "name": "Delete co-pilot when a co-pilot is removed from a project", + "name": "Get Phase Product", "request": { - "method": "DELETE", + "method": "GET", "header": [ { "key": "Authorization", "value": "Bearer {{jwt-token}}" - }, - { - "key": "Content-Type", - "value": "application/json" } ], "body": { @@ -2484,52 +2497,27 @@ "raw": "" }, "url": { - "raw": "{{api-url}}/v4/projects/2/members/4", + "raw": "{{api-url}}/v4/projects/1/phases/1/products/1", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "2", - "members", - "4" + "1", + "phases", + "1", + "products", + "1" ] } }, "response": [] - } - ], - "event": [ - { - "listen": "prerequest", - "script": { - "id": "ef96ac6a-0fc0-4a64-a4fe-5390e17afe67", - "type": "text/javascript", - "exec": [ - "" - ] - } }, { - "listen": "test", - "script": { - "id": "12f9d794-0872-4058-aafa-77b89e72025b", - "type": "text/javascript", - "exec": [ - "" - ] - } - } - ] - }, - { - "name": "Project Phase", - "item": [ - { - "name": "Create Phase", + "name": "Update Phase Product", "request": { - "method": "POST", + "method": "PATCH", "header": [ { "key": "Authorization", @@ -2542,43 +2530,42 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"name\": \"test project phase\",\n\t\t\"status\": \"active\",\n\t\t\"startDate\": \"2019-02-15T00:00:00\",\n\t\t\"endDate\": \"2019-05-16T00:00:00\",\n\t\t\"budget\": 20,\n\t\t\"details\": {\n\t\t\t\"aDetails\": \"a details\"\n\t\t}\n\t}\n}" + "raw": "{\n\t\"param\": {\n\t\t\"name\": \"test phase product xxx\",\n\t\t\"type\": \"type 2\",\n\t\t\"templateId\": 10,\n\t\t\"estimatedPrice\": 1.234567,\n\t\t\"actualPrice\": 2.34567,\n\t\t\"details\": {\n\t\t\t\"message\": \"this is a JSON type. You can use any json\"\n\t\t}\n\t}\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/2/phases", + "raw": "{{api-url}}/v4/projects/1/phases/1/products/1", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "2", - "phases" + "1", + "phases", + "1", + "products", + "1" ] } }, "response": [] }, { - "name": "Create Phase with order", + "name": "Delete Phase Product", "request": { - "method": "POST", + "method": "DELETE", "header": [ { "key": "Authorization", "value": "Bearer {{jwt-token}}" - }, - { - "key": "Content-Type", - "value": "application/json" } ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"name\": \"test project phase\",\n\t\t\"status\": \"active\",\n\t\t\"startDate\": \"2018-05-15T00:00:00\",\n\t\t\"endDate\": \"2018-05-16T00:00:00\",\n\t\t\"budget\": 20,\n\t\t\"details\": {\n\t\t\t\"aDetails\": \"a details\"\n\t\t},\n\t\t\"order\": 1\n\t}\n}" + "raw": "" }, "url": { - "raw": "{{api-url}}/v4/projects/1/phases", + "raw": "{{api-url}}/v4/projects/1/phases/1/products/1", "host": [ "{{api-url}}" ], @@ -2586,207 +2573,197 @@ "v4", "projects", "1", - "phases" + "phases", + "1", + "products", + "1" ] } }, "response": [] - }, + } + ] + }, + { + "name": "Project Templates", + "item": [ { - "name": "Create Phase with productTemplateId", + "name": "Create project template", "request": { "method": "POST", "header": [ - { - "key": "Authorization", - "value": "Bearer {{jwt-token}}" - }, { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" } ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"name\": \"test project phase\",\n\t\t\"status\": \"active\",\n\t\t\"startDate\": \"2018-05-15T00:00:00\",\n\t\t\"endDate\": \"2018-05-16T00:00:00\",\n\t\t\"budget\": 20,\n\t\t\"details\": {\n\t\t\t\"aDetails\": \"a details\"\n\t\t},\n\t\t\"order\": 1,\n\t\t\"productTemplateId\": 1\n\t}\n}" + "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"key\":\"new key\",\r\n \"category\":\"app\",\r\n \"icon\": \"http://example.com/icon1.ico\",\r\n \"question\": \"question 1\",\r\n \"info\": \"info 1\",\r\n \"aliases\": [\"key-1\", \"key_1\"],\r\n \"scope\":{\r\n \"scope1\":\"scope 1\"\r\n },\r\n \"phases\":{\r\n \"phase1\":\"phase 1\"\r\n }\r\n }\r\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/1/phases", + "raw": "{{api-url}}/v4/projects/metadata/projectTemplates", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "1", - "phases" + "metadata", + "projectTemplates" ] } }, "response": [] }, { - "name": "List Phase", + "name": "Create project template with form, priceConfig, planConfig", "request": { - "method": "GET", + "method": "POST", "header": [ - { - "key": "Authorization", - "value": "Bearer {{jwt-token}}" - }, { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" } ], "body": { "mode": "raw", - "raw": "" + "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"key\":\"new key\",\r\n \"category\":\"app\",\r\n \"icon\": \"http://example.com/icon1.ico\",\r\n \"question\": \"question 1\",\r\n \"info\": \"info 1\",\r\n \"aliases\": [\"key-1\", \"key_1\"],\r\n \"form\": {\r\n \t\"key\": \"dev\",\r\n \t\"version\": 1\r\n },\r\n \"priceConfig\": {\r\n \t\"key\": \"dev\",\r\n \t\"version\": 1\r\n },\r\n \"planConfig\": {\r\n \t\"key\": \"dev\",\r\n \t\"version\": 1\r\n }\r\n }\r\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/1/phases", + "raw": "{{api-url}}/v4/projects/metadata/projectTemplates", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "1", - "phases" + "metadata", + "projectTemplates" ] } }, "response": [] }, { - "name": "List Phase with fields", + "name": "Create project template with only form key", "request": { - "method": "GET", + "method": "POST", "header": [ - { - "key": "Authorization", - "value": "Bearer {{jwt-token}}" - }, { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" } ], "body": { "mode": "raw", - "raw": "" + "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"key\":\"new key\",\r\n \"category\":\"app\",\r\n \"icon\": \"http://example.com/icon1.ico\",\r\n \"question\": \"question 1\",\r\n \"info\": \"info 1\",\r\n \"aliases\": [\"key-1\", \"key_1\"],\r\n \"form\": {\r\n \t\"key\": \"dev\"\r\n }\r\n }\r\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/1/phases?fields=status,name,budget", + "raw": "{{api-url}}/v4/projects/metadata/projectTemplates", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "1", - "phases" - ], - "query": [ - { - "key": "fields", - "value": "status,name,budget" - } + "metadata", + "projectTemplates" ] } }, "response": [] }, { - "name": "List Phase with sort", + "name": "Create project template with wrong form key", "request": { - "method": "GET", + "method": "POST", "header": [ - { - "key": "Authorization", - "value": "Bearer {{jwt-token}}" - }, { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" } ], "body": { "mode": "raw", - "raw": "" + "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"key\":\"new key\",\r\n \"category\":\"app\",\r\n \"icon\": \"http://example.com/icon1.ico\",\r\n \"question\": \"question 1\",\r\n \"info\": \"info 1\",\r\n \"aliases\": [\"key-1\", \"key_1\"],\r\n \"form\": {\r\n \t\"key\": \"wrong-key\"\r\n }\r\n }\r\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/1/phases?sort=status desc", + "raw": "{{api-url}}/v4/projects/metadata/projectTemplates", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "1", - "phases" - ], - "query": [ - { - "key": "sort", - "value": "status desc" - } + "metadata", + "projectTemplates" ] } }, "response": [] }, { - "name": "List Phase with sort by order", + "name": "Create project template with wrong model version", "request": { - "method": "GET", + "method": "POST", "header": [ - { - "key": "Authorization", - "value": "Bearer {{jwt-token}}" - }, { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" } ], "body": { "mode": "raw", - "raw": "" + "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"key\":\"new key\",\r\n \"category\":\"app\",\r\n \"icon\": \"http://example.com/icon1.ico\",\r\n \"question\": \"question 1\",\r\n \"info\": \"info 1\",\r\n \"aliases\": [\"key-1\", \"key_1\"],\r\n \"form\": {\r\n \t\"key\": \"dev\",\r\n \t\"version\": 1123\r\n },\r\n \"priceConfig\": {\r\n \t\"key\": \"dev\",\r\n \t\"version\": 1123\r\n },\r\n \"planConfig\": {\r\n \t\"key\": \"dev\",\r\n \t\"version\": 1123\r\n }\r\n }\r\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/1/phases?sort=order desc", + "raw": "{{api-url}}/v4/projects/metadata/projectTemplates", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "1", - "phases" - ], - "query": [ - { - "key": "sort", - "value": "order desc" - } + "metadata", + "projectTemplates" ] } }, "response": [] }, { - "name": "Get Phase", + "name": "List project templates", "request": { "method": "GET", "header": [ - { - "key": "Authorization", - "value": "Bearer {{jwt-token}}" - }, { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" } ], "body": { @@ -2794,49 +2771,48 @@ "raw": "" }, "url": { - "raw": "{{api-url}}/v4/projects/1/phases/1", + "raw": "{{api-url}}/v4/projects/metadata/projectTemplates", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "1", - "phases", - "1" + "metadata", + "projectTemplates" ] } }, "response": [] }, { - "name": "Update Phase", + "name": "Get project template", "request": { - "method": "PATCH", + "method": "GET", "header": [ - { - "key": "Authorization", - "value": "Bearer {{jwt-token}}" - }, { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" } ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"name\": \"test project phase xxx\",\n\t\t\"status\": \"inactive\",\n\t\t\"startDate\": \"2018-05-14T00:00:00\",\n\t\t\"endDate\": \"2018-05-15T00:00:00\",\n\t\t\"budget\": 30,\n\t\t\"progress\": 15,\n\t\t\"details\": {\n\t\t\t\"message\": \"phase details\"\n\t\t}\n\t}\n}" + "raw": "" }, "url": { - "raw": "{{api-url}}/v4/projects/1/phases/1", + "raw": "{{api-url}}/v4/projects/metadata/projectTemplates/1", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "1", - "phases", + "metadata", + "projectTemplates", "1" ] } @@ -2844,217 +2820,49 @@ "response": [] }, { - "name": "Update Phase with order", + "name": "Upgrade a project template with from, priceConfig, planConfig", "request": { - "method": "PATCH", + "method": "POST", "header": [ - { - "key": "Authorization", - "value": "Bearer {{jwt-token}}" - }, { "key": "Content-Type", "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" } ], "body": { "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"name\": \"test project phase xxx\",\n\t\t\"status\": \"inactive\",\n\t\t\"startDate\": \"2018-05-14T00:00:00\",\n\t\t\"endDate\": \"2018-05-15T00:00:00\",\n\t\t\"budget\": 30,\n\t\t\"progress\": 15,\n\t\t\"details\": {\n\t\t\t\"message\": \"phase details\"\n\t\t},\n\t\t\"order\": 1\n\t}\n}" + "raw": "{\r\n \"param\":{\r\n \"form\": {\r\n \t\"key\": \"dev\",\t\r\n \t \"version\": 2\r\n },\r\n \"priceConfig\": {\r\n \t\"key\": \"dev\",\t\r\n \t \"version\": 1\r\n },\r\n \"planConfig\": {\r\n \t\"key\": \"qa\",\t\r\n \t \"version\": 2\t\r\n }\r\n }\r\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/1/phases/1", + "raw": "{{api-url}}/v4/projects/metadata/projectTemplates/2/upgrade", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", - "1", - "phases", - "1" - ] - } - }, - "response": [] - }, - { - "name": "Delete Phase", - "request": { - "method": "DELETE", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{jwt-token}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{api-url}}/v4/projects/1/phases/2", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "projects", - "1", - "phases", - "2" + "metadata", + "projectTemplates", + "2", + "upgrade" ] } }, "response": [] - } - ] - }, - { - "name": "Phase Products", - "item": [ + }, { - "name": "Create Phase Product", + "name": "Upgrade a project template with wrong model version", "request": { "method": "POST", "header": [ - { - "key": "Authorization", - "value": "Bearer {{jwt-token}}" - }, { "key": "Content-Type", "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"name\": \"test phase product\",\n\t\t\"type\": \"application_development\",\n\t\t\"estimatedPrice\": 10000\n\t}\n}" - }, - "url": { - "raw": "{{api-url}}/v4/projects/1/phases/1/products", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "projects", - "1", - "phases", - "1", - "products" - ] - } - }, - "response": [] - }, - { - "name": "List Phase Products", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{jwt-token}}" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{api-url}}/v4/projects/1/phases/1/products", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "projects", - "1", - "phases", - "1", - "products" - ] - } - }, - "response": [] - }, - { - "name": "Get Phase Product", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{jwt-token}}" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{api-url}}/v4/projects/1/phases/1/products/1", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "projects", - "1", - "phases", - "1", - "products", - "1" - ] - } - }, - "response": [] - }, - { - "name": "Update Phase Product", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{jwt-token}}" }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n\t\"param\": {\n\t\t\"name\": \"test phase product xxx\",\n\t\t\"type\": \"type 2\",\n\t\t\"templateId\": 10,\n\t\t\"estimatedPrice\": 1.234567,\n\t\t\"actualPrice\": 2.34567,\n\t\t\"details\": {\n\t\t\t\"message\": \"this is a JSON type. You can use any json\"\n\t\t}\n\t}\n}" - }, - "url": { - "raw": "{{api-url}}/v4/projects/1/phases/1/products/1", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "projects", - "1", - "phases", - "1", - "products", - "1" - ] - } - }, - "response": [] - }, - { - "name": "Delete Phase Product", - "request": { - "method": "DELETE", - "header": [ { "key": "Authorization", "value": "Bearer {{jwt-token}}" @@ -3062,33 +2870,27 @@ ], "body": { "mode": "raw", - "raw": "" + "raw": "{\r\n \"param\":{\r\n \"form\": {\r\n \t\"key\": \"dev\",\t\r\n \t \"version\": 1234\r\n },\r\n \"priceConfig\": {\r\n \t\"key\": \"dev\",\t\r\n \t \"version\": 1234\r\n },\r\n \"planConfig\": {\r\n \t\"key\": \"dev\",\t\r\n \t \"version\": 1234\r\n }\r\n }\r\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/1/phases/1/products/1", + "raw": "{{api-url}}/v4/projects/metadata/projectTemplates/1/upgrade", "host": [ "{{api-url}}" ], "path": [ "v4", "projects", + "metadata", + "projectTemplates", "1", - "phases", - "1", - "products", - "1" + "upgrade" ] } }, "response": [] - } - ] - }, - { - "name": "Project Templates", - "item": [ + }, { - "name": "Create project template", + "name": "Upgrade a project template without define form, priceConfig, planConfig", "request": { "method": "POST", "header": [ @@ -3103,10 +2905,10 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"key\":\"new key\",\r\n \"category\":\"app\",\r\n \"icon\": \"http://example.com/icon1.ico\",\r\n \"question\": \"question 1\",\r\n \"info\": \"info 1\",\r\n \"aliases\": [\"key-1\", \"key_1\"],\r\n \"scope\":{\r\n \"scope1\":\"scope 1\"\r\n },\r\n \"phases\":{\r\n \"phase1\":\"phase 1\"\r\n }\r\n }\r\n}" + "raw": "{\r\n \"param\":{ \r\n }\r\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/metadata/projectTemplates", + "raw": "{{api-url}}/v4/projects/metadata/projectTemplates/3/upgrade", "host": [ "{{api-url}}" ], @@ -3114,16 +2916,18 @@ "v4", "projects", "metadata", - "projectTemplates" + "projectTemplates", + "3", + "upgrade" ] } }, "response": [] }, { - "name": "List project templates", + "name": "Update project template", "request": { - "method": "GET", + "method": "PATCH", "header": [ { "key": "Content-Type", @@ -3136,10 +2940,10 @@ ], "body": { "mode": "raw", - "raw": "" + "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"key\":\"new key\",\r\n \"category\":\"app\",\r\n \"scope\":{\r\n \"scope1\":\"scope 1\",\r\n \"scope2\": [\"a\"]\r\n },\r\n \"phases\":{\r\n \"phase1\":\"phase 1\",\r\n \"phase2\": {\r\n \t\"another\": \"another\"\r\n }\r\n }\r\n }\r\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/metadata/projectTemplates", + "raw": "{{api-url}}/v4/projects/metadata/projectTemplates/1", "host": [ "{{api-url}}" ], @@ -3147,16 +2951,17 @@ "v4", "projects", "metadata", - "projectTemplates" + "projectTemplates", + "1" ] } }, "response": [] }, { - "name": "Get project template", + "name": "Update project template with define form, priceConfig, planConfig", "request": { - "method": "GET", + "method": "PATCH", "header": [ { "key": "Content-Type", @@ -3169,7 +2974,7 @@ ], "body": { "mode": "raw", - "raw": "" + "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"key\":\"new key\",\r\n \"category\":\"app\",\r\n \"form\": {\r\n \"key\": \"dev\",\r\n \"version\": 1\r\n },\r\n \"priceConfig\": {\r\n \"key\": \"dev\",\r\n \"version\": 1\r\n },\r\n \"planConfig\": {\r\n \"key\": \"dev\",\r\n \"version\": 1\r\n }\r\n }\r\n}" }, "url": { "raw": "{{api-url}}/v4/projects/metadata/projectTemplates/1", @@ -3188,7 +2993,7 @@ "response": [] }, { - "name": "Update project template", + "name": "Update project template with wrong form, priceConfig, planConfig", "request": { "method": "PATCH", "header": [ @@ -3203,7 +3008,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"key\":\"new key\",\r\n \"category\":\"app\",\r\n \"scope\":{\r\n \"scope1\":\"scope 1\",\r\n \"scope2\": [\"a\"]\r\n },\r\n \"phases\":{\r\n \"phase1\":\"phase 1\",\r\n \"phase2\": {\r\n \t\"another\": \"another\"\r\n }\r\n }\r\n }\r\n}" + "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"key\":\"new key\",\r\n \"category\":\"app\",\r\n\t\"form\": {\r\n \"key\": \"dev\",\r\n \"version\": 1123\r\n },\r\n \"priceConfig\": {\r\n \"key\": \"dev\",\r\n \"version\": 1123\r\n },\r\n \"planConfig\": {\r\n \"key\": \"dev\",\r\n \"version\": 1123\r\n }\r\n }\r\n}" }, "url": { "raw": "{{api-url}}/v4/projects/metadata/projectTemplates/1", @@ -3449,7 +3254,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"key\": \"app\",\r\n \"displayName\": \"new displayName\",\r\n \"icon\": \"http://example.com/icon4.ico\",\r\n \t\"question\": \"question 4\",\r\n \t\"info\": \"info 4\",\r\n \t\"aliases\": [\"key-41\", \"key_42\"],\r\n \t\"metadata\": {}\r\n }\r\n}" + "raw": "{\r\n \"param\":{\r\n \"key\": \"new key\",\r\n \"displayName\": \"new displayName\",\r\n \"icon\": \"http://example.com/icon4.ico\",\r\n \t\"question\": \"question 4\",\r\n \t\"info\": \"info 4\",\r\n \t\"aliases\": [\"key-41\", \"key_42\"],\r\n \t\"metadata\": {}\r\n }\r\n}" }, "url": { "raw": "{{api-url}}/v4/projects/metadata/projectTypes", @@ -3604,10 +3409,10 @@ ] }, { - "name": "Organization Config", + "name": "Product Category", "item": [ { - "name": "Create organization config", + "name": "Create product category", "request": { "method": "POST", "header": [ @@ -3622,10 +3427,10 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"orgId\": \"20000013\",\r\n \"configName\": \"project_catalog_url\",\r\n \"configValue\": \"/projects/1\"\r\n }\r\n}" + "raw": "{\r\n \"param\":{\r\n \"key\": \"generic\",\r\n \"displayName\": \"new displayName\",\r\n \"icon\": \"icon\",\r\n \"question\": \"question\",\r\n \"info\": \"info\",\r\n \"aliases\": [\"key-1\", \"key-2\"]\r\n }\r\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/metadata/orgConfig", + "raw": "{{api-url}}/v4/projects/metadata/productCategories", "host": [ "{{api-url}}" ], @@ -3633,14 +3438,14 @@ "v4", "projects", "metadata", - "orgConfig" + "productCategories" ] } }, "response": [] }, { - "name": "List organization config - error without filter", + "name": "List product categories", "request": { "method": "GET", "header": [ @@ -3658,7 +3463,7 @@ "raw": "" }, "url": { - "raw": "{{api-url}}/v4/projects/metadata/orgConfig", + "raw": "{{api-url}}/v4/projects/metadata/productCategories", "host": [ "{{api-url}}" ], @@ -3666,14 +3471,14 @@ "v4", "projects", "metadata", - "orgConfig" + "productCategories" ] } }, "response": [] }, { - "name": "List organization config - filter", + "name": "Get product category", "request": { "method": "GET", "header": [ @@ -3691,28 +3496,25 @@ "raw": "" }, "url": { - "raw": "{{api-url}}/v4/orgConfig?filter=orgId=in(20000010,20000013,20000015)%26configName%3Dproject_catalog_url", + "raw": "{{api-url}}/v4/projects/metadata/productCategories/generic", "host": [ "{{api-url}}" ], "path": [ "v4", - "orgConfig" - ], - "query": [ - { - "key": "filter", - "value": "orgId=in(20000010,20000013,20000015)%26configName%3Dproject_catalog_url" - } + "projects", + "metadata", + "productCategories", + "generic" ] } }, "response": [] }, { - "name": "Get organization config", + "name": "Update product category", "request": { - "method": "GET", + "method": "PATCH", "header": [ { "key": "Content-Type", @@ -3725,10 +3527,10 @@ ], "body": { "mode": "raw", - "raw": "" + "raw": "{\r\n \"param\":{\r\n \"displayName\": \"Chatbot-updated\"\r\n }\r\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/metadata/orgConfig/1", + "raw": "{{api-url}}/v4/projects/metadata/productCategories/generic", "host": [ "{{api-url}}" ], @@ -3736,17 +3538,17 @@ "v4", "projects", "metadata", - "orgConfig", - "1" + "productCategories", + "generic" ] } }, "response": [] }, { - "name": "Update organization config", + "name": "Delete product category", "request": { - "method": "PATCH", + "method": "DELETE", "header": [ { "key": "Content-Type", @@ -3759,10 +3561,10 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"configName\": \"project_catalog_url\"\r\n }\r\n}" + "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"key\":\"new key\",\r\n \"category\":\"new category\",\r\n \"scope\":{\r\n \"scope1\":\"scope 1\",\r\n \"scope2\": [\"a\"]\r\n },\r\n \"phases\":{\r\n \"phase1\":\"phase 1\",\r\n \"phase2\": {\r\n \t\"another\": \"another\"\r\n }\r\n }\r\n }\r\n}" }, "url": { - "raw": "{{api-url}}/v4/projects/metadata/orgConfig/1", + "raw": "{{api-url}}/v4/projects/metadata/productCategories/generic", "host": [ "{{api-url}}" ], @@ -3770,215 +3572,8 @@ "v4", "projects", "metadata", - "orgConfig", - "1" - ] - } - }, - "response": [] - }, - { - "name": "Delete organization config", - "request": { - "method": "DELETE", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{jwt-token}}" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{api-url}}/v4/projects/metadata/orgConfig/1", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "projects", - "metadata", - "orgConfig", - "1" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Product Category", - "item": [ - { - "name": "Create product category", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{jwt-token}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"key\": \"app\",\r\n \"displayName\": \"new displayName\",\r\n \"icon\": \"icon\",\r\n \"question\": \"question\",\r\n \"info\": \"info\",\r\n \"aliases\": [\"key-1\", \"key-2\"]\r\n }\r\n}" - }, - "url": { - "raw": "{{api-url}}/v4/projects/metadata/productCategories", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "projects", - "metadata", - "productCategories" - ] - } - }, - "response": [] - }, - { - "name": "List product categories", - "request": { - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{jwt-token}}" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{api-url}}/v4/projects/metadata/productCategories", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "projects", - "metadata", - "productCategories" - ] - } - }, - "response": [] - }, - { - "name": "Get product category", - "request": { - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{jwt-token}}" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{api-url}}/v4/projects/metadata/productCategories/generic", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "projects", - "metadata", - "productCategories", - "generic" - ] - } - }, - "response": [] - }, - { - "name": "Update product category", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{jwt-token}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"displayName\": \"Chatbot-updated\"\r\n }\r\n}" - }, - "url": { - "raw": "{{api-url}}/v4/projects/metadata/productCategories/generic", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "projects", - "metadata", - "productCategories", - "generic" - ] - } - }, - "response": [] - }, - { - "name": "Delete product category", - "request": { - "method": "DELETE", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{jwt-token}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"key\":\"new key\",\r\n \"category\":\"new category\",\r\n \"scope\":{\r\n \"scope1\":\"scope 1\",\r\n \"scope2\": [\"a\"]\r\n },\r\n \"phases\":{\r\n \"phase1\":\"phase 1\",\r\n \"phase2\": {\r\n \t\"another\": \"another\"\r\n }\r\n }\r\n }\r\n}" - }, - "url": { - "raw": "{{api-url}}/v4/projects/metadata/productCategories/generic", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "projects", - "metadata", - "productCategories", - "generic" + "productCategories", + "generic" ] } }, @@ -4175,7 +3770,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"description\":\"new description\",\r\n \"startDate\":\"2019-02-29T00:00:00.000Z\",\r\n \"endDate\": \"2019-04-30T00:00:00.000Z\",\r\n \"reference\": \"project\",\r\n \"referenceId\": 1\r\n }\r\n}" + "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"description\":\"new description\",\r\n \"startDate\":\"2018-05-29T00:00:00.000Z\",\r\n \"endDate\": \"2018-05-30T00:00:00.000Z\",\r\n \"reference\": \"project\",\r\n \"referenceId\": 1\r\n }\r\n}" }, "url": { "raw": "{{api-url}}/v4/timelines", @@ -4206,7 +3801,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"description\":\"new description\",\r\n \"startDate\":\"2019-02-29T00:00:00.000Z\",\r\n \"endDate\": \"2019-04-30T00:00:00.000Z\",\r\n \"reference\": \"project\",\r\n \"referenceId\": 1,\r\n \"templateId\": 1\r\n }\r\n}" + "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"description\":\"new description\",\r\n \"startDate\":\"2018-05-29T00:00:00.000Z\",\r\n \"endDate\": \"2018-05-30T00:00:00.000Z\",\r\n \"reference\": \"project\",\r\n \"referenceId\": 1,\r\n \"templateId\": 1\r\n }\r\n}" }, "url": { "raw": "{{api-url}}/v4/timelines", @@ -5499,8 +5094,24 @@ { "name": "Get all metadata", "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, "method": "GET", - "header": [], + "header": [ + { + "key": "", + "value": "", + "type": "text" + } + ], "body": { "mode": "raw", "raw": "" @@ -5518,8 +5129,1455 @@ } }, "response": [] + }, + { + "name": "Get all metadata with includeAllVersion", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata?includeAllReferred=true", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata" + ], + "query": [ + { + "key": "includeAllReferred", + "value": "true" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Form Version", + "item": [ + { + "name": "List forms", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/form/dev/versions", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "form", + "dev", + "versions" + ] + } + }, + "response": [] + }, + { + "name": "Get a particular version", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/form/dev/versions/1", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "form", + "dev", + "versions", + "1" + ] + } + }, + "response": [] + }, + { + "name": "Get latest version", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/form/dev", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "form", + "dev" + ] + } + }, + "response": [] + }, + { + "name": "Create form", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"param\":{\r\n \t\"scope\": {\r\n \t\t\"hello\": \"test\"\r\n \t}\r\n }\r\n}" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/form/dev/versions", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "form", + "dev", + "versions" + ] + } + }, + "response": [] + }, + { + "name": "Update form", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"param\":{\r\n \t\"scope\": {\r\n \t\t\"hello\": \"test111\"\r\n \t}\r\n }\r\n}" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/form/dev/versions/1", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "form", + "dev", + "versions", + "1" + ] + } + }, + "response": [] + }, + { + "name": "Delete form", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/form/dev/versions/1", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "form", + "dev", + "versions", + "1" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Form Revision", + "item": [ + { + "name": "List all revision for version", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/form/dev/versions/1/revisions", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "form", + "dev", + "versions", + "1", + "revisions" + ] + } + }, + "response": [] + }, + { + "name": "Get a particular revision", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/form/dev/versions/1/revisions/1", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "form", + "dev", + "versions", + "1", + "revisions", + "1" + ] + } + }, + "response": [] + }, + { + "name": "Create form", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"param\":{\r\n \t\"scope\": {\r\n \t\t\"hello\": \"test\"\r\n \t}\r\n }\r\n}" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/form/dev/versions/1/revisions", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "form", + "dev", + "versions", + "1", + "revisions" + ] + } + }, + "response": [] + }, + { + "name": "Create for no exist key", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"param\":{\r\n \t\"scope\": {\r\n \t\t\"hello\": \"test\"\r\n \t}\r\n }\r\n}" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/form/no-exist-2222key36/versions/1/revisions", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "form", + "no-exist-2222key36", + "versions", + "1", + "revisions" + ] + } + }, + "response": [] + }, + { + "name": "Delete revision", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/form/dev/versions/1/revisions/1", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "form", + "dev", + "versions", + "1", + "revisions", + "1" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Price Config Version", + "item": [ + { + "name": "List price configs", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/priceConfig/dev/versions", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "priceConfig", + "dev", + "versions" + ] + } + }, + "response": [] + }, + { + "name": "Get a particular version", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/priceConfig/dev/versions/1", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "priceConfig", + "dev", + "versions", + "1" + ] + } + }, + "response": [] + }, + { + "name": "Get latest version", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/priceConfig/dev", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "priceConfig", + "dev" + ] + } + }, + "response": [] + }, + { + "name": "Create priceConfig", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"param\":{\r\n \t\"config\": {\r\n \t\t\"hello\": \"test\"\r\n \t}\r\n }\r\n}" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/priceConfig/dev/versions", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "priceConfig", + "dev", + "versions" + ] + } + }, + "response": [] + }, + { + "name": "Update priceConfig", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"param\":{\r\n \t\"config\": {\r\n \t\t\"hello\": \"test111\"\r\n \t}\r\n }\r\n}" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/priceConfig/dev/versions/1", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "priceConfig", + "dev", + "versions", + "1" + ] + } + }, + "response": [] + }, + { + "name": "Delete priceConfig", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/priceConfig/dev/versions/1", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "priceConfig", + "dev", + "versions", + "1" + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "id": "59182724-4332-4d76-90ea-f7520a7b1be9", + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "id": "abc13dca-e8a4-4995-970f-00e5889a5f2d", + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + }, + { + "name": "Price Config Revision", + "item": [ + { + "name": "List all revision for version", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/priceConfig/dev/versions/3/revisions", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "priceConfig", + "dev", + "versions", + "3", + "revisions" + ] + } + }, + "response": [] + }, + { + "name": "Get a particular revision", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/priceConfig/dev/versions/1/revisions/1", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "priceConfig", + "dev", + "versions", + "1", + "revisions", + "1" + ] + } + }, + "response": [] + }, + { + "name": "Create price config", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"param\":{\r\n \t\"config\": {\r\n \t\t\"hello\": \"test\"\r\n \t}\r\n }\r\n}" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/priceConfig/dev/versions/1/revisions", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "priceConfig", + "dev", + "versions", + "1", + "revisions" + ] + } + }, + "response": [] + }, + { + "name": "Create for no exist key", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"param\":{\r\n \t\"config\": {\r\n \t\t\"hello\": \"test\"\r\n \t}\r\n }\r\n}" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/priceConfig/no-exist-key/versions/1/revisions", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "priceConfig", + "no-exist-key", + "versions", + "1", + "revisions" + ] + } + }, + "response": [] + }, + { + "name": "Delete revision", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/priceConfig/dev/versions/1/revisions/1", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "priceConfig", + "dev", + "versions", + "1", + "revisions", + "1" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Plan Config Version", + "item": [ + { + "name": "List plan configs", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/planConfig/dev/versions", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "planConfig", + "dev", + "versions" + ] + } + }, + "response": [] + }, + { + "name": "Get a particular version", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/planConfig/dev/versions/3", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "planConfig", + "dev", + "versions", + "3" + ] + } + }, + "response": [] + }, + { + "name": "Get latest version", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/planConfig/dev", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "planConfig", + "dev" + ] + } + }, + "response": [] + }, + { + "name": "Create plan config", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"param\":{\r\n \t\"phases\": {\r\n \t\t\"hello\": \"test\"\r\n \t}\r\n }\r\n}" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/planConfig/dev/versions", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "planConfig", + "dev", + "versions" + ] + } + }, + "response": [] + }, + { + "name": "Update plan config", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"param\":{\r\n \t\"phases\": {\r\n \t\t\"hello\": \"test111\"\r\n \t}\r\n }\r\n}" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/planConfig/dev/versions/1", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "planConfig", + "dev", + "versions", + "1" + ] + } + }, + "response": [] + }, + { + "name": "Delete plan config", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/planConfig/dev/versions/1", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "planConfig", + "dev", + "versions", + "1" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Plan Config Revision", + "item": [ + { + "name": "List all revision for version", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/planConfig/dev/versions/1/revisions", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "planConfig", + "dev", + "versions", + "1", + "revisions" + ] + } + }, + "response": [] + }, + { + "name": "Get a particular revision", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/planConfig/dev/versions/1/revisions/1", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "planConfig", + "dev", + "versions", + "1", + "revisions", + "1" + ] + } + }, + "response": [] + }, + { + "name": "Create plan config", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"param\":{\r\n \t\"phases\": {\r\n \t\t\"hello\": \"test\"\r\n \t}\r\n }\r\n}" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/planConfig/dev/versions/1/revisions", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "planConfig", + "dev", + "versions", + "1", + "revisions" + ] + } + }, + "response": [] + }, + { + "name": "Create for no exist key", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"param\":{\r\n \t\"phases\": {\r\n \t\t\"hello\": \"test\"\r\n \t}\r\n }\r\n}" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/planConfig/no-exist-key/versions/1/revisions", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "planConfig", + "no-exist-key", + "versions", + "1", + "revisions" + ] + } + }, + "response": [] + }, + { + "name": "Delete revision", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{api-url}}/v4/projects/metadata/planConfig/dev/versions/1/revisions/1", + "host": [ + "{{api-url}}" + ], + "path": [ + "v4", + "projects", + "metadata", + "planConfig", + "dev", + "versions", + "1", + "revisions", + "1" + ] + } + }, + "response": [] } ] } ] -} +} \ No newline at end of file diff --git a/src/models/form.js b/src/models/form.js new file mode 100644 index 00000000..60d4a00c --- /dev/null +++ b/src/models/form.js @@ -0,0 +1,47 @@ +/* eslint-disable valid-jsdoc */ + +/** + * The Form model + */ + +import versionModelClassMethods from './versionModelClassMethods'; + +module.exports = (sequelize, DataTypes) => { + const Form = sequelize.define('Form', { + id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, + key: { type: DataTypes.STRING(45), allowNull: false }, + version: { type: DataTypes.BIGINT, allowNull: false, defaultValue: 1 }, + revision: { type: DataTypes.BIGINT, allowNull: false, defaultValue: 1 }, + scope: { type: DataTypes.JSON, allowNull: false }, + + deletedAt: { type: DataTypes.DATE, allowNull: true }, + createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + deletedBy: { type: DataTypes.INTEGER, allowNull: true }, + createdBy: { type: DataTypes.INTEGER, allowNull: false }, + updatedBy: { type: DataTypes.INTEGER, allowNull: false }, + }, { + tableName: 'form', + paranoid: true, + timestamps: true, + updatedAt: 'updatedAt', + createdAt: 'createdAt', + deletedAt: 'deletedAt', + indexes: [ + { + unique: true, + fields: ['key', 'version', 'revision'], + }, + ], + }); + + const classMethods = versionModelClassMethods(Form, 'scope'); + Form.deleteOldestRevision = classMethods.deleteOldestRevision; + Form.newVersionNumber = classMethods.newVersionNumber; + Form.createNewVersion = classMethods.createNewVersion; + Form.latestVersion = classMethods.latestVersion; + Form.latestRevisionofLatestVersion = classMethods.latestRevisionofLatestVersion; + Form.latestVersionIncludeUsed = classMethods.latestVersionIncludeUsed; + + return Form; +}; diff --git a/src/models/planConfig.js b/src/models/planConfig.js new file mode 100644 index 00000000..6ec763da --- /dev/null +++ b/src/models/planConfig.js @@ -0,0 +1,47 @@ +/* eslint-disable valid-jsdoc */ + +/** + * The PlanConfig model + */ + +import versionModelClassMethods from './versionModelClassMethods'; + +module.exports = (sequelize, DataTypes) => { + const PlanConfig = sequelize.define('PlanConfig', { + id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, + key: { type: DataTypes.STRING(45), allowNull: false }, + version: { type: DataTypes.BIGINT, allowNull: false, defaultValue: 1 }, + revision: { type: DataTypes.BIGINT, allowNull: false, defaultValue: 1 }, + phases: { type: DataTypes.JSON, allowNull: false }, + + deletedAt: { type: DataTypes.DATE, allowNull: true }, + createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + deletedBy: { type: DataTypes.INTEGER, allowNull: true }, + createdBy: { type: DataTypes.INTEGER, allowNull: false }, + updatedBy: { type: DataTypes.INTEGER, allowNull: false }, + }, { + tableName: 'plan_config', + paranoid: true, + timestamps: true, + updatedAt: 'updatedAt', + createdAt: 'createdAt', + deletedAt: 'deletedAt', + indexes: [ + { + unique: true, + fields: ['key', 'version', 'revision'], + }, + ], + }); + + const classMethods = versionModelClassMethods(PlanConfig, 'phases'); + PlanConfig.deleteOldestRevision = classMethods.deleteOldestRevision; + PlanConfig.newVersionNumber = classMethods.newVersionNumber; + PlanConfig.createNewVersion = classMethods.createNewVersion; + PlanConfig.latestVersion = classMethods.latestVersion; + PlanConfig.latestRevisionofLatestVersion = classMethods.latestRevisionofLatestVersion; + PlanConfig.latestVersionIncludeUsed = classMethods.latestVersionIncludeUsed; + + return PlanConfig; +}; diff --git a/src/models/priceConfig.js b/src/models/priceConfig.js new file mode 100644 index 00000000..e324965d --- /dev/null +++ b/src/models/priceConfig.js @@ -0,0 +1,46 @@ +/* eslint-disable valid-jsdoc */ + +/** + * The PriceConfig model + */ + +import versionModelClassMethods from './versionModelClassMethods'; + +module.exports = (sequelize, DataTypes) => { + const PriceConfig = sequelize.define('PriceConfig', { + id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, + key: { type: DataTypes.STRING(45), allowNull: false }, + version: { type: DataTypes.BIGINT, allowNull: false, defaultValue: 1 }, + revision: { type: DataTypes.BIGINT, allowNull: false, defaultValue: 1 }, + config: { type: DataTypes.JSON, allowNull: false }, + + deletedAt: { type: DataTypes.DATE, allowNull: true }, + createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + deletedBy: { type: DataTypes.INTEGER, allowNull: true }, + createdBy: { type: DataTypes.INTEGER, allowNull: false }, + updatedBy: { type: DataTypes.INTEGER, allowNull: false }, + }, { + tableName: 'price_config', + paranoid: true, + timestamps: true, + updatedAt: 'updatedAt', + createdAt: 'createdAt', + deletedAt: 'deletedAt', + indexes: [ + { + unique: true, + fields: ['key', 'version', 'revision'], + }, + ], + }); + const classMethods = versionModelClassMethods(PriceConfig, 'config'); + PriceConfig.deleteOldestRevision = classMethods.deleteOldestRevision; + PriceConfig.newVersionNumber = classMethods.newVersionNumber; + PriceConfig.createNewVersion = classMethods.createNewVersion; + PriceConfig.latestVersion = classMethods.latestVersion; + PriceConfig.latestRevisionofLatestVersion = classMethods.latestRevisionofLatestVersion; + PriceConfig.latestVersionIncludeUsed = classMethods.latestVersionIncludeUsed; + + return PriceConfig; +}; diff --git a/src/models/projectTemplate.js b/src/models/projectTemplate.js index b2cb2581..dd61765e 100644 --- a/src/models/projectTemplate.js +++ b/src/models/projectTemplate.js @@ -13,8 +13,11 @@ module.exports = (sequelize, DataTypes) => { question: { type: DataTypes.STRING(255), allowNull: false }, info: { type: DataTypes.STRING(255), allowNull: false }, aliases: { type: DataTypes.JSON, allowNull: false }, - scope: { type: DataTypes.JSON, allowNull: false }, - phases: { type: DataTypes.JSON, allowNull: false }, + scope: { type: DataTypes.JSON, allowNull: true }, + phases: { type: DataTypes.JSON, allowNull: true }, + form: { type: DataTypes.JSON }, + planConfig: { type: DataTypes.JSON }, + priceConfig: { type: DataTypes.JSON }, disabled: { type: DataTypes.BOOLEAN, defaultValue: false }, hidden: { type: DataTypes.BOOLEAN, defaultValue: false }, deletedAt: DataTypes.DATE, diff --git a/src/models/versionModelClassMethods.js b/src/models/versionModelClassMethods.js new file mode 100644 index 00000000..3a143e23 --- /dev/null +++ b/src/models/versionModelClassMethods.js @@ -0,0 +1,124 @@ +/* eslint-disable valid-jsdoc */ +/** + * generate class methods for version model + * + * @param {Model} model Model + * @param {string} jsonField field for json + * + * @returns classMethod for Model + */ +function versionModelClassMethods(model, jsonField) { + return { + deleteOldestRevision(userId, key, version) { + return model.findOne({ + where: { + key, + version, + }, + order: [['revision', 'ASC']], + }).then(record => record.update({ deletedBy: userId })).then(record => record.destroy()); + }, + newVersionNumber(key) { + return model.findAll({ + where: { + key, + }, + order: [['version', 'DESC']], + }).then((records) => { + let latestVersion = 1; + if (records.length !== 0) { + const latestVersionRecord = records.reduce((prev, current) => + ((prev.version < current.version) ? current : prev)); + latestVersion = latestVersionRecord.version + 1; + } + return Promise.resolve(latestVersion); + }); + }, + createNewVersion(key, json, userId) { + return model.newVersionNumber(key) + .then(newVersion => model.create({ + version: newVersion, + revision: 1, + key, + [jsonField]: json, + createdBy: userId, + updatedBy: userId, + })); + }, + latestVersion() { + const query = { + attributes: { exclude: ['deletedAt', 'deletedBy'] }, + raw: true, + }; + return model.findAll(query) + .then((records) => { + const keys = {}; + records.forEach((record) => { + const { key, version, revision } = record; + const isNewerVersion = (keys[key] != null) && (keys[key].version < version); + const isNewerRevision = (keys[key] != null) && + (keys[key].version === version) && (keys[key].revision < revision); + if ((keys[key] == null) || isNewerVersion || isNewerRevision) { + keys[key] = record; + } + }); + return Promise.resolve(Object.values(keys)); + }); + }, + latestRevisionofLatestVersion(key) { + return model.findAll({ + where: { + key, + }, + order: [['version', 'DESC'], ['revision', 'DESC']], + attributes: { exclude: ['deletedAt', 'deletedBy'] }, + }) + .then(records => (records.length > 0 ? Promise.resolve(records[0]) : Promise.resolve(null))); + }, + latestVersionIncludeUsed(usedKeyVersionsMap) { + const query = { + attributes: { exclude: ['deletedAt', 'deletedBy'] }, + raw: true, + }; + let allRecord; + let latestVersionRecord; + const usedKeyVersion = usedKeyVersionsMap; + return model.findAll(query) + .then((records) => { + allRecord = records; + return model.latestVersion(); + }).then((records) => { + latestVersionRecord = records; + const versions = {}; + latestVersionRecord.forEach((record) => { + usedKeyVersion[record.key] = usedKeyVersion[record.key] ? usedKeyVersion[record.key] : {}; + usedKeyVersion[record.key][record.version] = true; + }); + + allRecord.forEach((record) => { + const { key, version, revision } = record; + if (usedKeyVersion[key] && usedKeyVersion[key][version]) { + if (versions[key] && versions[key][version]) { + if (versions[key][version].revision < revision) { + versions[record.key][record.version] = record; + } + } else { + versions[key] = versions[key] ? versions[key] : {}; + versions[key][version] = record; + } + } + }); + const result = []; + Object.values(versions).forEach((key) => { + Object.values(key).forEach((record) => { + result.push(record); + }); + }); + return Promise.resolve(result); + }); + }, + }; +} + + +export default versionModelClassMethods; diff --git a/src/permissions/index.js b/src/permissions/index.js index 227c12b8..c68c44a4 100644 --- a/src/permissions/index.js +++ b/src/permissions/index.js @@ -36,6 +36,7 @@ module.exports = () => { Authorizer.setPolicy('productTemplate.create', projectAdmin); Authorizer.setPolicy('productTemplate.edit', projectAdmin); Authorizer.setPolicy('productTemplate.delete', projectAdmin); + Authorizer.setPolicy('projectTemplate.upgrade', projectAdmin); Authorizer.setPolicy('productTemplate.view', true); Authorizer.setPolicy('project.addProjectPhase', copilotAndAbove); @@ -81,4 +82,19 @@ module.exports = () => { Authorizer.setPolicy('projectMemberInvite.create', projectView); Authorizer.setPolicy('projectMemberInvite.put', true); Authorizer.setPolicy('projectMemberInvite.get', true); + + Authorizer.setPolicy('form.create', projectAdmin); + Authorizer.setPolicy('form.edit', projectAdmin); + Authorizer.setPolicy('form.delete', projectAdmin); + Authorizer.setPolicy('form.view', true); // anyone can view form + + Authorizer.setPolicy('priceConfig.create', projectAdmin); + Authorizer.setPolicy('priceConfig.edit', projectAdmin); + Authorizer.setPolicy('priceConfig.delete', projectAdmin); + Authorizer.setPolicy('priceConfig.view', true); // anyone can view plan config + + Authorizer.setPolicy('planConfig.create', projectAdmin); + Authorizer.setPolicy('planConfig.edit', projectAdmin); + Authorizer.setPolicy('planConfig.delete', projectAdmin); + Authorizer.setPolicy('planConfig.view', true); // anyone can view price config }; diff --git a/src/routes/form/revision/create.js b/src/routes/form/revision/create.js new file mode 100644 index 00000000..6a6a2aa0 --- /dev/null +++ b/src/routes/form/revision/create.js @@ -0,0 +1,66 @@ +/** + * API to add a form revision + */ +import validate from 'express-validation'; +import _ from 'lodash'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../../util'; +import models from '../../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + key: Joi.string().max(45).required(), + version: Joi.number().integer().positive().required(), + }, + body: { + param: Joi.object().keys({ + scope: Joi.object().required(), + + createdAt: Joi.any().strip(), + updatedAt: Joi.any().strip(), + deletedAt: Joi.any().strip(), + createdBy: Joi.any().strip(), + updatedBy: Joi.any().strip(), + deletedBy: Joi.any().strip(), + }).required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('form.create'), + (req, res, next) => { + models.sequelize.transaction(() => models.Form.findOne({ + where: { + key: req.params.key, + version: req.params.version, + }, + order: [['revision', 'DESC']], + }).then((form) => { + if (form) { + const version = form ? form.version : 1; + const revision = form ? form.revision + 1 : 1; + const entity = _.assign(req.body.param, { + version, + revision, + createdBy: req.authUser.userId, + updatedBy: req.authUser.userId, + key: req.params.key, + scope: req.body.param.scope, + }); + return models.Form.create(entity); + } + const apiErr = new Error(`Form not exists for key ${req.params.key} version ${req.params.version}`); + apiErr.status = 404; + return Promise.reject(apiErr); + }).then((createdEntity) => { + // Omit deletedAt, deletedBy + res.status(201).json(util.wrapResponse( + req.id, _.omit(createdEntity.toJSON(), 'deletedAt', 'deletedBy'), 1, 201)); + }) + .catch(next)); + }, +]; diff --git a/src/routes/form/revision/create.spec.js b/src/routes/form/revision/create.spec.js new file mode 100644 index 00000000..8bba544b --- /dev/null +++ b/src/routes/form/revision/create.spec.js @@ -0,0 +1,136 @@ +/* eslint-disable no-unused-expressions */ +/** + * Tests for create.js + */ +import chai from 'chai'; +import request from 'supertest'; +import _ from 'lodash'; +import server from '../../../app'; +import testUtil from '../../../tests/util'; +import models from '../../../models'; + +const should = chai.should(); + +describe('CREATE Form Revision', () => { + const forms = [ + { + key: 'dev', + scope: { + test: 'test1', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + scope: { + test: 'test2', + }, + version: 1, + revision: 2, + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.Form.create(forms[0])) + .then(() => models.Form.create(forms[1])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('Post /projects/metadata/form/{key}/versions/{version}/revision', () => { + const body = { + param: { + scope: { + 'test create': 'test create', + }, + }, + }; + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .post('/v4/projects/metadata/form/dev/versions/1/revisions') + .send(body) + .expect(403, done); + }); + + it('should return 404 if missing key', (done) => { + request(server) + .post('/v4/projects/metadata/form/no-exist-key/versions/1/revisions') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(404, done); + }); + + it('should return 404 if missing version', (done) => { + request(server) + .post('/v4/projects/metadata/form/dev/versions/100/revisions') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(404, done); + }); + + it('should return 422 if missing scope', (done) => { + const invalidBody = { + param: _.assign({}, body.param, { + scope: undefined, + }), + }; + + request(server) + .post('/v4/projects/metadata/form/no-exist-key/versions/1/revisions') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(invalidBody) + .expect('Content-Type', /json/) + .expect(422, done); + }); + + it('should return 201 for admin', (done) => { + request(server) + .post('/v4/projects/metadata/form/dev/versions/1/revisions') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + const resJson = res.body.result.content; + should.exist(resJson.id); + resJson.scope.should.be.eql(body.param.scope); + resJson.key.should.be.eql('dev'); + resJson.revision.should.be.eql(3); + resJson.version.should.be.eql(1); + resJson.createdBy.should.be.eql(40051333); // admin + should.exist(resJson.createdAt); + resJson.updatedBy.should.be.eql(40051333); // admin + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + done(); + }); + }); + + it('should return 403 for member', (done) => { + request(server) + .post('/v4/projects/metadata/form/dev/versions/1/revisions') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send(body) + .expect(403, done); + }); + }); +}); diff --git a/src/routes/form/revision/delete.js b/src/routes/form/revision/delete.js new file mode 100644 index 00000000..1abaf2ec --- /dev/null +++ b/src/routes/form/revision/delete.js @@ -0,0 +1,48 @@ +/** + * API to delete a revsion + */ +import validate from 'express-validation'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import models from '../../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + key: Joi.string().max(45).required(), + version: Joi.number().integer().positive().required(), + revision: Joi.number().integer().positive().required(), + }, +}; + + +module.exports = [ + validate(schema), + permissions('form.delete'), + (req, res, next) => { + models.sequelize.transaction(() => models.Form.findOne( + { + where: { + key: req.params.key, + version: req.params.version, + revision: req.params.revision, + }, + }).then((form) => { + if (!form) { + const apiErr = new Error('Form not found for key' + + ` ${req.params.key} version ${req.params.version} revision ${req.params.revision}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + return form.update({ + deletedBy: req.authUser.userId, + }); + }).then((form) => { + form.destroy(); + }).then(() => { + res.status(204).end(); + }) + .catch(next)); + }, +]; diff --git a/src/routes/form/revision/delete.spec.js b/src/routes/form/revision/delete.spec.js new file mode 100644 index 00000000..439e4fa0 --- /dev/null +++ b/src/routes/form/revision/delete.spec.js @@ -0,0 +1,153 @@ +/** + * Tests for delete.js + */ +import request from 'supertest'; +import chai from 'chai'; +import models from '../../../models'; +import server from '../../../app'; +import testUtil from '../../../tests/util'; + +const expectAfterDelete = (err, next) => { + if (err) throw err; + setTimeout(() => + models.Form.findOne({ + where: { + key: 'dev', + version: 1, + revision: 1, + }, + paranoid: false, + }) + .then((res) => { + if (!res) { + throw new Error('Should found the entity'); + } else { + chai.assert.isNotNull(res.deletedAt); + chai.assert.isNotNull(res.deletedBy); + + request(server) + .get('/v4/projects/metadata/form/dev/versions/1/revisions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, next); + } + }), 500); +}; + + +describe('DELETE form revision', () => { + const forms = [ + { + key: 'dev', + scope: { + test: 'test1', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + scope: { + test: 'test2', + }, + version: 1, + revision: 2, + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.Form.create(forms[0])) + .then(() => models.Form.create(forms[1])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + + describe('DELETE /projects/metadata/form/{key}/versions/{version}/revisions/{revision}', () => { + it('should return 403 if user is not authenticated', (done) => { + request(server) + .delete('/v4/projects/metadata/form/dev/versions/1/revisions/1') + .expect(403, done); + }); + + it('should return 403 for member', (done) => { + request(server) + .delete('/v4/projects/metadata/form/dev/versions/1/revisions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(403, done); + }); + + it('should return 403 for copilot', (done) => { + request(server) + .delete('/v4/projects/metadata/form/dev/versions/1/revisions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(403, done); + }); + + it('should return 403 for manager', (done) => { + request(server) + .delete('/v4/projects/metadata/form/dev/versions/1/revisions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(403, done); + }); + + it('should return 404 for non-existed key', (done) => { + request(server) + .delete('/v4/projects/metadata/form/no-existed-key/versions/1/revisions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + + it('should return 404 for non-existed version', (done) => { + request(server) + .delete('/v4/projects/metadata/form/dev/versions/100/revisions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + + + it('should return 404 for non-existed revision', (done) => { + request(server) + .delete('/v4/projects/metadata/form/dev/versions/1/revisions/100') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + + it('should return 204, for admin', (done) => { + request(server) + .delete('/v4/projects/metadata/form/dev/versions/1/revisions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(204) + .end(err => expectAfterDelete(err, done)); + }); + + it('should return 204, for connect admin', (done) => { + request(server) + .delete('/v4/projects/metadata/form/dev/versions/1/revisions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(204) + .end(err => expectAfterDelete(err, done)); + }); + }); +}); diff --git a/src/routes/form/revision/get.js b/src/routes/form/revision/get.js new file mode 100644 index 00000000..460331cf --- /dev/null +++ b/src/routes/form/revision/get.js @@ -0,0 +1,44 @@ +/** + * API to get a form for particular revision + */ +import validate from 'express-validation'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../../util'; +import models from '../../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + key: Joi.string().max(45).required(), + version: Joi.number().integer().positive().required(), + revision: Joi.number().integer().positive().required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('form.view'), + (req, res, next) => models.Form.findOne({ + where: { + key: req.params.key, + version: req.params.version, + revision: req.params.revision, + }, + attributes: { exclude: ['deletedAt', 'deletedBy'] }, + }) + .then((form) => { + // Not found + if (!form) { + const apiErr = new Error('Form not found for key' + + ` ${req.params.key} version ${req.params.version} revision ${req.params.revision}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + + res.json(util.wrapResponse(req.id, form)); + return Promise.resolve(); + }) + .catch(next), +]; diff --git a/src/routes/form/revision/get.spec.js b/src/routes/form/revision/get.spec.js new file mode 100644 index 00000000..b95a4c97 --- /dev/null +++ b/src/routes/form/revision/get.spec.js @@ -0,0 +1,113 @@ +/** + * Tests for get.js + */ +import chai from 'chai'; +import request from 'supertest'; + +import models from '../../../models'; +import server from '../../../app'; +import testUtil from '../../../tests/util'; + +const should = chai.should(); + +describe('GET a particular revision of specific version Form', () => { + const forms = [ + { + key: 'dev', + scope: { + test: 'test1', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + scope: { + test: 'test2', + }, + version: 1, + revision: 2, + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.Form.create(forms[0])) + .then(() => models.Form.create(forms[1])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('GET /projects/metadata/form/dev/versions/{version}/revisions/{revision}', () => { + it('should return 200 for admin', (done) => { + request(server) + .get('/v4/projects/metadata/form/dev/versions/1/revisions/2') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end((err, res) => { + const form = forms[1]; + const resJson = res.body.result.content; + + resJson.key.should.be.eql(form.key); + resJson.scope.should.be.eql(form.scope); + resJson.version.should.be.eql(form.version); + resJson.revision.should.be.eql(form.revision); + should.exist(resJson.createdAt); + resJson.updatedBy.should.be.eql(form.updatedBy); + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + done(); + }); + }); + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .get('/v4/projects/metadata/form/dev/versions/1/revisions/2') + .expect(403, done); + }); + + it('should return 200 for connect admin', (done) => { + request(server) + .get('/v4/projects/metadata/form/dev/versions/1/revisions/2') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for connect manager', (done) => { + request(server) + .get('/v4/projects/metadata/form/dev/versions/1/revisions/2') + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for member', (done) => { + request(server) + .get('/v4/projects/metadata/form/dev/versions/1/revisions/2') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(200, done); + }); + + it('should return 200 for copilot', (done) => { + request(server) + .get('/v4/projects/metadata/form/dev/versions/1/revisions/2') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(200, done); + }); + }); +}); diff --git a/src/routes/form/revision/list.js b/src/routes/form/revision/list.js new file mode 100644 index 00000000..d4acc1d0 --- /dev/null +++ b/src/routes/form/revision/list.js @@ -0,0 +1,41 @@ +/** + * API to get revison list + */ +import validate from 'express-validation'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../../util'; +import models from '../../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + key: Joi.string().max(45).required(), + version: Joi.number().integer().positive().required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('form.view'), + (req, res, next) => models.Form.findAll({ + where: { + key: req.params.key, + version: req.params.version, + }, + attributes: { exclude: ['deletedAt', 'deletedBy'] }, + }) + .then((forms) => { + // Not found + if ((!forms) || (forms.length === 0)) { + const apiErr = new Error(`Form not found for key ${req.params.key} version ${req.params.version}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + + res.json(util.wrapResponse(req.id, forms)); + return Promise.resolve(); + }) + .catch(next), +]; diff --git a/src/routes/form/revision/list.spec.js b/src/routes/form/revision/list.spec.js new file mode 100644 index 00000000..715fc38a --- /dev/null +++ b/src/routes/form/revision/list.spec.js @@ -0,0 +1,115 @@ +/* eslint-disable quote-props */ +/** + * Tests for list.js + */ +import chai from 'chai'; +import request from 'supertest'; + +import models from '../../../models'; +import server from '../../../app'; +import testUtil from '../../../tests/util'; + +const should = chai.should(); + +describe('LIST form revisions', () => { + const forms = [ + { + key: 'dev', + scope: { + 'test': 'test1', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + scope: { + test: 'test2', + }, + version: 1, + revision: 2, + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.Form.create(forms[0])) + .then(() => models.Form.create(forms[1])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('GET /projects/metadata/form/dev/versions/{version}/revisions', () => { + it('should return 200 for admin', (done) => { + request(server) + .get('/v4/projects/metadata/form/dev/versions/1/revisions') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end((err, res) => { + const form = forms[0]; + const resJson = res.body.result.content; + resJson.should.have.length(2); + + resJson[0].key.should.be.eql(form.key); + resJson[0].scope.should.be.eql(form.scope); + resJson[0].version.should.be.eql(form.version); + resJson[0].revision.should.be.eql(form.revision); + should.exist(resJson[0].createdAt); + resJson[0].updatedBy.should.be.eql(form.updatedBy); + should.exist(resJson[0].updatedAt); + should.not.exist(resJson[0].deletedBy); + should.not.exist(resJson[0].deletedAt); + done(); + }); + }); + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .get('/v4/projects/metadata/form/dev/versions/1/revisions') + .expect(403, done); + }); + + it('should return 200 for connect admin', (done) => { + request(server) + .get('/v4/projects/metadata/form/dev/versions/1/revisions') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for connect manager', (done) => { + request(server) + .get('/v4/projects/metadata/form/dev/versions/1/revisions') + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for member', (done) => { + request(server) + .get('/v4/projects/metadata/form/dev/versions/1/revisions') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(200, done); + }); + + it('should return 200 for copilot', (done) => { + request(server) + .get('/v4/projects/metadata/form/dev/versions/1/revisions') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(200, done); + }); + }); +}); diff --git a/src/routes/form/version/create.js b/src/routes/form/version/create.js new file mode 100644 index 00000000..c53f1f7a --- /dev/null +++ b/src/routes/form/version/create.js @@ -0,0 +1,64 @@ +/** + * API to add a new version of form + */ +import validate from 'express-validation'; +import _ from 'lodash'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../../util'; +import models from '../../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + key: Joi.string().max(45).required(), + }, + body: { + param: Joi.object().keys({ + scope: Joi.object().required(), + + createdAt: Joi.any().strip(), + updatedAt: Joi.any().strip(), + deletedAt: Joi.any().strip(), + createdBy: Joi.any().strip(), + updatedBy: Joi.any().strip(), + deletedBy: Joi.any().strip(), + }).required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('form.create'), + (req, res, next) => { + models.sequelize.transaction(() => models.Form.findAll({ + where: { + key: req.params.key, + }, + order: [['version', 'DESC']], + }).then((forms) => { + let latestVersion = 1; + if (forms.length !== 0) { + const latestVersionForm = forms.reduce((prev, current) => + ((prev.version < current.version) ? current : prev)); + latestVersion = latestVersionForm.version + 1; + } + + const entity = _.assign(req.body.param, { + version: latestVersion, + revision: 1, + createdBy: req.authUser.userId, + updatedBy: req.authUser.userId, + key: req.params.key, + scope: req.body.param.scope, + }); + return models.Form.create(entity); + }).then((createdEntity) => { + // Omit deletedAt, deletedBy + res.status(201).json(util.wrapResponse( + req.id, _.omit(createdEntity.toJSON(), 'deletedAt', 'deletedBy'), 1, 201)); + }) + .catch(next)); + }, +]; diff --git a/src/routes/form/version/create.spec.js b/src/routes/form/version/create.spec.js new file mode 100644 index 00000000..d8680972 --- /dev/null +++ b/src/routes/form/version/create.spec.js @@ -0,0 +1,114 @@ +/* eslint-disable no-unused-expressions */ +/** + * Tests for create.js + */ +import chai from 'chai'; +import request from 'supertest'; +import _ from 'lodash'; +import server from '../../../app'; +import testUtil from '../../../tests/util'; +import models from '../../../models'; + +const should = chai.should(); + +describe('CREATE Form version', () => { + const forms = [ + { + key: 'dev', + scope: { + test: 'test1', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + scope: { + test: 'test2', + }, + version: 1, + revision: 2, + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.Form.create(forms[0])) + .then(() => models.Form.create(forms[1])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('Post /projects/metadata/form/{key}/versions/', () => { + const body = { + param: { + scope: { + 'test create': 'test create', + }, + }, + }; + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .post('/v4/projects/metadata/form/dev/versions') + .send(body) + .expect(403, done); + }); + + it('should return 422 if missing scope', (done) => { + const invalidBody = { + param: _.assign({}, body.param, { + scope: undefined, + }), + }; + + request(server) + .post('/v4/projects/metadata/form/dev/versions/') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(invalidBody) + .expect('Content-Type', /json/) + .expect(422, done); + }); + + it('should return 201 for admin', (done) => { + request(server) + .post('/v4/projects/metadata/form/dev/versions/') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + const resJson = res.body.result.content; + should.exist(resJson.id); + resJson.scope.should.be.eql(body.param.scope); + resJson.key.should.be.eql('dev'); + resJson.revision.should.be.eql(1); + resJson.version.should.be.eql(2); + resJson.createdBy.should.be.eql(40051333); // admin + should.exist(resJson.createdAt); + resJson.updatedBy.should.be.eql(40051333); // admin + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + done(); + }); + }); + + it('should return 403 for member', (done) => { + request(server) + .post('/v4/projects/metadata/form/dev/versions/') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send(body) + .expect(403, done); + }); + }); +}); diff --git a/src/routes/form/version/delete.js b/src/routes/form/version/delete.js new file mode 100644 index 00000000..cd195934 --- /dev/null +++ b/src/routes/form/version/delete.js @@ -0,0 +1,54 @@ +/** + * API to add a project type + */ +import validate from 'express-validation'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import models from '../../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + version: Joi.number().integer().positive().required(), + key: Joi.string().max(45).required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('form.create'), + (req, res, next) => { + models.sequelize.transaction(() => models.Form.findAll( + { + where: { + key: req.params.key, + version: req.params.version, + }, + }).then((allRevision) => { + if (allRevision.length === 0) { + const apiErr = new Error(`Form not found for key ${req.params.key} version ${req.params.version}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + return models.Form.update( + { + deletedBy: req.authUser.userId, + }, { + where: { + key: req.params.key, + version: req.params.version, + }, + }); + }) + .then(() => models.Form.destroy({ + where: { + key: req.params.key, + version: req.params.version, + }, + })).then(() => { + res.status(204).end(); + }) + .catch(next)); + }, +]; diff --git a/src/routes/form/version/delete.spec.js b/src/routes/form/version/delete.spec.js new file mode 100644 index 00000000..92bfaaf8 --- /dev/null +++ b/src/routes/form/version/delete.spec.js @@ -0,0 +1,139 @@ +/** + * Tests for delete.js + */ +import request from 'supertest'; +import chai from 'chai'; +import models from '../../../models'; +import server from '../../../app'; +import testUtil from '../../../tests/util'; + +const expectAfterDelete = (err, next) => { + if (err) throw err; + setTimeout(() => + models.Form.findAll({ + where: { + key: 'dev', + version: 1, + }, + paranoid: false, + }) + .then((forms) => { + if (forms.length === 0) { + throw new Error('Should found the entity'); + } else { + chai.assert.isNotNull(forms[0].deletedAt); + chai.assert.isNotNull(forms[0].deletedBy); + + chai.assert.isNotNull(forms[1].deletedAt); + chai.assert.isNotNull(forms[1].deletedBy); + next(); + } + }), 500); +}; + + +describe('DELETE form version', () => { + const forms = [ + { + key: 'dev', + scope: { + test: 'test1', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + scope: { + test: 'test2', + }, + version: 1, + revision: 2, + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.Form.create(forms[0])) + .then(() => models.Form.create(forms[1])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + + describe('DELETE /projects/metadata/form/{key}/versions/{version}', () => { + it('should return 403 if user is not authenticated', (done) => { + request(server) + .delete('/v4/projects/metadata/form/dev/versions/1') + .expect(403, done); + }); + + it('should return 403 for member', (done) => { + request(server) + .delete('/v4/projects/metadata/form/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(403, done); + }); + + it('should return 403 for copilot', (done) => { + request(server) + .delete('/v4/projects/metadata/form/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(403, done); + }); + + it('should return 403 for manager', (done) => { + request(server) + .delete('/v4/projects/metadata/form/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(403, done); + }); + + it('should return 404 for non-existed key', (done) => { + request(server) + .delete('/v4/projects/metadata/form/dev111/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + + it('should return 404 for non-existed version', (done) => { + request(server) + .delete('/v4/projects/metadata/form/dev/versions/111') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + + it('should return 204, for admin', (done) => { + request(server) + .delete('/v4/projects/metadata/form/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(204) + .end(err => expectAfterDelete(err, done)); + }); + + it('should return 204, for connect admin', (done) => { + request(server) + .delete('/v4/projects/metadata/form/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(204) + .end(err => expectAfterDelete(err, done)); + }); + }); +}); diff --git a/src/routes/form/version/get.js b/src/routes/form/version/get.js new file mode 100644 index 00000000..a4f87081 --- /dev/null +++ b/src/routes/form/version/get.js @@ -0,0 +1,34 @@ +/** + * API to get a latest version for key + * + */ +import validate from 'express-validation'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../../util'; +import models from '../../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + key: Joi.string().max(45).required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('form.view'), + (req, res, next) => + models.Form.latestRevisionofLatestVersion(req.params.key) + .then((form) => { + if (form == null) { + const apiErr = new Error(`Form not found for key ${req.params.key} version ${req.params.version}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + res.json(util.wrapResponse(req.id, form)); + return Promise.resolve(); + }) + .catch(next), +]; diff --git a/src/routes/form/version/get.spec.js b/src/routes/form/version/get.spec.js new file mode 100644 index 00000000..0be71262 --- /dev/null +++ b/src/routes/form/version/get.spec.js @@ -0,0 +1,133 @@ +/** + * Tests for get.js + */ +import chai from 'chai'; +import request from 'supertest'; + +import models from '../../../models'; +import server from '../../../app'; +import testUtil from '../../../tests/util'; + +const should = chai.should(); + +describe('GET a latest version of specific key of Form', () => { + const forms = [ + { + key: 'dev', + scope: { + test: 'test1', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + scope: { + test: 'test2', + }, + version: 2, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + scope: { + test: 'test2', + }, + version: 2, + revision: 2, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + scope: { + test: 'test3', + }, + version: 1, + revision: 2, + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.Form.create(forms[0])) + .then(() => models.Form.create(forms[1])) + .then(() => models.Form.create(forms[2])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('GET /projects/metadata/form/dev/versions/{version}', () => { + it('should return 200 and correct version for admin', (done) => { + request(server) + .get('/v4/projects/metadata/form/dev') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end((err, res) => { + const form = forms[2]; + const resJson = res.body.result.content; + resJson.key.should.be.eql(form.key); + resJson.scope.should.be.eql(form.scope); + resJson.version.should.be.eql(form.version); + resJson.revision.should.be.eql(form.revision); + should.exist(resJson.createdAt); + resJson.updatedBy.should.be.eql(form.updatedBy); + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + done(); + }); + }); + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .get('/v4/projects/metadata/form/dev') + .expect(403, done); + }); + + it('should return 200 for connect admin', (done) => { + request(server) + .get('/v4/projects/metadata/form/dev') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for connect manager', (done) => { + request(server) + .get('/v4/projects/metadata/form/dev') + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for member', (done) => { + request(server) + .get('/v4/projects/metadata/form/dev') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(200, done); + }); + + it('should return 200 for copilot', (done) => { + request(server) + .get('/v4/projects/metadata/form/dev') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(200, done); + }); + }); +}); diff --git a/src/routes/form/version/getVersion.js b/src/routes/form/version/getVersion.js new file mode 100644 index 00000000..74480b60 --- /dev/null +++ b/src/routes/form/version/getVersion.js @@ -0,0 +1,42 @@ +/** + * API to get a form for particular version + */ +import validate from 'express-validation'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../../util'; +import models from '../../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + key: Joi.string().max(45).required(), + version: Joi.number().integer().positive().required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('form.view'), + (req, res, next) => models.Form.findOne({ + where: { + key: req.params.key, + version: req.params.version, + }, + order: [['revision', 'DESC']], + limit: 1, + attributes: { exclude: ['deletedAt', 'deletedBy'] }, + }) + .then((form) => { + // Not found + if (!form) { + const apiErr = new Error(`Form not found for key ${req.params.key} version ${req.params.version}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + res.json(util.wrapResponse(req.id, form)); + return Promise.resolve(); + }) + .catch(next), +]; diff --git a/src/routes/form/version/getVersion.spec.js b/src/routes/form/version/getVersion.spec.js new file mode 100644 index 00000000..2415e7d4 --- /dev/null +++ b/src/routes/form/version/getVersion.spec.js @@ -0,0 +1,124 @@ +/** + * Tests for getVersion.js + */ +import chai from 'chai'; +import request from 'supertest'; + +import models from '../../../models'; +import server from '../../../app'; +import testUtil from '../../../tests/util'; + +const should = chai.should(); + +describe('GET a particular version of specific key of Form', () => { + const forms = [ + { + key: 'dev', + scope: { + test: 'test1', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + scope: { + test: 'test2', + }, + version: 2, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + scope: { + test: 'test3', + }, + version: 2, + revision: 2, + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.Form.create(forms[0])) + .then(() => models.Form.create(forms[1])) + .then(() => models.Form.create(forms[2])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('GET /projects/metadata/form/dev/versions/{version}', () => { + it('should return 200 for admin', (done) => { + request(server) + .get('/v4/projects/metadata/form/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end((err, res) => { + const form = forms[0]; + const resJson = res.body.result.content; + + resJson.key.should.be.eql(form.key); + resJson.scope.should.be.eql(form.scope); + resJson.version.should.be.eql(form.version); + resJson.revision.should.be.eql(form.revision); + should.exist(resJson.createdAt); + resJson.updatedBy.should.be.eql(form.updatedBy); + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + done(); + }); + }); + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .get('/v4/projects/metadata/form/dev/versions/1') + .expect(403, done); + }); + + it('should return 200 for connect admin', (done) => { + request(server) + .get('/v4/projects/metadata/form/dev') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for connect manager', (done) => { + request(server) + .get('/v4/projects/metadata/form/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for member', (done) => { + request(server) + .get('/v4/projects/metadata/form/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(200, done); + }); + + it('should return 200 for copilot', (done) => { + request(server) + .get('/v4/projects/metadata/form/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(200, done); + }); + }); +}); diff --git a/src/routes/form/version/list.js b/src/routes/form/version/list.js new file mode 100644 index 00000000..1186c55c --- /dev/null +++ b/src/routes/form/version/list.js @@ -0,0 +1,47 @@ +/** + * API to get a form list + */ +import validate from 'express-validation'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../../util'; +import models from '../../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + key: Joi.string().max(45).required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('form.view'), + (req, res, next) => models.Form.findAll({ + where: { + key: req.params.key, + }, + attributes: { exclude: ['deletedAt', 'deletedBy'] }, + }) + .then((forms) => { + // Not found + if ((!forms) || (forms.length === 0)) { + const apiErr = new Error(`Form not found for key ${req.params.key}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + + const latestForms = {}; + forms.forEach((element) => { + const isNewerRevision = (latestForms[element.version] != null) && + (latestForms[element.version].revision < element.revision); + if ((latestForms[element.version] == null) || isNewerRevision) { + latestForms[element.version] = element; + } + }); + res.json(util.wrapResponse(req.id, Object.values(latestForms))); + return Promise.resolve(); + }) + .catch(next), +]; diff --git a/src/routes/form/version/list.spec.js b/src/routes/form/version/list.spec.js new file mode 100644 index 00000000..1d785a5e --- /dev/null +++ b/src/routes/form/version/list.spec.js @@ -0,0 +1,115 @@ +/* eslint-disable quote-props */ +/** + * Tests for list.js + */ +import chai from 'chai'; +import request from 'supertest'; + +import models from '../../../models'; +import server from '../../../app'; +import testUtil from '../../../tests/util'; + +const should = chai.should(); + +describe('LIST form versions', () => { + const forms = [ + { + key: 'dev', + scope: { + 'test': 'test1', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + scope: { + test: 'test2', + }, + version: 2, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.Form.create(forms[0])) + .then(() => models.Form.create(forms[1])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('GET /projects/metadata/form/dev/versions/{version}', () => { + it('should return 200 for admin', (done) => { + request(server) + .get('/v4/projects/metadata/form/dev/versions') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end((err, res) => { + const form = forms[0]; + const resJson = res.body.result.content; + resJson.should.have.length(2); + + resJson[0].key.should.be.eql(form.key); + resJson[0].scope.should.be.eql(form.scope); + resJson[0].version.should.be.eql(form.version); + resJson[0].revision.should.be.eql(form.revision); + should.exist(resJson[0].createdAt); + resJson[0].updatedBy.should.be.eql(form.updatedBy); + should.exist(resJson[0].updatedAt); + should.not.exist(resJson[0].deletedBy); + should.not.exist(resJson[0].deletedAt); + done(); + }); + }); + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .get('/v4/projects/metadata/form/dev/versions') + .expect(403, done); + }); + + it('should return 200 for connect admin', (done) => { + request(server) + .get('/v4/projects/metadata/form/dev/versions') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for connect manager', (done) => { + request(server) + .get('/v4/projects/metadata/form/dev/versions') + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for member', (done) => { + request(server) + .get('/v4/projects/metadata/form/dev/versions') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(200, done); + }); + + it('should return 200 for copilot', (done) => { + request(server) + .get('/v4/projects/metadata/form/dev/versions') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(200, done); + }); + }); +}); diff --git a/src/routes/form/version/update.js b/src/routes/form/version/update.js new file mode 100644 index 00000000..649e2d28 --- /dev/null +++ b/src/routes/form/version/update.js @@ -0,0 +1,74 @@ +/* eslint-disable no-trailing-spaces */ +/** + * API to add a project type + */ +import config from 'config'; +import validate from 'express-validation'; +import _ from 'lodash'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../../util'; +import models from '../../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + version: Joi.number().integer().positive().required(), + key: Joi.string().max(45).required(), + }, + body: { + param: Joi.object().keys({ + scope: Joi.object().required(), + + createdAt: Joi.any().strip(), + updatedAt: Joi.any().strip(), + deletedAt: Joi.any().strip(), + createdBy: Joi.any().strip(), + updatedBy: Joi.any().strip(), + deletedBy: Joi.any().strip(), + }).required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('form.create'), + (req, res, next) => { + models.sequelize.transaction(() => models.Form.findAll({ + where: { + key: req.params.key, + version: req.params.version, + }, + order: [['revision', 'DESC']], + }).then((forms) => { + if (forms.length >= config.get('MAX_REVISION_NUMBER')) { + return models.Form.deleteOldestRevision(req.authUser.userId, req.params.key, req.params.version) + .then(() => Promise.resolve(forms[0])); + } else if (forms.length === 0) { + const apiErr = new Error(`Form not found for key ${req.params.key} version ${req.params.version}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + return Promise.resolve(forms[0]); + }) + .then((form) => { + const revisison = form.revision + 1; + const entity = { + version: req.params.version, + revision: revisison, + createdBy: req.authUser.userId, + updatedBy: req.authUser.userId, + key: req.params.key, + scope: req.body.param.scope, + }; + return models.Form.create(entity); + }) + .then((createdEntity) => { + // Omit deletedAt, deletedBy + res.status(201).json(util.wrapResponse( + req.id, _.omit(createdEntity.toJSON(), 'deletedAt', 'deletedBy'), 1, 201)); + }) + .catch(next)); + }, +]; diff --git a/src/routes/form/version/update.spec.js b/src/routes/form/version/update.spec.js new file mode 100644 index 00000000..a8249aa2 --- /dev/null +++ b/src/routes/form/version/update.spec.js @@ -0,0 +1,113 @@ +/* eslint-disable no-unused-expressions */ +/** + * Tests for create.js + */ +import chai from 'chai'; +import request from 'supertest'; +import _ from 'lodash'; +import server from '../../../app'; +import testUtil from '../../../tests/util'; +import models from '../../../models'; + +const should = chai.should(); + +describe('UPDATE Form version', () => { + const forms = [ + { + key: 'dev', + scope: { + test: 'test1', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + scope: { + test: 'test2', + }, + version: 1, + revision: 2, + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.Form.create(forms[0])) + .then(() => models.Form.create(forms[1])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('Post /projects/metadata/form/{key}/versions/{version}', () => { + const body = { + param: { + scope: { + 'test create': 'test create', + }, + }, + }; + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .patch('/v4/projects/metadata/form/dev/versions/1') + .send(body) + .expect(403, done); + }); + + it('should return 422 if missing scope', (done) => { + const invalidBody = { + param: _.assign({}, body.param, { + scope: undefined, + }), + }; + request(server) + .patch('/v4/projects/metadata/form/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(invalidBody) + .expect('Content-Type', /json/) + .expect(422, done); + }); + + it('should return 201 for admin', (done) => { + request(server) + .patch('/v4/projects/metadata/form/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + const resJson = res.body.result.content; + should.exist(resJson.id); + resJson.scope.should.be.eql(body.param.scope); + resJson.key.should.be.eql('dev'); + resJson.revision.should.be.eql(3); + resJson.version.should.be.eql(1); + resJson.createdBy.should.be.eql(40051333); // admin + should.exist(resJson.createdAt); + resJson.updatedBy.should.be.eql(40051333); // admin + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + done(); + }); + }); + + it('should return 403 for member', (done) => { + request(server) + .patch('/v4/projects/metadata/form/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send(body) + .expect(403, done); + }); + }); +}); diff --git a/src/routes/index.js b/src/routes/index.js index 0312b624..045384bf 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -49,6 +49,9 @@ router.route('/v4/projects/metadata/projectTypes') router.route('/v4/projects/metadata/projectTypes/:key') .get(require('./projectTypes/get')); +router.route('/v4/projects/metadata/projectTemplates/:templateId(\\d+)/upgrade') +.post(require('./projectTemplates/upgrade')); + router.route('/v4/projects/metadata/orgConfig') .get(require('./orgConfig/list')); @@ -195,6 +198,71 @@ router.route('/v4/projects/metadata/orgConfig/:id(\\d+)') .patch(require('./orgConfig/update')) .delete(require('./orgConfig/delete')); +// form + +router.route('/v4/projects/metadata/form/:key/versions/:version(\\d+)/revisions/:revision(\\d+)') + .get(require('./form/revision/get')) + .delete(require('./form/revision/delete')); + +router.route('/v4/projects/metadata/form/:key/versions/:version(\\d+)/revisions') + .get(require('./form/revision/list')) + .post(require('./form/revision/create')); + +router.route('/v4/projects/metadata/form/:key') + .get(require('./form/version/get')); + +router.route('/v4/projects/metadata/form/:key/versions') + .get(require('./form/version/list')) + .post(require('./form/version/create')); + +router.route('/v4/projects/metadata/form/:key/versions/:version(\\d+)') + .get(require('./form/version/getVersion')) + .patch(require('./form/version/update')) + .delete(require('./form/version/delete')); + +// price config + +router.route('/v4/projects/metadata/priceConfig/:key/versions/:version(\\d+)/revisions/:revision(\\d+)') + .get(require('./priceConfig/revision/get')) + .delete(require('./priceConfig/revision/delete')); + +router.route('/v4/projects/metadata/priceConfig/:key/versions/:version(\\d+)/revisions') + .get(require('./priceConfig/revision/list')) + .post(require('./priceConfig/revision/create')); + +router.route('/v4/projects/metadata/priceConfig/:key') +.get(require('./priceConfig/version/get')); + +router.route('/v4/projects/metadata/priceConfig/:key/versions') +.get(require('./priceConfig/version/list')) +.post(require('./priceConfig/version/create')); + +router.route('/v4/projects/metadata/priceConfig/:key/versions/:version(\\d+)') +.get(require('./priceConfig/version/getVersion')) +.patch(require('./priceConfig/version/update')) +.delete(require('./priceConfig/version/delete')); + +// plan config +router.route('/v4/projects/metadata/planConfig/:key/versions/:version(\\d+)/revisions/:revision(\\d+)') + .get(require('./planConfig/revision/get')) + .delete(require('./planConfig/revision/delete')); + +router.route('/v4/projects/metadata/planConfig/:key/versions/:version(\\d+)/revisions') + .get(require('./planConfig/revision/list')) + .post(require('./planConfig/revision/create')); + +router.route('/v4/projects/metadata/planConfig/:key') + .get(require('./planConfig/version/get')); + +router.route('/v4/projects/metadata/planConfig/:key/versions') + .get(require('./planConfig/version/list')) + .post(require('./planConfig/version/create')); + +router.route('/v4/projects/metadata/planConfig/:key/versions/:version(\\d+)') + .get(require('./planConfig/version/getVersion')) + .patch(require('./planConfig/version/update')) + .delete(require('./planConfig/version/delete')); + // register error handler router.use((err, req, res, next) => { // eslint-disable-line no-unused-vars // DO NOT REMOVE next arg.. even though eslint diff --git a/src/routes/metadata/list.js b/src/routes/metadata/list.js index 5a173913..7a4b0388 100644 --- a/src/routes/metadata/list.js +++ b/src/routes/metadata/list.js @@ -1,13 +1,61 @@ -/** + /** * API to list all metadata */ import { middleware as tcMiddleware } from 'tc-core-library-js'; +import Joi from 'joi'; +import validate from 'express-validation'; import util from '../../util'; import models from '../../models'; const permissions = tcMiddleware.permissions; +const schema = { + query: { + includeAllReferred: Joi.boolean().optional(), + }, +}; + +/** + * Found all form, planConfig, priceConfig latest version records + * + * @return {object} used model key/version map for project template + */ +function getUsedModel() { + const modelUsed = { + form: { }, + planConfig: { }, + priceConfig: { }, + }; + const query = { + attributes: { exclude: ['deletedAt', 'deletedBy'] }, + raw: true, + }; + return models.ProjectTemplate.findAll(query) + .then((templates) => { + templates.forEach((template) => { + const { form, planConfig, priceConfig } = template; + if ((form) && (form.key) && (form.version)) { + modelUsed.form[form.key] = modelUsed.form[form.key] ? modelUsed.form[form.key] : {}; + modelUsed.form[form.key][form.version] = true; + } + if ((priceConfig) && (priceConfig.key) && (priceConfig.version)) { + modelUsed.priceConfig[priceConfig.key] = modelUsed.priceConfig[priceConfig.key] ? + modelUsed.priceConfig[priceConfig.key] : {}; + modelUsed.priceConfig[priceConfig.key][priceConfig.version] = true; + } + if ((planConfig) && (planConfig.key) && (planConfig.version)) { + modelUsed.planConfig[planConfig.key] = modelUsed.planConfig[planConfig.key] ? + modelUsed.planConfig[planConfig.key] : {}; + modelUsed.planConfig[planConfig.key][planConfig.version] = true; + } + }); + return Promise.resolve(modelUsed); + }); +} + + module.exports = [ + validate(schema), permissions('metadata.list'), (req, res, next) => { const query = { @@ -15,22 +63,69 @@ module.exports = [ raw: true, }; + // when user query with includeAllReferred, return result with all used version of + // Form, PriceConfig, PlanConfig + if (req.query.includeAllReferred) { + let usedModelMap; + let latestVersion; + return getUsedModel() + .then((modelUsed) => { + // found all latest version & used in project template version record + // for Form, PriceConfig, PlanConfig + usedModelMap = modelUsed; + return Promise.all([ + models.Form.latestVersionIncludeUsed(usedModelMap.form), + models.PriceConfig.latestVersionIncludeUsed(usedModelMap.priceConfig), + models.PlanConfig.latestVersionIncludeUsed(usedModelMap.planConfig), + ]); + }).then((latestVersionModels) => { + latestVersion = latestVersionModels; + return Promise.all([ + models.ProjectTemplate.findAll(query), + models.ProductTemplate.findAll(query), + models.MilestoneTemplate.findAll(query), + models.ProjectType.findAll(query), + models.ProductCategory.findAll(query), + Promise.resolve(latestVersion[0]), + Promise.resolve(latestVersion[1]), + Promise.resolve(latestVersion[2]), + ]); + }).then((queryAllResult) => { + res.json(util.wrapResponse(req.id, { + projectTemplates: queryAllResult[0], + productTemplates: queryAllResult[1], + milestoneTemplates: queryAllResult[2], + projectTypes: queryAllResult[3], + productCategories: queryAllResult[4], + forms: queryAllResult[5], + priceConfigs: queryAllResult[6], + planConfigs: queryAllResult[7], + })); + }) + .catch(next); + } return Promise.all([ models.ProjectTemplate.findAll(query), models.ProductTemplate.findAll(query), models.MilestoneTemplate.findAll(query), models.ProjectType.findAll(query), models.ProductCategory.findAll(query), + models.Form.latestVersion(), + models.PriceConfig.latestVersion(), + models.PlanConfig.latestVersion(), ]) - .then((results) => { - res.json(util.wrapResponse(req.id, { - projectTemplates: results[0], - productTemplates: results[1], - milestoneTemplates: results[2], - projectTypes: results[3], - productCategories: results[4], - })); - }) - .catch(next); + .then((results) => { + res.json(util.wrapResponse(req.id, { + projectTemplates: results[0], + productTemplates: results[1], + milestoneTemplates: results[2], + projectTypes: results[3], + productCategories: results[4], + forms: results[5], + priceConfigs: results[6], + planConfigs: results[7], + })); + }) + .catch(next); }, ]; diff --git a/src/routes/metadata/list.spec.js b/src/routes/metadata/list.spec.js index 48f82532..7880114b 100644 --- a/src/routes/metadata/list.spec.js +++ b/src/routes/metadata/list.spec.js @@ -21,6 +21,9 @@ const projectTemplates = [ aliases: ['key-1', 'key_1'], scope: {}, phases: {}, + form: { key: 'key1', version: 1 }, + planConfig: { key: 'key1', version: 1 }, + priceConfig: { key: 'key1', version: 1 }, createdBy: 1, updatedBy: 1, }, @@ -83,6 +86,72 @@ const productCategories = [ updatedBy: 1, }, ]; +const forms = [ + { + key: 'key1', + scope: { + hello: 'world', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'key1', + scope: { + hello: 'world', + }, + version: 2, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, +]; +const priceConfigs = [ + { + key: 'key1', + config: { + hello: 'world', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'key1', + config: { + hello: 'world', + }, + version: 2, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, +]; +const planConfigs = [ + { + key: 'key1', + phases: { + hello: 'world', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'key1', + phases: { + hello: 'world', + }, + version: 2, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, +]; describe('GET all metadata', () => { beforeEach(() => testUtil.clearDb() @@ -90,7 +159,10 @@ describe('GET all metadata', () => { .then(() => models.ProductTemplate.bulkCreate(productTemplates)) .then(() => models.MilestoneTemplate.bulkCreate(milestoneTemplates)) .then(() => models.ProjectType.bulkCreate(projectTypes)) - .then(() => models.ProductCategory.bulkCreate(productCategories)), + .then(() => models.ProductCategory.bulkCreate(productCategories)) + .then(() => models.Form.bulkCreate(forms)) + .then(() => models.PriceConfig.bulkCreate(priceConfigs)) + .then(() => models.PlanConfig.bulkCreate(planConfigs)), ); after(testUtil.clearDb); @@ -126,7 +198,36 @@ describe('GET all metadata', () => { resJson.milestoneTemplates.should.have.length(1); resJson.projectTypes.should.have.length(1); resJson.productCategories.should.have.length(1); + resJson.forms.should.have.length(1); + resJson.planConfigs.should.have.length(1); + resJson.priceConfigs.should.have.length(1); + resJson.forms[0].version.should.be.eql(2); + resJson.planConfigs[0].version.should.be.eql(2); + resJson.priceConfigs[0].version.should.be.eql(2); + + done(); + }); + }); + + it('should return all used model when request with includeAllReferred query', (done) => { + request(server) + .get('/v4/projects/metadata?includeAllReferred=true') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end((err, res) => { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.projectTemplates.should.have.length(1); + resJson.productTemplates.should.have.length(1); + resJson.milestoneTemplates.should.have.length(1); + resJson.projectTypes.should.have.length(1); + resJson.productCategories.should.have.length(1); + resJson.forms.should.have.length(2); + resJson.planConfigs.should.have.length(2); + resJson.priceConfigs.should.have.length(2); done(); }); }); diff --git a/src/routes/planConfig/revision/create.js b/src/routes/planConfig/revision/create.js new file mode 100644 index 00000000..2ad88b48 --- /dev/null +++ b/src/routes/planConfig/revision/create.js @@ -0,0 +1,66 @@ +/** + * API to add a planConfig revision + */ +import validate from 'express-validation'; +import _ from 'lodash'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../../util'; +import models from '../../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + key: Joi.string().max(45).required(), + version: Joi.number().integer().positive().required(), + }, + body: { + param: Joi.object().keys({ + phases: Joi.object().required(), + + createdAt: Joi.any().strip(), + updatedAt: Joi.any().strip(), + deletedAt: Joi.any().strip(), + createdBy: Joi.any().strip(), + updatedBy: Joi.any().strip(), + deletedBy: Joi.any().strip(), + }).required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('planConfig.create'), + (req, res, next) => { + models.sequelize.transaction(() => models.PlanConfig.findOne({ + where: { + key: req.params.key, + version: req.params.version, + }, + order: [['revision', 'DESC']], + }).then((planConfig) => { + if (planConfig) { + const version = planConfig ? planConfig.version : 1; + const revision = planConfig ? planConfig.revision + 1 : 1; + const entity = _.assign(req.body.param, { + version, + revision, + createdBy: req.authUser.userId, + updatedBy: req.authUser.userId, + key: req.params.key, + phases: req.body.param.phases, + }); + return models.PlanConfig.create(entity); + } + const apiErr = new Error(`PlanConfig not exists for key ${req.params.key}`); + apiErr.status = 404; + return Promise.reject(apiErr); + }).then((createdEntity) => { + // Omit deletedAt, deletedBy + res.status(201).json(util.wrapResponse( + req.id, _.omit(createdEntity.toJSON(), 'deletedAt', 'deletedBy'), 1, 201)); + }) + .catch(next)); + }, +]; diff --git a/src/routes/planConfig/revision/create.spec.js b/src/routes/planConfig/revision/create.spec.js new file mode 100644 index 00000000..227b5eee --- /dev/null +++ b/src/routes/planConfig/revision/create.spec.js @@ -0,0 +1,136 @@ +/* eslint-disable no-unused-expressions */ +/** + * Tests for create.js + */ +import chai from 'chai'; +import request from 'supertest'; +import _ from 'lodash'; +import server from '../../../app'; +import testUtil from '../../../tests/util'; +import models from '../../../models'; + +const should = chai.should(); + +describe('CREATE PlanConfig Revision', () => { + const planConfigs = [ + { + key: 'dev', + phases: { + test: 'test1', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + phases: { + test: 'test2', + }, + version: 1, + revision: 2, + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.PlanConfig.create(planConfigs[0])) + .then(() => models.PlanConfig.create(planConfigs[1])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('Post /projects/metadata/planConfig/{key}/versions/{version}/revision', () => { + const body = { + param: { + phases: { + 'test create': 'test create', + }, + }, + }; + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .post('/v4/projects/metadata/planConfig/dev/versions/1/revisions') + .send(body) + .expect(403, done); + }); + + it('should return 404 if missing key', (done) => { + request(server) + .post('/v4/projects/metadata/planConfig/no-exist-key/versions/1/revisions') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(404, done); + }); + + it('should return 404 if missing version', (done) => { + request(server) + .post('/v4/projects/metadata/planConfig/dev/versions/100/revisions') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(404, done); + }); + + it('should return 422 if missing phases', (done) => { + const invalidBody = { + param: _.assign({}, body.param, { + phases: undefined, + }), + }; + + request(server) + .post('/v4/projects/metadata/planConfig/no-exist-key/versions/1/revisions') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(invalidBody) + .expect('Content-Type', /json/) + .expect(422, done); + }); + + it('should return 201 for admin', (done) => { + request(server) + .post('/v4/projects/metadata/planConfig/dev/versions/1/revisions') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + const resJson = res.body.result.content; + should.exist(resJson.id); + resJson.phases.should.be.eql(body.param.phases); + resJson.key.should.be.eql('dev'); + resJson.revision.should.be.eql(3); + resJson.version.should.be.eql(1); + resJson.createdBy.should.be.eql(40051333); // admin + should.exist(resJson.createdAt); + resJson.updatedBy.should.be.eql(40051333); // admin + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + done(); + }); + }); + + it('should return 403 for member', (done) => { + request(server) + .post('/v4/projects/metadata/planConfig/dev/versions/1/revisions') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send(body) + .expect(403, done); + }); + }); +}); diff --git a/src/routes/planConfig/revision/delete.js b/src/routes/planConfig/revision/delete.js new file mode 100644 index 00000000..0c890678 --- /dev/null +++ b/src/routes/planConfig/revision/delete.js @@ -0,0 +1,48 @@ +/** + * API to delete a revsion + */ +import validate from 'express-validation'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import models from '../../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + key: Joi.string().max(45).required(), + version: Joi.number().integer().positive().required(), + revision: Joi.number().integer().positive().required(), + }, +}; + + +module.exports = [ + validate(schema), + permissions('planConfig.delete'), + (req, res, next) => { + models.sequelize.transaction(() => models.PlanConfig.findOne( + { + where: { + key: req.params.key, + version: req.params.version, + revision: req.params.revision, + }, + }).then((planConfig) => { + if (!planConfig) { + const apiErr = new Error('PlanConfig not found for key' + + ` ${req.params.key} version ${req.params.version} revision ${req.params.revision}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + return planConfig.update({ + deletedBy: req.authUser.userId, + }); + }).then((planConfig) => { + planConfig.destroy(); + }).then(() => { + res.status(204).end(); + }) + .catch(next)); + }, +]; diff --git a/src/routes/planConfig/revision/delete.spec.js b/src/routes/planConfig/revision/delete.spec.js new file mode 100644 index 00000000..3a668eba --- /dev/null +++ b/src/routes/planConfig/revision/delete.spec.js @@ -0,0 +1,153 @@ +/** + * Tests for delete.js + */ +import request from 'supertest'; +import chai from 'chai'; +import models from '../../../models'; +import server from '../../../app'; +import testUtil from '../../../tests/util'; + +const expectAfterDelete = (err, next) => { + if (err) throw err; + setTimeout(() => + models.PlanConfig.findOne({ + where: { + key: 'dev', + version: 1, + revision: 1, + }, + paranoid: false, + }) + .then((res) => { + if (!res) { + throw new Error('Should found the entity'); + } else { + chai.assert.isNotNull(res.deletedAt); + chai.assert.isNotNull(res.deletedBy); + + request(server) + .get('/v4/projects/metadata/planConfig/dev/versions/1/revisions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, next); + } + }), 500); +}; + + +describe('DELETE planConfig revision', () => { + const planConfigs = [ + { + key: 'dev', + phases: { + test: 'test1', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + phases: { + test: 'test2', + }, + version: 1, + revision: 2, + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.PlanConfig.create(planConfigs[0])) + .then(() => models.PlanConfig.create(planConfigs[1])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + + describe('DELETE /projects/metadata/planConfig/{key}/versions/{version}/revisions/{revision}', () => { + it('should return 403 if user is not authenticated', (done) => { + request(server) + .delete('/v4/projects/metadata/planConfig/dev/versions/1/revisions/1') + .expect(403, done); + }); + + it('should return 403 for member', (done) => { + request(server) + .delete('/v4/projects/metadata/planConfig/dev/versions/1/revisions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(403, done); + }); + + it('should return 403 for copilot', (done) => { + request(server) + .delete('/v4/projects/metadata/planConfig/dev/versions/1/revisions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(403, done); + }); + + it('should return 403 for manager', (done) => { + request(server) + .delete('/v4/projects/metadata/planConfig/dev/versions/1/revisions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(403, done); + }); + + it('should return 404 for non-existed key', (done) => { + request(server) + .delete('/v4/projects/metadata/planConfig/no-existed-key/versions/1/revisions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + + it('should return 404 for non-existed version', (done) => { + request(server) + .delete('/v4/projects/metadata/planConfig/dev/versions/100/revisions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + + + it('should return 404 for non-existed revision', (done) => { + request(server) + .delete('/v4/projects/metadata/planConfig/dev/versions/1/revisions/100') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + + it('should return 204, for admin', (done) => { + request(server) + .delete('/v4/projects/metadata/planConfig/dev/versions/1/revisions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(204) + .end(err => expectAfterDelete(err, done)); + }); + + it('should return 204, for connect admin', (done) => { + request(server) + .delete('/v4/projects/metadata/planConfig/dev/versions/1/revisions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(204) + .end(err => expectAfterDelete(err, done)); + }); + }); +}); diff --git a/src/routes/planConfig/revision/get.js b/src/routes/planConfig/revision/get.js new file mode 100644 index 00000000..2f20a564 --- /dev/null +++ b/src/routes/planConfig/revision/get.js @@ -0,0 +1,44 @@ +/** + * API to get a planConfig for particular revision + */ +import validate from 'express-validation'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../../util'; +import models from '../../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + key: Joi.string().max(45).required(), + version: Joi.number().integer().positive().required(), + revision: Joi.number().integer().positive().required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('planConfig.view'), + (req, res, next) => models.PlanConfig.findOne({ + where: { + key: req.params.key, + version: req.params.version, + revision: req.params.revision, + }, + attributes: { exclude: ['deletedAt', 'deletedBy'] }, + }) + .then((planConfig) => { + // Not found + if (!planConfig) { + const apiErr = new Error('PlanConfig not found for key' + + `${req.params.key} version ${req.params.version} revision ${req.params.revision}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + + res.json(util.wrapResponse(req.id, planConfig)); + return Promise.resolve(); + }) + .catch(next), +]; diff --git a/src/routes/planConfig/revision/get.spec.js b/src/routes/planConfig/revision/get.spec.js new file mode 100644 index 00000000..e38faf1e --- /dev/null +++ b/src/routes/planConfig/revision/get.spec.js @@ -0,0 +1,113 @@ +/** + * Tests for get.js + */ +import chai from 'chai'; +import request from 'supertest'; + +import models from '../../../models'; +import server from '../../../app'; +import testUtil from '../../../tests/util'; + +const should = chai.should(); + +describe('GET a particular revision of specific version PlanConfig', () => { + const planConfigs = [ + { + key: 'dev', + phases: { + test: 'test1', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + phases: { + test: 'test2', + }, + version: 1, + revision: 2, + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.PlanConfig.create(planConfigs[0])) + .then(() => models.PlanConfig.create(planConfigs[1])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('GET /projects/metadata/planConfig/dev/versions/{version}/revisions/{revision}', () => { + it('should return 200 for admin', (done) => { + request(server) + .get('/v4/projects/metadata/planConfig/dev/versions/1/revisions/2') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end((err, res) => { + const planConfig = planConfigs[1]; + const resJson = res.body.result.content; + + resJson.key.should.be.eql(planConfig.key); + resJson.phases.should.be.eql(planConfig.phases); + resJson.version.should.be.eql(planConfig.version); + resJson.revision.should.be.eql(planConfig.revision); + should.exist(resJson.createdAt); + resJson.updatedBy.should.be.eql(planConfig.updatedBy); + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + done(); + }); + }); + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .get('/v4/projects/metadata/planConfig/dev/versions/1/revisions/2') + .expect(403, done); + }); + + it('should return 200 for connect admin', (done) => { + request(server) + .get('/v4/projects/metadata/planConfig/dev/versions/1/revisions/2') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for connect manager', (done) => { + request(server) + .get('/v4/projects/metadata/planConfig/dev/versions/1/revisions/2') + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for member', (done) => { + request(server) + .get('/v4/projects/metadata/planConfig/dev/versions/1/revisions/2') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(200, done); + }); + + it('should return 200 for copilot', (done) => { + request(server) + .get('/v4/projects/metadata/planConfig/dev/versions/1/revisions/2') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(200, done); + }); + }); +}); diff --git a/src/routes/planConfig/revision/list.js b/src/routes/planConfig/revision/list.js new file mode 100644 index 00000000..a1bbb6ec --- /dev/null +++ b/src/routes/planConfig/revision/list.js @@ -0,0 +1,41 @@ +/** + * API to get revison list + */ +import validate from 'express-validation'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../../util'; +import models from '../../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + key: Joi.string().max(45).required(), + version: Joi.number().integer().positive().required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('planConfig.view'), + (req, res, next) => models.PlanConfig.findAll({ + where: { + key: req.params.key, + version: req.params.version, + }, + attributes: { exclude: ['deletedAt', 'deletedBy'] }, + }) + .then((planConfigs) => { + // Not found + if ((!planConfigs) || (planConfigs.length === 0)) { + const apiErr = new Error(`PlanConfig not found for key ${req.params.key} version ${req.params.version}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + + res.json(util.wrapResponse(req.id, planConfigs)); + return Promise.resolve(); + }) + .catch(next), +]; diff --git a/src/routes/planConfig/revision/list.spec.js b/src/routes/planConfig/revision/list.spec.js new file mode 100644 index 00000000..a69d49fb --- /dev/null +++ b/src/routes/planConfig/revision/list.spec.js @@ -0,0 +1,115 @@ +/* eslint-disable quote-props */ +/** + * Tests for list.js + */ +import chai from 'chai'; +import request from 'supertest'; + +import models from '../../../models'; +import server from '../../../app'; +import testUtil from '../../../tests/util'; + +const should = chai.should(); + +describe('LIST planConfig revisions', () => { + const planConfigs = [ + { + key: 'dev', + phases: { + 'test': 'test1', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + phases: { + test: 'test2', + }, + version: 1, + revision: 2, + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.PlanConfig.create(planConfigs[0])) + .then(() => models.PlanConfig.create(planConfigs[1])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('GET /projects/metadata/planConfig/dev/versions/{version}/revisions', () => { + it('should return 200 for admin', (done) => { + request(server) + .get('/v4/projects/metadata/planConfig/dev/versions/1/revisions') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end((err, res) => { + const planConfig = planConfigs[0]; + const resJson = res.body.result.content; + resJson.should.have.length(2); + + resJson[0].key.should.be.eql(planConfig.key); + resJson[0].phases.should.be.eql(planConfig.phases); + resJson[0].version.should.be.eql(planConfig.version); + resJson[0].revision.should.be.eql(planConfig.revision); + should.exist(resJson[0].createdAt); + resJson[0].updatedBy.should.be.eql(planConfig.updatedBy); + should.exist(resJson[0].updatedAt); + should.not.exist(resJson[0].deletedBy); + should.not.exist(resJson[0].deletedAt); + done(); + }); + }); + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .get('/v4/projects/metadata/planConfig/dev/versions/1/revisions') + .expect(403, done); + }); + + it('should return 200 for connect admin', (done) => { + request(server) + .get('/v4/projects/metadata/planConfig/dev/versions/1/revisions') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for connect manager', (done) => { + request(server) + .get('/v4/projects/metadata/planConfig/dev/versions/1/revisions') + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for member', (done) => { + request(server) + .get('/v4/projects/metadata/planConfig/dev/versions/1/revisions') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(200, done); + }); + + it('should return 200 for copilot', (done) => { + request(server) + .get('/v4/projects/metadata/planConfig/dev/versions/1/revisions') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(200, done); + }); + }); +}); diff --git a/src/routes/planConfig/version/create.js b/src/routes/planConfig/version/create.js new file mode 100644 index 00000000..ac5c9be9 --- /dev/null +++ b/src/routes/planConfig/version/create.js @@ -0,0 +1,64 @@ +/** + * API to add a new version of planConfig + */ +import validate from 'express-validation'; +import _ from 'lodash'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../../util'; +import models from '../../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + key: Joi.string().max(45).required(), + }, + body: { + param: Joi.object().keys({ + phases: Joi.object().required(), + + createdAt: Joi.any().strip(), + updatedAt: Joi.any().strip(), + deletedAt: Joi.any().strip(), + createdBy: Joi.any().strip(), + updatedBy: Joi.any().strip(), + deletedBy: Joi.any().strip(), + }).required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('planConfig.create'), + (req, res, next) => { + models.sequelize.transaction(() => models.PlanConfig.findAll({ + where: { + key: req.params.key, + }, + order: [['version', 'DESC']], + }).then((planConfigs) => { + let latestVersion = 1; + if (planConfigs.length !== 0) { + const latestVersionPlanConfig = planConfigs.reduce((prev, current) => + ((prev.version < current.version) ? current : prev)); + latestVersion = latestVersionPlanConfig.version + 1; + } + + const entity = _.assign(req.body.param, { + version: latestVersion, + revision: 1, + createdBy: req.authUser.userId, + updatedBy: req.authUser.userId, + key: req.params.key, + phases: req.body.param.phases, + }); + return models.PlanConfig.create(entity); + }).then((createdEntity) => { + // Omit deletedAt, deletedBy + res.status(201).json(util.wrapResponse( + req.id, _.omit(createdEntity.toJSON(), 'deletedAt', 'deletedBy'), 1, 201)); + }) + .catch(next)); + }, +]; diff --git a/src/routes/planConfig/version/create.spec.js b/src/routes/planConfig/version/create.spec.js new file mode 100644 index 00000000..298a6cd9 --- /dev/null +++ b/src/routes/planConfig/version/create.spec.js @@ -0,0 +1,114 @@ +/* eslint-disable no-unused-expressions */ +/** + * Tests for create.js + */ +import chai from 'chai'; +import request from 'supertest'; +import _ from 'lodash'; +import server from '../../../app'; +import testUtil from '../../../tests/util'; +import models from '../../../models'; + +const should = chai.should(); + +describe('CREATE PlanConfig version', () => { + const planConfigs = [ + { + key: 'dev', + phases: { + test: 'test1', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + phases: { + test: 'test2', + }, + version: 1, + revision: 2, + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.PlanConfig.create(planConfigs[0])) + .then(() => models.PlanConfig.create(planConfigs[1])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('Post /projects/metadata/planConfig/{key}/versions/', () => { + const body = { + param: { + phases: { + 'test create': 'test create', + }, + }, + }; + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .post('/v4/projects/metadata/planConfig/dev/versions') + .send(body) + .expect(403, done); + }); + + it('should return 422 if missing phases', (done) => { + const invalidBody = { + param: _.assign({}, body.param, { + phases: undefined, + }), + }; + + request(server) + .post('/v4/projects/metadata/planConfig/dev/versions/') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(invalidBody) + .expect('Content-Type', /json/) + .expect(422, done); + }); + + it('should return 201 for admin', (done) => { + request(server) + .post('/v4/projects/metadata/planConfig/dev/versions/') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + const resJson = res.body.result.content; + should.exist(resJson.id); + resJson.phases.should.be.eql(body.param.phases); + resJson.key.should.be.eql('dev'); + resJson.revision.should.be.eql(1); + resJson.version.should.be.eql(2); + resJson.createdBy.should.be.eql(40051333); // admin + should.exist(resJson.createdAt); + resJson.updatedBy.should.be.eql(40051333); // admin + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + done(); + }); + }); + + it('should return 403 for member', (done) => { + request(server) + .post('/v4/projects/metadata/planConfig/dev/versions/') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send(body) + .expect(403, done); + }); + }); +}); diff --git a/src/routes/planConfig/version/delete.js b/src/routes/planConfig/version/delete.js new file mode 100644 index 00000000..a6b141c9 --- /dev/null +++ b/src/routes/planConfig/version/delete.js @@ -0,0 +1,54 @@ +/** + * API to add a project type + */ +import validate from 'express-validation'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import models from '../../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + version: Joi.number().integer().positive().required(), + key: Joi.string().max(45).required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('planConfig.create'), + (req, res, next) => { + models.sequelize.transaction(() => models.PlanConfig.findAll( + { + where: { + key: req.params.key, + version: req.params.version, + }, + }).then((allRevision) => { + if (allRevision.length === 0) { + const apiErr = new Error(`PlanConfig not found for key ${req.params.key} version ${req.params.version}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + return models.PlanConfig.update( + { + deletedBy: req.authUser.userId, + }, { + where: { + key: req.params.key, + version: req.params.version, + }, + }); + }) + .then(() => models.PlanConfig.destroy({ + where: { + key: req.params.key, + version: req.params.version, + }, + })).then(() => { + res.status(204).end(); + }) + .catch(next)); + }, +]; diff --git a/src/routes/planConfig/version/delete.spec.js b/src/routes/planConfig/version/delete.spec.js new file mode 100644 index 00000000..33cb7581 --- /dev/null +++ b/src/routes/planConfig/version/delete.spec.js @@ -0,0 +1,139 @@ +/** + * Tests for delete.js + */ +import request from 'supertest'; +import chai from 'chai'; +import models from '../../../models'; +import server from '../../../app'; +import testUtil from '../../../tests/util'; + +const expectAfterDelete = (err, next) => { + if (err) throw err; + setTimeout(() => + models.PlanConfig.findAll({ + where: { + key: 'dev', + version: 1, + }, + paranoid: false, + }) + .then((planConfigs) => { + if (planConfigs.length === 0) { + throw new Error('Should found the entity'); + } else { + chai.assert.isNotNull(planConfigs[0].deletedAt); + chai.assert.isNotNull(planConfigs[0].deletedBy); + + chai.assert.isNotNull(planConfigs[1].deletedAt); + chai.assert.isNotNull(planConfigs[1].deletedBy); + next(); + } + }), 500); +}; + + +describe('DELETE planConfig version', () => { + const planConfigs = [ + { + key: 'dev', + phases: { + test: 'test1', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + phases: { + test: 'test2', + }, + version: 1, + revision: 2, + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.PlanConfig.create(planConfigs[0])) + .then(() => models.PlanConfig.create(planConfigs[1])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + + describe('DELETE /projects/metadata/planConfig/{key}/versions/{version}', () => { + it('should return 403 if user is not authenticated', (done) => { + request(server) + .delete('/v4/projects/metadata/planConfig/dev/versions/1') + .expect(403, done); + }); + + it('should return 403 for member', (done) => { + request(server) + .delete('/v4/projects/metadata/planConfig/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(403, done); + }); + + it('should return 403 for copilot', (done) => { + request(server) + .delete('/v4/projects/metadata/planConfig/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(403, done); + }); + + it('should return 403 for manager', (done) => { + request(server) + .delete('/v4/projects/metadata/planConfig/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(403, done); + }); + + it('should return 404 for non-existed key', (done) => { + request(server) + .delete('/v4/projects/metadata/planConfig/dev111/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + + it('should return 404 for non-existed version', (done) => { + request(server) + .delete('/v4/projects/metadata/planConfig/dev/versions/111') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + + it('should return 204, for admin', (done) => { + request(server) + .delete('/v4/projects/metadata/planConfig/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(204) + .end(err => expectAfterDelete(err, done)); + }); + + it('should return 204, for connect admin', (done) => { + request(server) + .delete('/v4/projects/metadata/planConfig/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(204) + .end(err => expectAfterDelete(err, done)); + }); + }); +}); diff --git a/src/routes/planConfig/version/get.js b/src/routes/planConfig/version/get.js new file mode 100644 index 00000000..cedc728b --- /dev/null +++ b/src/routes/planConfig/version/get.js @@ -0,0 +1,33 @@ +/** + * API to get a latest version for key + * + */ +import validate from 'express-validation'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../../util'; +import models from '../../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + key: Joi.string().max(45).required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('planConfig.view'), + (req, res, next) => models.PlanConfig.latestRevisionofLatestVersion(req.params.key) + .then((form) => { + if (form == null) { + const apiErr = new Error(`PlanConfig not found for key ${req.params.key} version ${req.params.version}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + res.json(util.wrapResponse(req.id, form)); + return Promise.resolve(); + }) + .catch(next), +]; diff --git a/src/routes/planConfig/version/get.spec.js b/src/routes/planConfig/version/get.spec.js new file mode 100644 index 00000000..ca04907c --- /dev/null +++ b/src/routes/planConfig/version/get.spec.js @@ -0,0 +1,134 @@ +/** + * Tests for get.js + */ +import chai from 'chai'; +import request from 'supertest'; + +import models from '../../../models'; +import server from '../../../app'; +import testUtil from '../../../tests/util'; + +const should = chai.should(); + +describe('GET a latest version of specific key of PlanConfig', () => { + const planConfigs = [ + { + key: 'dev', + phases: { + test: 'test1', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + phases: { + test: 'test2', + }, + version: 2, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + phases: { + test: 'test2', + }, + version: 2, + revision: 2, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + phases: { + test: 'test3', + }, + version: 1, + revision: 2, + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.PlanConfig.create(planConfigs[0])) + .then(() => models.PlanConfig.create(planConfigs[1])) + .then(() => models.PlanConfig.create(planConfigs[2])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('GET /projects/metadata/planConfig/dev/versions/{version}', () => { + it('should return 200 and correct version for admin', (done) => { + request(server) + .get('/v4/projects/metadata/planConfig/dev') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end((err, res) => { + const planConfig = planConfigs[2]; + const resJson = res.body.result.content; + + resJson.key.should.be.eql(planConfig.key); + resJson.phases.should.be.eql(planConfig.phases); + resJson.version.should.be.eql(planConfig.version); + resJson.revision.should.be.eql(planConfig.revision); + should.exist(resJson.createdAt); + resJson.updatedBy.should.be.eql(planConfig.updatedBy); + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + done(); + }); + }); + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .get('/v4/projects/metadata/planConfig/dev') + .expect(403, done); + }); + + it('should return 200 for connect admin', (done) => { + request(server) + .get('/v4/projects/metadata/planConfig/dev') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for connect manager', (done) => { + request(server) + .get('/v4/projects/metadata/planConfig/dev') + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for member', (done) => { + request(server) + .get('/v4/projects/metadata/planConfig/dev') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(200, done); + }); + + it('should return 200 for copilot', (done) => { + request(server) + .get('/v4/projects/metadata/planConfig/dev') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(200, done); + }); + }); +}); diff --git a/src/routes/planConfig/version/getVersion.js b/src/routes/planConfig/version/getVersion.js new file mode 100644 index 00000000..556d8248 --- /dev/null +++ b/src/routes/planConfig/version/getVersion.js @@ -0,0 +1,42 @@ +/** + * API to get a planConfig for particular version + */ +import validate from 'express-validation'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../../util'; +import models from '../../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + key: Joi.string().max(45).required(), + version: Joi.number().integer().positive().required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('planConfig.view'), + (req, res, next) => models.PlanConfig.findOne({ + where: { + key: req.params.key, + version: req.params.version, + }, + order: [['revision', 'DESC']], + limit: 1, + attributes: { exclude: ['deletedAt', 'deletedBy'] }, + }) + .then((planConfig) => { + // Not found + if (!planConfig) { + const apiErr = new Error(`PlanConfig not found for key ${req.params.key} version ${req.params.version}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + res.json(util.wrapResponse(req.id, planConfig)); + return Promise.resolve(); + }) + .catch(next), +]; diff --git a/src/routes/planConfig/version/getVersion.spec.js b/src/routes/planConfig/version/getVersion.spec.js new file mode 100644 index 00000000..e73e45a7 --- /dev/null +++ b/src/routes/planConfig/version/getVersion.spec.js @@ -0,0 +1,124 @@ +/** + * Tests for getVersion.js + */ +import chai from 'chai'; +import request from 'supertest'; + +import models from '../../../models'; +import server from '../../../app'; +import testUtil from '../../../tests/util'; + +const should = chai.should(); + +describe('GET a particular version of specific key of PlanConfig', () => { + const planConfigs = [ + { + key: 'dev', + phases: { + test: 'test1', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + phases: { + test: 'test2', + }, + version: 2, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + phases: { + test: 'test3', + }, + version: 2, + revision: 2, + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.PlanConfig.create(planConfigs[0])) + .then(() => models.PlanConfig.create(planConfigs[1])) + .then(() => models.PlanConfig.create(planConfigs[2])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('GET /projects/metadata/planConfig/dev/versions/{version}', () => { + it('should return 200 for admin', (done) => { + request(server) + .get('/v4/projects/metadata/planConfig/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end((err, res) => { + const planConfig = planConfigs[0]; + const resJson = res.body.result.content; + + resJson.key.should.be.eql(planConfig.key); + resJson.phases.should.be.eql(planConfig.phases); + resJson.version.should.be.eql(planConfig.version); + resJson.revision.should.be.eql(planConfig.revision); + should.exist(resJson.createdAt); + resJson.updatedBy.should.be.eql(planConfig.updatedBy); + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + done(); + }); + }); + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .get('/v4/projects/metadata/planConfig/dev/versions/1') + .expect(403, done); + }); + + it('should return 200 for connect admin', (done) => { + request(server) + .get('/v4/projects/metadata/planConfig/dev') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for connect manager', (done) => { + request(server) + .get('/v4/projects/metadata/planConfig/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for member', (done) => { + request(server) + .get('/v4/projects/metadata/planConfig/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(200, done); + }); + + it('should return 200 for copilot', (done) => { + request(server) + .get('/v4/projects/metadata/planConfig/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(200, done); + }); + }); +}); diff --git a/src/routes/planConfig/version/list.js b/src/routes/planConfig/version/list.js new file mode 100644 index 00000000..5624a981 --- /dev/null +++ b/src/routes/planConfig/version/list.js @@ -0,0 +1,47 @@ +/** + * API to get a planConfig list + */ +import validate from 'express-validation'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../../util'; +import models from '../../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + key: Joi.string().max(45).required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('planConfig.view'), + (req, res, next) => models.PlanConfig.findAll({ + where: { + key: req.params.key, + }, + attributes: { exclude: ['deletedAt', 'deletedBy'] }, + }) + .then((planConfigs) => { + // Not found + if ((!planConfigs) || (planConfigs.length === 0)) { + const apiErr = new Error(`PlanConfig not found for key ${req.params.key}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + + const latestPlanConfigs = {}; + planConfigs.forEach((element) => { + const isNewerRevision = (latestPlanConfigs[element.version] != null) && + (latestPlanConfigs[element.version].revision < element.revision); + if ((latestPlanConfigs[element.version] == null) || isNewerRevision) { + latestPlanConfigs[element.version] = element; + } + }); + res.json(util.wrapResponse(req.id, Object.values(latestPlanConfigs))); + return Promise.resolve(); + }) + .catch(next), +]; diff --git a/src/routes/planConfig/version/list.spec.js b/src/routes/planConfig/version/list.spec.js new file mode 100644 index 00000000..a5197ee8 --- /dev/null +++ b/src/routes/planConfig/version/list.spec.js @@ -0,0 +1,115 @@ +/* eslint-disable quote-props */ +/** + * Tests for list.js + */ +import chai from 'chai'; +import request from 'supertest'; + +import models from '../../../models'; +import server from '../../../app'; +import testUtil from '../../../tests/util'; + +const should = chai.should(); + +describe('LIST planConfig versions', () => { + const planConfigs = [ + { + key: 'dev', + phases: { + 'test': 'test1', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + phases: { + test: 'test2', + }, + version: 2, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.PlanConfig.create(planConfigs[0])) + .then(() => models.PlanConfig.create(planConfigs[1])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('GET /projects/metadata/planConfig/dev/versions/{version}', () => { + it('should return 200 for admin', (done) => { + request(server) + .get('/v4/projects/metadata/planConfig/dev/versions') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end((err, res) => { + const planConfig = planConfigs[0]; + const resJson = res.body.result.content; + resJson.should.have.length(2); + + resJson[0].key.should.be.eql(planConfig.key); + resJson[0].phases.should.be.eql(planConfig.phases); + resJson[0].version.should.be.eql(planConfig.version); + resJson[0].revision.should.be.eql(planConfig.revision); + should.exist(resJson[0].createdAt); + resJson[0].updatedBy.should.be.eql(planConfig.updatedBy); + should.exist(resJson[0].updatedAt); + should.not.exist(resJson[0].deletedBy); + should.not.exist(resJson[0].deletedAt); + done(); + }); + }); + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .get('/v4/projects/metadata/planConfig/dev/versions') + .expect(403, done); + }); + + it('should return 200 for connect admin', (done) => { + request(server) + .get('/v4/projects/metadata/planConfig/dev/versions') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for connect manager', (done) => { + request(server) + .get('/v4/projects/metadata/planConfig/dev/versions') + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for member', (done) => { + request(server) + .get('/v4/projects/metadata/planConfig/dev/versions') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(200, done); + }); + + it('should return 200 for copilot', (done) => { + request(server) + .get('/v4/projects/metadata/planConfig/dev/versions') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(200, done); + }); + }); +}); diff --git a/src/routes/planConfig/version/update.js b/src/routes/planConfig/version/update.js new file mode 100644 index 00000000..77c67a32 --- /dev/null +++ b/src/routes/planConfig/version/update.js @@ -0,0 +1,74 @@ +/* eslint-disable no-trailing-spaces */ +/** + * API to add a project type + */ +import config from 'config'; +import validate from 'express-validation'; +import _ from 'lodash'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../../util'; +import models from '../../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + version: Joi.number().integer().positive().required(), + key: Joi.string().max(45).required(), + }, + body: { + param: Joi.object().keys({ + phases: Joi.object().required(), + + createdAt: Joi.any().strip(), + updatedAt: Joi.any().strip(), + deletedAt: Joi.any().strip(), + createdBy: Joi.any().strip(), + updatedBy: Joi.any().strip(), + deletedBy: Joi.any().strip(), + }).required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('planConfig.create'), + (req, res, next) => { + models.sequelize.transaction(() => models.PlanConfig.findAll({ + where: { + key: req.params.key, + version: req.params.version, + }, + order: [['revision', 'DESC']], + }).then((planConfigs) => { + if (planConfigs.length >= config.get('MAX_REVISION_NUMBER')) { + return models.PlanConfig.deleteOldestRevision(req.authUser.userId, req.params.key, req.params.version) + .then(() => Promise.resolve(planConfigs[0])); + } else if (planConfigs.length === 0) { + const apiErr = new Error(`PlanConfig not found for key ${req.params.key} version ${req.params.version}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + return Promise.resolve(planConfigs[0]); + }) + .then((planConfig) => { + const revisison = planConfig.revision + 1; + const entity = { + version: req.params.version, + revision: revisison, + createdBy: req.authUser.userId, + updatedBy: req.authUser.userId, + key: req.params.key, + phases: req.body.param.phases, + }; + return models.PlanConfig.create(entity); + }) + .then((createdEntity) => { + // Omit deletedAt, deletedBy + res.status(201).json(util.wrapResponse( + req.id, _.omit(createdEntity.toJSON(), 'deletedAt', 'deletedBy'), 1, 201)); + }) + .catch(next)); + }, +]; diff --git a/src/routes/planConfig/version/update.spec.js b/src/routes/planConfig/version/update.spec.js new file mode 100644 index 00000000..7a8ea328 --- /dev/null +++ b/src/routes/planConfig/version/update.spec.js @@ -0,0 +1,113 @@ +/* eslint-disable no-unused-expressions */ +/** + * Tests for create.js + */ +import chai from 'chai'; +import request from 'supertest'; +import _ from 'lodash'; +import server from '../../../app'; +import testUtil from '../../../tests/util'; +import models from '../../../models'; + +const should = chai.should(); + +describe('UPDATE PlanConfig version', () => { + const planConfigs = [ + { + key: 'dev', + phases: { + test: 'test1', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + phases: { + test: 'test2', + }, + version: 1, + revision: 2, + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.PlanConfig.create(planConfigs[0])) + .then(() => models.PlanConfig.create(planConfigs[1])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('Post /projects/metadata/planConfig/{key}/versions/{version}', () => { + const body = { + param: { + phases: { + 'test create': 'test create', + }, + }, + }; + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .patch('/v4/projects/metadata/planConfig/dev/versions/1') + .send(body) + .expect(403, done); + }); + + it('should return 422 if missing phases', (done) => { + const invalidBody = { + param: _.assign({}, body.param, { + phases: undefined, + }), + }; + request(server) + .patch('/v4/projects/metadata/planConfig/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(invalidBody) + .expect('Content-Type', /json/) + .expect(422, done); + }); + + it('should return 201 for admin', (done) => { + request(server) + .patch('/v4/projects/metadata/planConfig/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + const resJson = res.body.result.content; + should.exist(resJson.id); + resJson.phases.should.be.eql(body.param.phases); + resJson.key.should.be.eql('dev'); + resJson.revision.should.be.eql(3); + resJson.version.should.be.eql(1); + resJson.createdBy.should.be.eql(40051333); // admin + should.exist(resJson.createdAt); + resJson.updatedBy.should.be.eql(40051333); // admin + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + done(); + }); + }); + + it('should return 403 for member', (done) => { + request(server) + .patch('/v4/projects/metadata/planConfig/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send(body) + .expect(403, done); + }); + }); +}); diff --git a/src/routes/priceConfig/revision/create.js b/src/routes/priceConfig/revision/create.js new file mode 100644 index 00000000..83790b82 --- /dev/null +++ b/src/routes/priceConfig/revision/create.js @@ -0,0 +1,66 @@ +/** + * API to add a priceConfig revision + */ +import validate from 'express-validation'; +import _ from 'lodash'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../../util'; +import models from '../../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + key: Joi.string().max(45).required(), + version: Joi.number().integer().positive().required(), + }, + body: { + param: Joi.object().keys({ + config: Joi.object().required(), + + createdAt: Joi.any().strip(), + updatedAt: Joi.any().strip(), + deletedAt: Joi.any().strip(), + createdBy: Joi.any().strip(), + updatedBy: Joi.any().strip(), + deletedBy: Joi.any().strip(), + }).required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('priceConfig.create'), + (req, res, next) => { + models.sequelize.transaction(() => models.PriceConfig.findOne({ + where: { + key: req.params.key, + version: req.params.version, + }, + order: [['revision', 'DESC']], + }).then((priceConfig) => { + if (priceConfig) { + const version = priceConfig ? priceConfig.version : 1; + const revision = priceConfig ? priceConfig.revision + 1 : 1; + const entity = _.assign(req.body.param, { + version, + revision, + createdBy: req.authUser.userId, + updatedBy: req.authUser.userId, + key: req.params.key, + config: req.body.param.config, + }); + return models.PriceConfig.create(entity); + } + const apiErr = new Error(`PriceConfig not exists for key ${req.params.key} version ${req.params.version}`); + apiErr.status = 404; + return Promise.reject(apiErr); + }).then((createdEntity) => { + // Omit deletedAt, deletedBy + res.status(201).json(util.wrapResponse( + req.id, _.omit(createdEntity.toJSON(), 'deletedAt', 'deletedBy'), 1, 201)); + }) + .catch(next)); + }, +]; diff --git a/src/routes/priceConfig/revision/create.spec.js b/src/routes/priceConfig/revision/create.spec.js new file mode 100644 index 00000000..b082eddc --- /dev/null +++ b/src/routes/priceConfig/revision/create.spec.js @@ -0,0 +1,136 @@ +/* eslint-disable no-unused-expressions */ +/** + * Tests for create.js + */ +import chai from 'chai'; +import request from 'supertest'; +import _ from 'lodash'; +import server from '../../../app'; +import testUtil from '../../../tests/util'; +import models from '../../../models'; + +const should = chai.should(); + +describe('CREATE PriceConfig Revision', () => { + const priceConfigs = [ + { + key: 'dev', + config: { + test: 'test1', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + config: { + test: 'test2', + }, + version: 1, + revision: 2, + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.PriceConfig.create(priceConfigs[0])) + .then(() => models.PriceConfig.create(priceConfigs[1])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('Post /projects/metadata/priceConfig/{key}/versions/{version}/revision', () => { + const body = { + param: { + config: { + 'test create': 'test create', + }, + }, + }; + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .post('/v4/projects/metadata/priceConfig/dev/versions/1/revisions') + .send(body) + .expect(403, done); + }); + + it('should return 404 if missing key', (done) => { + request(server) + .post('/v4/projects/metadata/priceConfig/no-exist-key/versions/1/revisions') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(404, done); + }); + + it('should return 404 if missing version', (done) => { + request(server) + .post('/v4/projects/metadata/priceConfig/dev/versions/100/revisions') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(404, done); + }); + + it('should return 422 if missing config', (done) => { + const invalidBody = { + param: _.assign({}, body.param, { + config: undefined, + }), + }; + + request(server) + .post('/v4/projects/metadata/priceConfig/no-exist-key/versions/1/revisions') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(invalidBody) + .expect('Content-Type', /json/) + .expect(422, done); + }); + + it('should return 201 for admin', (done) => { + request(server) + .post('/v4/projects/metadata/priceConfig/dev/versions/1/revisions') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + const resJson = res.body.result.content; + should.exist(resJson.id); + resJson.config.should.be.eql(body.param.config); + resJson.key.should.be.eql('dev'); + resJson.revision.should.be.eql(3); + resJson.version.should.be.eql(1); + resJson.createdBy.should.be.eql(40051333); // admin + should.exist(resJson.createdAt); + resJson.updatedBy.should.be.eql(40051333); // admin + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + done(); + }); + }); + + it('should return 403 for member', (done) => { + request(server) + .post('/v4/projects/metadata/priceConfig/dev/versions/1/revisions') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send(body) + .expect(403, done); + }); + }); +}); diff --git a/src/routes/priceConfig/revision/delete.js b/src/routes/priceConfig/revision/delete.js new file mode 100644 index 00000000..3ce81743 --- /dev/null +++ b/src/routes/priceConfig/revision/delete.js @@ -0,0 +1,48 @@ +/** + * API to delete a revsion + */ +import validate from 'express-validation'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import models from '../../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + key: Joi.string().max(45).required(), + version: Joi.number().integer().positive().required(), + revision: Joi.number().integer().positive().required(), + }, +}; + + +module.exports = [ + validate(schema), + permissions('priceConfig.delete'), + (req, res, next) => { + models.sequelize.transaction(() => models.PriceConfig.findOne( + { + where: { + key: req.params.key, + version: req.params.version, + revision: req.params.revision, + }, + }).then((priceConfig) => { + if (!priceConfig) { + const apiErr = new Error('PriceConfig not found for key' + + ` ${req.params.key} version ${req.params.version} revision ${req.params.revision}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + return priceConfig.update({ + deletedBy: req.authUser.userId, + }); + }).then((priceConfig) => { + priceConfig.destroy(); + }).then(() => { + res.status(204).end(); + }) + .catch(next)); + }, +]; diff --git a/src/routes/priceConfig/revision/delete.spec.js b/src/routes/priceConfig/revision/delete.spec.js new file mode 100644 index 00000000..0acc1491 --- /dev/null +++ b/src/routes/priceConfig/revision/delete.spec.js @@ -0,0 +1,153 @@ +/** + * Tests for delete.js + */ +import request from 'supertest'; +import chai from 'chai'; +import models from '../../../models'; +import server from '../../../app'; +import testUtil from '../../../tests/util'; + +const expectAfterDelete = (err, next) => { + if (err) throw err; + setTimeout(() => + models.PriceConfig.findOne({ + where: { + key: 'dev', + version: 1, + revision: 1, + }, + paranoid: false, + }) + .then((res) => { + if (!res) { + throw new Error('Should found the entity'); + } else { + chai.assert.isNotNull(res.deletedAt); + chai.assert.isNotNull(res.deletedBy); + + request(server) + .get('/v4/projects/metadata/priceConfig/dev/versions/1/revisions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, next); + } + }), 500); +}; + + +describe('DELETE priceConfig revision', () => { + const priceConfigs = [ + { + key: 'dev', + config: { + test: 'test1', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + config: { + test: 'test2', + }, + version: 1, + revision: 2, + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.PriceConfig.create(priceConfigs[0])) + .then(() => models.PriceConfig.create(priceConfigs[1])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + + describe('DELETE /projects/metadata/priceConfig/{key}/versions/{version}/revisions/{revision}', () => { + it('should return 403 if user is not authenticated', (done) => { + request(server) + .delete('/v4/projects/metadata/priceConfig/dev/versions/1/revisions/1') + .expect(403, done); + }); + + it('should return 403 for member', (done) => { + request(server) + .delete('/v4/projects/metadata/priceConfig/dev/versions/1/revisions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(403, done); + }); + + it('should return 403 for copilot', (done) => { + request(server) + .delete('/v4/projects/metadata/priceConfig/dev/versions/1/revisions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(403, done); + }); + + it('should return 403 for manager', (done) => { + request(server) + .delete('/v4/projects/metadata/priceConfig/dev/versions/1/revisions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(403, done); + }); + + it('should return 404 for non-existed key', (done) => { + request(server) + .delete('/v4/projects/metadata/priceConfig/no-existed-key/versions/1/revisions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + + it('should return 404 for non-existed version', (done) => { + request(server) + .delete('/v4/projects/metadata/priceConfig/dev/versions/100/revisions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + + + it('should return 404 for non-existed revision', (done) => { + request(server) + .delete('/v4/projects/metadata/priceConfig/dev/versions/1/revisions/100') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + + it('should return 204, for admin', (done) => { + request(server) + .delete('/v4/projects/metadata/priceConfig/dev/versions/1/revisions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(204) + .end(err => expectAfterDelete(err, done)); + }); + + it('should return 204, for connect admin', (done) => { + request(server) + .delete('/v4/projects/metadata/priceConfig/dev/versions/1/revisions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(204) + .end(err => expectAfterDelete(err, done)); + }); + }); +}); diff --git a/src/routes/priceConfig/revision/get.js b/src/routes/priceConfig/revision/get.js new file mode 100644 index 00000000..081f051c --- /dev/null +++ b/src/routes/priceConfig/revision/get.js @@ -0,0 +1,44 @@ +/** + * API to get a priceConfig for particular revision + */ +import validate from 'express-validation'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../../util'; +import models from '../../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + key: Joi.string().max(45).required(), + version: Joi.number().integer().positive().required(), + revision: Joi.number().integer().positive().required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('priceConfig.view'), + (req, res, next) => models.PriceConfig.findOne({ + where: { + key: req.params.key, + version: req.params.version, + revision: req.params.revision, + }, + attributes: { exclude: ['deletedAt', 'deletedBy'] }, + }) + .then((priceConfig) => { + // Not found + if (!priceConfig) { + const apiErr = new Error('PriceConfig not found for key' + + ` ${req.params.key} version ${req.params.version} revision ${req.params.revision}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + + res.json(util.wrapResponse(req.id, priceConfig)); + return Promise.resolve(); + }) + .catch(next), +]; diff --git a/src/routes/priceConfig/revision/get.spec.js b/src/routes/priceConfig/revision/get.spec.js new file mode 100644 index 00000000..7830f440 --- /dev/null +++ b/src/routes/priceConfig/revision/get.spec.js @@ -0,0 +1,113 @@ +/** + * Tests for get.js + */ +import chai from 'chai'; +import request from 'supertest'; + +import models from '../../../models'; +import server from '../../../app'; +import testUtil from '../../../tests/util'; + +const should = chai.should(); + +describe('GET a particular revision of specific version PriceConfig', () => { + const priceConfigs = [ + { + key: 'dev', + config: { + test: 'test1', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + config: { + test: 'test2', + }, + version: 1, + revision: 2, + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.PriceConfig.create(priceConfigs[0])) + .then(() => models.PriceConfig.create(priceConfigs[1])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('GET /projects/metadata/priceConfig/dev/versions/{version}/revisions/{revision}', () => { + it('should return 200 for admin', (done) => { + request(server) + .get('/v4/projects/metadata/priceConfig/dev/versions/1/revisions/2') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end((err, res) => { + const priceConfig = priceConfigs[1]; + const resJson = res.body.result.content; + + resJson.key.should.be.eql(priceConfig.key); + resJson.config.should.be.eql(priceConfig.config); + resJson.version.should.be.eql(priceConfig.version); + resJson.revision.should.be.eql(priceConfig.revision); + should.exist(resJson.createdAt); + resJson.updatedBy.should.be.eql(priceConfig.updatedBy); + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + done(); + }); + }); + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .get('/v4/projects/metadata/priceConfig/dev/versions/1/revisions/2') + .expect(403, done); + }); + + it('should return 200 for connect admin', (done) => { + request(server) + .get('/v4/projects/metadata/priceConfig/dev/versions/1/revisions/2') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for connect manager', (done) => { + request(server) + .get('/v4/projects/metadata/priceConfig/dev/versions/1/revisions/2') + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for member', (done) => { + request(server) + .get('/v4/projects/metadata/priceConfig/dev/versions/1/revisions/2') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(200, done); + }); + + it('should return 200 for copilot', (done) => { + request(server) + .get('/v4/projects/metadata/priceConfig/dev/versions/1/revisions/2') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(200, done); + }); + }); +}); diff --git a/src/routes/priceConfig/revision/list.js b/src/routes/priceConfig/revision/list.js new file mode 100644 index 00000000..1d67a9d1 --- /dev/null +++ b/src/routes/priceConfig/revision/list.js @@ -0,0 +1,41 @@ +/** + * API to get revison list + */ +import validate from 'express-validation'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../../util'; +import models from '../../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + key: Joi.string().max(45).required(), + version: Joi.number().integer().positive().required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('priceConfig.view'), + (req, res, next) => models.PriceConfig.findAll({ + where: { + key: req.params.key, + version: req.params.version, + }, + attributes: { exclude: ['deletedAt', 'deletedBy'] }, + }) + .then((priceConfigs) => { + // Not found + if ((!priceConfigs) || (priceConfigs.length === 0)) { + const apiErr = new Error(`PriceConfig not found for key ${req.params.key} version ${req.params.version}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + + res.json(util.wrapResponse(req.id, priceConfigs)); + return Promise.resolve(); + }) + .catch(next), +]; diff --git a/src/routes/priceConfig/revision/list.spec.js b/src/routes/priceConfig/revision/list.spec.js new file mode 100644 index 00000000..04363891 --- /dev/null +++ b/src/routes/priceConfig/revision/list.spec.js @@ -0,0 +1,115 @@ +/* eslint-disable quote-props */ +/** + * Tests for list.js + */ +import chai from 'chai'; +import request from 'supertest'; + +import models from '../../../models'; +import server from '../../../app'; +import testUtil from '../../../tests/util'; + +const should = chai.should(); + +describe('LIST priceConfig revisions', () => { + const priceConfigs = [ + { + key: 'dev', + config: { + 'test': 'test1', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + config: { + test: 'test2', + }, + version: 1, + revision: 2, + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.PriceConfig.create(priceConfigs[0])) + .then(() => models.PriceConfig.create(priceConfigs[1])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('GET /projects/metadata/priceConfig/dev/versions/{version}/revisions', () => { + it('should return 200 for admin', (done) => { + request(server) + .get('/v4/projects/metadata/priceConfig/dev/versions/1/revisions') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end((err, res) => { + const priceConfig = priceConfigs[0]; + const resJson = res.body.result.content; + resJson.should.have.length(2); + + resJson[0].key.should.be.eql(priceConfig.key); + resJson[0].config.should.be.eql(priceConfig.config); + resJson[0].version.should.be.eql(priceConfig.version); + resJson[0].revision.should.be.eql(priceConfig.revision); + should.exist(resJson[0].createdAt); + resJson[0].updatedBy.should.be.eql(priceConfig.updatedBy); + should.exist(resJson[0].updatedAt); + should.not.exist(resJson[0].deletedBy); + should.not.exist(resJson[0].deletedAt); + done(); + }); + }); + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .get('/v4/projects/metadata/priceConfig/dev/versions/1/revisions') + .expect(403, done); + }); + + it('should return 200 for connect admin', (done) => { + request(server) + .get('/v4/projects/metadata/priceConfig/dev/versions/1/revisions') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for connect manager', (done) => { + request(server) + .get('/v4/projects/metadata/priceConfig/dev/versions/1/revisions') + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for member', (done) => { + request(server) + .get('/v4/projects/metadata/priceConfig/dev/versions/1/revisions') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(200, done); + }); + + it('should return 200 for copilot', (done) => { + request(server) + .get('/v4/projects/metadata/priceConfig/dev/versions/1/revisions') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(200, done); + }); + }); +}); diff --git a/src/routes/priceConfig/version/create.js b/src/routes/priceConfig/version/create.js new file mode 100644 index 00000000..d3c71335 --- /dev/null +++ b/src/routes/priceConfig/version/create.js @@ -0,0 +1,64 @@ +/** + * API to add a new version of priceConfig + */ +import validate from 'express-validation'; +import _ from 'lodash'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../../util'; +import models from '../../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + key: Joi.string().max(45).required(), + }, + body: { + param: Joi.object().keys({ + config: Joi.object().required(), + + createdAt: Joi.any().strip(), + updatedAt: Joi.any().strip(), + deletedAt: Joi.any().strip(), + createdBy: Joi.any().strip(), + updatedBy: Joi.any().strip(), + deletedBy: Joi.any().strip(), + }).required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('priceConfig.create'), + (req, res, next) => { + models.sequelize.transaction(() => models.PriceConfig.findAll({ + where: { + key: req.params.key, + }, + order: [['version', 'DESC']], + }).then((priceConfigs) => { + let latestVersion = 1; + if (priceConfigs.length !== 0) { + const latestVersionPriceConfig = priceConfigs.reduce((prev, current) => + ((prev.version < current.version) ? current : prev)); + latestVersion = latestVersionPriceConfig.version + 1; + } + + const entity = _.assign(req.body.param, { + version: latestVersion, + revision: 1, + createdBy: req.authUser.userId, + updatedBy: req.authUser.userId, + key: req.params.key, + config: req.body.param.config, + }); + return models.PriceConfig.create(entity); + }).then((createdEntity) => { + // Omit deletedAt, deletedBy + res.status(201).json(util.wrapResponse( + req.id, _.omit(createdEntity.toJSON(), 'deletedAt', 'deletedBy'), 1, 201)); + }) + .catch(next)); + }, +]; diff --git a/src/routes/priceConfig/version/create.spec.js b/src/routes/priceConfig/version/create.spec.js new file mode 100644 index 00000000..6ad58665 --- /dev/null +++ b/src/routes/priceConfig/version/create.spec.js @@ -0,0 +1,114 @@ +/* eslint-disable no-unused-expressions */ +/** + * Tests for create.js + */ +import chai from 'chai'; +import request from 'supertest'; +import _ from 'lodash'; +import server from '../../../app'; +import testUtil from '../../../tests/util'; +import models from '../../../models'; + +const should = chai.should(); + +describe('CREATE PriceConfig version', () => { + const priceConfigs = [ + { + key: 'dev', + config: { + test: 'test1', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + config: { + test: 'test2', + }, + version: 1, + revision: 2, + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.PriceConfig.create(priceConfigs[0])) + .then(() => models.PriceConfig.create(priceConfigs[1])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('Post /projects/metadata/priceConfig/{key}/versions/', () => { + const body = { + param: { + config: { + 'test create': 'test create', + }, + }, + }; + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .post('/v4/projects/metadata/priceConfig/dev/versions') + .send(body) + .expect(403, done); + }); + + it('should return 422 if missing config', (done) => { + const invalidBody = { + param: _.assign({}, body.param, { + config: undefined, + }), + }; + + request(server) + .post('/v4/projects/metadata/priceConfig/dev/versions/') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(invalidBody) + .expect('Content-Type', /json/) + .expect(422, done); + }); + + it('should return 201 for admin', (done) => { + request(server) + .post('/v4/projects/metadata/priceConfig/dev/versions/') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + const resJson = res.body.result.content; + should.exist(resJson.id); + resJson.config.should.be.eql(body.param.config); + resJson.key.should.be.eql('dev'); + resJson.revision.should.be.eql(1); + resJson.version.should.be.eql(2); + resJson.createdBy.should.be.eql(40051333); // admin + should.exist(resJson.createdAt); + resJson.updatedBy.should.be.eql(40051333); // admin + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + done(); + }); + }); + + it('should return 403 for member', (done) => { + request(server) + .post('/v4/projects/metadata/priceConfig/dev/versions/') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send(body) + .expect(403, done); + }); + }); +}); diff --git a/src/routes/priceConfig/version/delete.js b/src/routes/priceConfig/version/delete.js new file mode 100644 index 00000000..3aa4bc66 --- /dev/null +++ b/src/routes/priceConfig/version/delete.js @@ -0,0 +1,54 @@ +/** + * API to add a project type + */ +import validate from 'express-validation'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import models from '../../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + version: Joi.number().integer().positive().required(), + key: Joi.string().max(45).required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('priceConfig.create'), + (req, res, next) => { + models.sequelize.transaction(() => models.PriceConfig.findAll( + { + where: { + key: req.params.key, + version: req.params.version, + }, + }).then((allRevision) => { + if (allRevision.length === 0) { + const apiErr = new Error(`PriceConfig not found for key ${req.params.key} version ${req.params.version}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + return models.PriceConfig.update( + { + deletedBy: req.authUser.userId, + }, { + where: { + key: req.params.key, + version: req.params.version, + }, + }); + }) + .then(() => models.PriceConfig.destroy({ + where: { + key: req.params.key, + version: req.params.version, + }, + })).then(() => { + res.status(204).end(); + }) + .catch(next)); + }, +]; diff --git a/src/routes/priceConfig/version/delete.spec.js b/src/routes/priceConfig/version/delete.spec.js new file mode 100644 index 00000000..2bcac11f --- /dev/null +++ b/src/routes/priceConfig/version/delete.spec.js @@ -0,0 +1,139 @@ +/** + * Tests for delete.js + */ +import request from 'supertest'; +import chai from 'chai'; +import models from '../../../models'; +import server from '../../../app'; +import testUtil from '../../../tests/util'; + +const expectAfterDelete = (err, next) => { + if (err) throw err; + setTimeout(() => + models.PriceConfig.findAll({ + where: { + key: 'dev', + version: 1, + }, + paranoid: false, + }) + .then((priceConfigs) => { + if (priceConfigs.length === 0) { + throw new Error('Should found the entity'); + } else { + chai.assert.isNotNull(priceConfigs[0].deletedAt); + chai.assert.isNotNull(priceConfigs[0].deletedBy); + + chai.assert.isNotNull(priceConfigs[1].deletedAt); + chai.assert.isNotNull(priceConfigs[1].deletedBy); + next(); + } + }), 500); +}; + + +describe('DELETE priceConfig version', () => { + const priceConfigs = [ + { + key: 'dev', + config: { + test: 'test1', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + config: { + test: 'test2', + }, + version: 1, + revision: 2, + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.PriceConfig.create(priceConfigs[0])) + .then(() => models.PriceConfig.create(priceConfigs[1])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + + describe('DELETE /projects/metadata/priceConfig/{key}/versions/{version}', () => { + it('should return 403 if user is not authenticated', (done) => { + request(server) + .delete('/v4/projects/metadata/priceConfig/dev/versions/1') + .expect(403, done); + }); + + it('should return 403 for member', (done) => { + request(server) + .delete('/v4/projects/metadata/priceConfig/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(403, done); + }); + + it('should return 403 for copilot', (done) => { + request(server) + .delete('/v4/projects/metadata/priceConfig/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(403, done); + }); + + it('should return 403 for manager', (done) => { + request(server) + .delete('/v4/projects/metadata/priceConfig/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(403, done); + }); + + it('should return 404 for non-existed key', (done) => { + request(server) + .delete('/v4/projects/metadata/priceConfig/dev111/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + + it('should return 404 for non-existed version', (done) => { + request(server) + .delete('/v4/projects/metadata/priceConfig/dev/versions/111') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + + it('should return 204, for admin', (done) => { + request(server) + .delete('/v4/projects/metadata/priceConfig/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(204) + .end(err => expectAfterDelete(err, done)); + }); + + it('should return 204, for connect admin', (done) => { + request(server) + .delete('/v4/projects/metadata/priceConfig/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(204) + .end(err => expectAfterDelete(err, done)); + }); + }); +}); diff --git a/src/routes/priceConfig/version/get.js b/src/routes/priceConfig/version/get.js new file mode 100644 index 00000000..b997b9b7 --- /dev/null +++ b/src/routes/priceConfig/version/get.js @@ -0,0 +1,33 @@ +/** + * API to get a latest version for key + * + */ +import validate from 'express-validation'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../../util'; +import models from '../../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + key: Joi.string().max(45).required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('priceConfig.view'), + (req, res, next) => models.PriceConfig.latestRevisionofLatestVersion(req.params.key) + .then((form) => { + if (form == null) { + const apiErr = new Error(`PriceConfig not found for key ${req.params.key} version ${req.params.version}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + res.json(util.wrapResponse(req.id, form)); + return Promise.resolve(); + }) +.catch(next), +]; diff --git a/src/routes/priceConfig/version/get.spec.js b/src/routes/priceConfig/version/get.spec.js new file mode 100644 index 00000000..d76afd60 --- /dev/null +++ b/src/routes/priceConfig/version/get.spec.js @@ -0,0 +1,134 @@ +/** + * Tests for get.js + */ +import chai from 'chai'; +import request from 'supertest'; + +import models from '../../../models'; +import server from '../../../app'; +import testUtil from '../../../tests/util'; + +const should = chai.should(); + +describe('GET a latest version of specific key of PriceConfig', () => { + const priceConfigs = [ + { + key: 'dev', + config: { + test: 'test1', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + config: { + test: 'test2', + }, + version: 2, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + config: { + test: 'test2', + }, + version: 2, + revision: 2, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + config: { + test: 'test3', + }, + version: 1, + revision: 2, + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.PriceConfig.create(priceConfigs[0])) + .then(() => models.PriceConfig.create(priceConfigs[1])) + .then(() => models.PriceConfig.create(priceConfigs[2])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('GET /projects/metadata/priceConfig/dev/versions/{version}', () => { + it('should return 200 and correct version for admin', (done) => { + request(server) + .get('/v4/projects/metadata/priceConfig/dev') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end((err, res) => { + const priceConfig = priceConfigs[2]; + const resJson = res.body.result.content; + + resJson.key.should.be.eql(priceConfig.key); + resJson.config.should.be.eql(priceConfig.config); + resJson.version.should.be.eql(priceConfig.version); + resJson.revision.should.be.eql(priceConfig.revision); + should.exist(resJson.createdAt); + resJson.updatedBy.should.be.eql(priceConfig.updatedBy); + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + done(); + }); + }); + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .get('/v4/projects/metadata/priceConfig/dev') + .expect(403, done); + }); + + it('should return 200 for connect admin', (done) => { + request(server) + .get('/v4/projects/metadata/priceConfig/dev') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for connect manager', (done) => { + request(server) + .get('/v4/projects/metadata/priceConfig/dev') + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for member', (done) => { + request(server) + .get('/v4/projects/metadata/priceConfig/dev') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(200, done); + }); + + it('should return 200 for copilot', (done) => { + request(server) + .get('/v4/projects/metadata/priceConfig/dev') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(200, done); + }); + }); +}); diff --git a/src/routes/priceConfig/version/getVersion.js b/src/routes/priceConfig/version/getVersion.js new file mode 100644 index 00000000..8dd9d17c --- /dev/null +++ b/src/routes/priceConfig/version/getVersion.js @@ -0,0 +1,42 @@ +/** + * API to get a priceConfig for particular version + */ +import validate from 'express-validation'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../../util'; +import models from '../../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + key: Joi.string().max(45).required(), + version: Joi.number().integer().positive().required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('priceConfig.view'), + (req, res, next) => models.PriceConfig.findOne({ + where: { + key: req.params.key, + version: req.params.version, + }, + order: [['revision', 'DESC']], + limit: 1, + attributes: { exclude: ['deletedAt', 'deletedBy'] }, + }) + .then((priceConfig) => { + // Not found + if (!priceConfig) { + const apiErr = new Error(`PriceConfig not found for key ${req.params.key} version ${req.params.version}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + res.json(util.wrapResponse(req.id, priceConfig)); + return Promise.resolve(); + }) + .catch(next), +]; diff --git a/src/routes/priceConfig/version/getVersion.spec.js b/src/routes/priceConfig/version/getVersion.spec.js new file mode 100644 index 00000000..04671910 --- /dev/null +++ b/src/routes/priceConfig/version/getVersion.spec.js @@ -0,0 +1,124 @@ +/** + * Tests for getVersion.js + */ +import chai from 'chai'; +import request from 'supertest'; + +import models from '../../../models'; +import server from '../../../app'; +import testUtil from '../../../tests/util'; + +const should = chai.should(); + +describe('GET a particular version of specific key of PriceConfig', () => { + const priceConfigs = [ + { + key: 'dev', + config: { + test: 'test1', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + config: { + test: 'test2', + }, + version: 2, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + config: { + test: 'test3', + }, + version: 2, + revision: 2, + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.PriceConfig.create(priceConfigs[0])) + .then(() => models.PriceConfig.create(priceConfigs[1])) + .then(() => models.PriceConfig.create(priceConfigs[2])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('GET /projects/metadata/priceConfig/dev/versions/{version}', () => { + it('should return 200 for admin', (done) => { + request(server) + .get('/v4/projects/metadata/priceConfig/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end((err, res) => { + const priceConfig = priceConfigs[0]; + const resJson = res.body.result.content; + + resJson.key.should.be.eql(priceConfig.key); + resJson.config.should.be.eql(priceConfig.config); + resJson.version.should.be.eql(priceConfig.version); + resJson.revision.should.be.eql(priceConfig.revision); + should.exist(resJson.createdAt); + resJson.updatedBy.should.be.eql(priceConfig.updatedBy); + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + done(); + }); + }); + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .get('/v4/projects/metadata/priceConfig/dev/versions/1') + .expect(403, done); + }); + + it('should return 200 for connect admin', (done) => { + request(server) + .get('/v4/projects/metadata/priceConfig/dev') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for connect manager', (done) => { + request(server) + .get('/v4/projects/metadata/priceConfig/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for member', (done) => { + request(server) + .get('/v4/projects/metadata/priceConfig/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(200, done); + }); + + it('should return 200 for copilot', (done) => { + request(server) + .get('/v4/projects/metadata/priceConfig/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(200, done); + }); + }); +}); diff --git a/src/routes/priceConfig/version/list.js b/src/routes/priceConfig/version/list.js new file mode 100644 index 00000000..aa4e9e77 --- /dev/null +++ b/src/routes/priceConfig/version/list.js @@ -0,0 +1,47 @@ +/** + * API to get a priceConfig list + */ +import validate from 'express-validation'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../../util'; +import models from '../../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + key: Joi.string().max(45).required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('priceConfig.view'), + (req, res, next) => models.PriceConfig.findAll({ + where: { + key: req.params.key, + }, + attributes: { exclude: ['deletedAt', 'deletedBy'] }, + }) + .then((priceConfigs) => { + // Not found + if ((!priceConfigs) || (priceConfigs.length === 0)) { + const apiErr = new Error(`PriceConfig not found for key ${req.params.key}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + + const latestPriceConfigs = {}; + priceConfigs.forEach((element) => { + const isNewerRevision = (latestPriceConfigs[element.version] != null) && + (latestPriceConfigs[element.version].revision < element.revision); + if ((latestPriceConfigs[element.version] == null) || isNewerRevision) { + latestPriceConfigs[element.version] = element; + } + }); + res.json(util.wrapResponse(req.id, Object.values(latestPriceConfigs))); + return Promise.resolve(); + }) + .catch(next), +]; diff --git a/src/routes/priceConfig/version/list.spec.js b/src/routes/priceConfig/version/list.spec.js new file mode 100644 index 00000000..2c58aca0 --- /dev/null +++ b/src/routes/priceConfig/version/list.spec.js @@ -0,0 +1,115 @@ +/* eslint-disable quote-props */ +/** + * Tests for list.js + */ +import chai from 'chai'; +import request from 'supertest'; + +import models from '../../../models'; +import server from '../../../app'; +import testUtil from '../../../tests/util'; + +const should = chai.should(); + +describe('LIST priceConfig versions', () => { + const priceConfigs = [ + { + key: 'dev', + config: { + 'test': 'test1', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + config: { + test: 'test2', + }, + version: 2, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.PriceConfig.create(priceConfigs[0])) + .then(() => models.PriceConfig.create(priceConfigs[1])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('GET /projects/metadata/priceConfig/dev/versions/{version}', () => { + it('should return 200 for admin', (done) => { + request(server) + .get('/v4/projects/metadata/priceConfig/dev/versions') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end((err, res) => { + const priceConfig = priceConfigs[0]; + const resJson = res.body.result.content; + resJson.should.have.length(2); + + resJson[0].key.should.be.eql(priceConfig.key); + resJson[0].config.should.be.eql(priceConfig.config); + resJson[0].version.should.be.eql(priceConfig.version); + resJson[0].revision.should.be.eql(priceConfig.revision); + should.exist(resJson[0].createdAt); + resJson[0].updatedBy.should.be.eql(priceConfig.updatedBy); + should.exist(resJson[0].updatedAt); + should.not.exist(resJson[0].deletedBy); + should.not.exist(resJson[0].deletedAt); + done(); + }); + }); + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .get('/v4/projects/metadata/priceConfig/dev/versions') + .expect(403, done); + }); + + it('should return 200 for connect admin', (done) => { + request(server) + .get('/v4/projects/metadata/priceConfig/dev/versions') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for connect manager', (done) => { + request(server) + .get('/v4/projects/metadata/priceConfig/dev/versions') + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for member', (done) => { + request(server) + .get('/v4/projects/metadata/priceConfig/dev/versions') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(200, done); + }); + + it('should return 200 for copilot', (done) => { + request(server) + .get('/v4/projects/metadata/priceConfig/dev/versions') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(200, done); + }); + }); +}); diff --git a/src/routes/priceConfig/version/update.js b/src/routes/priceConfig/version/update.js new file mode 100644 index 00000000..39753f07 --- /dev/null +++ b/src/routes/priceConfig/version/update.js @@ -0,0 +1,74 @@ +/* eslint-disable no-trailing-spaces */ +/** + * API to add a project type + */ +import config from 'config'; +import validate from 'express-validation'; +import _ from 'lodash'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../../util'; +import models from '../../../models'; + +const permissions = tcMiddleware.permissions; + +const schema = { + params: { + version: Joi.number().integer().positive().required(), + key: Joi.string().max(45).required(), + }, + body: { + param: Joi.object().keys({ + config: Joi.object().required(), + + createdAt: Joi.any().strip(), + updatedAt: Joi.any().strip(), + deletedAt: Joi.any().strip(), + createdBy: Joi.any().strip(), + updatedBy: Joi.any().strip(), + deletedBy: Joi.any().strip(), + }).required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('priceConfig.create'), + (req, res, next) => { + models.sequelize.transaction(() => models.PriceConfig.findAll({ + where: { + key: req.params.key, + version: req.params.version, + }, + order: [['revision', 'DESC']], + }).then((priceConfigs) => { + if (priceConfigs.length >= config.get('MAX_REVISION_NUMBER')) { + return models.PriceConfig.deleteOldestRevision(req.authUser.userId, req.params.key, req.params.version) + .then(() => Promise.resolve(priceConfigs[0])); + } else if (priceConfigs.length === 0) { + const apiErr = new Error(`PriceConfig not found for key ${req.params.key} version ${req.params.version}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + return Promise.resolve(priceConfigs[0]); + }) + .then((priceConfig) => { + const revisison = priceConfig.revision + 1; + const entity = { + version: req.params.version, + revision: revisison, + createdBy: req.authUser.userId, + updatedBy: req.authUser.userId, + key: req.params.key, + config: req.body.param.config, + }; + return models.PriceConfig.create(entity); + }) + .then((createdEntity) => { + // Omit deletedAt, deletedBy + res.status(201).json(util.wrapResponse( + req.id, _.omit(createdEntity.toJSON(), 'deletedAt', 'deletedBy'), 1, 201)); + }) + .catch(next)); + }, +]; diff --git a/src/routes/priceConfig/version/update.spec.js b/src/routes/priceConfig/version/update.spec.js new file mode 100644 index 00000000..3a9cad6e --- /dev/null +++ b/src/routes/priceConfig/version/update.spec.js @@ -0,0 +1,113 @@ +/* eslint-disable no-unused-expressions */ +/** + * Tests for create.js + */ +import chai from 'chai'; +import request from 'supertest'; +import _ from 'lodash'; +import server from '../../../app'; +import testUtil from '../../../tests/util'; +import models from '../../../models'; + +const should = chai.should(); + +describe('UPDATE PriceConfig version', () => { + const priceConfigs = [ + { + key: 'dev', + config: { + test: 'test1', + }, + version: 1, + revision: 1, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'dev', + config: { + test: 'test2', + }, + version: 1, + revision: 2, + createdBy: 1, + updatedBy: 1, + }, + ]; + + beforeEach(() => testUtil.clearDb() + .then(() => models.PriceConfig.create(priceConfigs[0])) + .then(() => models.PriceConfig.create(priceConfigs[1])) + .then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('Post /projects/metadata/priceConfig/{key}/versions/{version}', () => { + const body = { + param: { + config: { + 'test create': 'test create', + }, + }, + }; + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .patch('/v4/projects/metadata/priceConfig/dev/versions/1') + .send(body) + .expect(403, done); + }); + + it('should return 422 if missing config', (done) => { + const invalidBody = { + param: _.assign({}, body.param, { + config: undefined, + }), + }; + request(server) + .patch('/v4/projects/metadata/priceConfig/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(invalidBody) + .expect('Content-Type', /json/) + .expect(422, done); + }); + + it('should return 201 for admin', (done) => { + request(server) + .patch('/v4/projects/metadata/priceConfig/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + const resJson = res.body.result.content; + should.exist(resJson.id); + resJson.config.should.be.eql(body.param.config); + resJson.key.should.be.eql('dev'); + resJson.revision.should.be.eql(3); + resJson.version.should.be.eql(1); + resJson.createdBy.should.be.eql(40051333); // admin + should.exist(resJson.createdAt); + resJson.updatedBy.should.be.eql(40051333); // admin + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + done(); + }); + }); + + it('should return 403 for member', (done) => { + request(server) + .patch('/v4/projects/metadata/priceConfig/dev/versions/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send(body) + .expect(403, done); + }); + }); +}); diff --git a/src/routes/projectTemplates/create.js b/src/routes/projectTemplates/create.js index 12f7c52e..12a0dfd2 100644 --- a/src/routes/projectTemplates/create.js +++ b/src/routes/projectTemplates/create.js @@ -22,8 +22,11 @@ const schema = { question: Joi.string().max(255).required(), info: Joi.string().max(255).required(), aliases: Joi.array().required(), - scope: Joi.object().required(), - phases: Joi.object().required(), + scope: Joi.object().optional().allow(null), + phases: Joi.object().optional().allow(null), + form: Joi.object().optional().allow(null), + planConfig: Joi.object().optional().allow(null), + priceConfig: Joi.object().optional().allow(null), disabled: Joi.boolean().optional(), hidden: Joi.boolean().optional(), createdAt: Joi.any().strip(), @@ -41,17 +44,68 @@ module.exports = [ permissions('projectTemplate.create'), fieldLookupValidation(models.ProjectType, 'key', 'body.param.category', 'Category'), (req, res, next) => { - const entity = _.assign(req.body.param, { - createdBy: req.authUser.userId, - updatedBy: req.authUser.userId, - }); + const param = req.body.param; + const { form, priceConfig, planConfig } = param; - return models.ProjectTemplate.create(entity) - .then((createdEntity) => { - // Omit deletedAt, deletedBy - res.status(201).json(util.wrapResponse( - req.id, _.omit(createdEntity.toJSON(), 'deletedAt', 'deletedBy'), 1, 201)); - }) - .catch(next); + const checkModel = (keyInfo, modelName, model) => { + let errorMessage = ''; + if (keyInfo == null) { + return Promise.resolve(null); + } + if ((keyInfo.version != null) && (keyInfo.key != null)) { + errorMessage = `${modelName} with key ${keyInfo.key} and version ${keyInfo.version}` + + ' referred in the project template is not found'; + return (model.findOne({ + where: { + key: keyInfo.key, + version: keyInfo.version, + }, + })).then((record) => { + if (record == null) { + return Promise.resolve(errorMessage); + } + return Promise.resolve(null); + }); + } else if ((keyInfo.version == null) && (keyInfo.key != null)) { + errorMessage = `${modelName} with key ${keyInfo.key}` + + ' referred in the project template is not found'; + return model.findOne({ + where: { + key: keyInfo.key, + }, + }).then((record) => { + if (record == null) { + return Promise.resolve(errorMessage); + } + return Promise.resolve(null); + }); + } + return Promise.resolve(null); + }; + + return Promise.all([ + checkModel(form, 'Form', models.Form, next), + checkModel(priceConfig, 'PriceConfig', models.PriceConfig, next), + checkModel(planConfig, 'PlanConfig', models.PlanConfig, next), + ]) + .then((errorMessages) => { + const errorMessage = errorMessages.find(e => e && e.length > 0); + if (errorMessage) { + const apiErr = new Error(errorMessage); + apiErr.status = 422; + throw apiErr; + } + const entity = _.assign(req.body.param, { + createdBy: req.authUser.userId, + updatedBy: req.authUser.userId, + }); + + return models.ProjectTemplate.create(entity) + .then((createdEntity) => { + // Omit deletedAt, deletedBy + res.status(201).json(util.wrapResponse( + req.id, _.omit(createdEntity.toJSON(), 'deletedAt', 'deletedBy'), 1, 201)); + }); + }).catch(next); }, ]; diff --git a/src/routes/projectTemplates/create.spec.js b/src/routes/projectTemplates/create.spec.js index 51989b4c..9d9fd594 100644 --- a/src/routes/projectTemplates/create.spec.js +++ b/src/routes/projectTemplates/create.spec.js @@ -68,6 +68,46 @@ describe('CREATE project template', () => { }, }; + const newModelBody = { + param: { + name: 'template 1', + key: 'key 1', + category: 'generic', + icon: 'http://example.com/icon1.ico', + question: 'question 1', + info: 'info 1', + aliases: ['key-1', 'key_1'], + disabled: true, + hidden: true, + form: { + scope1: { + subScope1A: 1, + subScope1B: 2, + }, + scope2: [1, 2, 3], + }, + priceConfig: { + first: '$800', + }, + planConfig: { + phase1: { + name: 'phase 1', + details: { + anyDetails: 'any details 1', + }, + others: ['others 11', 'others 12'], + }, + phase2: { + name: 'phase 2', + details: { + anyDetails: 'any details 2', + }, + others: ['others 21', 'others 22'], + }, + }, + }, + }; + it('should return 403 if user is not authenticated', (done) => { request(server) .post('/v4/projects/metadata/projectTemplates') @@ -180,6 +220,40 @@ describe('CREATE project template', () => { }); }); + it('should return 201 with new model', (done) => { + request(server) + .post('/v4/projects/metadata/projectTemplates') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(newModelBody) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + const resJson = res.body.result.content; + should.exist(resJson.id); + resJson.name.should.be.eql(newModelBody.param.name); + resJson.key.should.be.eql(newModelBody.param.key); + resJson.category.should.be.eql(newModelBody.param.category); + resJson.disabled.should.be.eql(true); + resJson.hidden.should.be.eql(true); + should.not.exist(resJson.scope); + should.not.exist(resJson.phase); + resJson.form.should.be.eql(newModelBody.param.form); + resJson.planConfig.should.be.eql(newModelBody.param.planConfig); + resJson.priceConfig.should.be.eql(newModelBody.param.priceConfig); + + resJson.createdBy.should.be.eql(40051333); // admin + should.exist(resJson.createdAt); + resJson.updatedBy.should.be.eql(40051333); // admin + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + + done(); + }); + }); + it('should return 201 for connect admin', (done) => { request(server) .post('/v4/projects/metadata/projectTemplates') diff --git a/src/routes/projectTemplates/update.js b/src/routes/projectTemplates/update.js index 10d55d70..4991a310 100644 --- a/src/routes/projectTemplates/update.js +++ b/src/routes/projectTemplates/update.js @@ -25,8 +25,11 @@ const schema = { question: Joi.string().max(255), info: Joi.string().max(255), aliases: Joi.array(), - scope: Joi.object(), - phases: Joi.object(), + scope: Joi.object().optional().allow(null), + phases: Joi.object().optional().allow(null), + form: Joi.object().optional().allow(null), + planConfig: Joi.object().optional().allow(null), + priceConfig: Joi.object().optional().allow(null), disabled: Joi.boolean().optional(), hidden: Joi.boolean().optional(), createdAt: Joi.any().strip(), @@ -44,41 +47,94 @@ module.exports = [ permissions('projectTemplate.edit'), fieldLookupValidation(models.ProjectType, 'key', 'body.param.category', 'Category'), (req, res, next) => { - const entityToUpdate = _.assign(req.body.param, { - updatedBy: req.authUser.userId, - }); + const param = req.body.param; + const { form, priceConfig, planConfig } = param; - return models.ProjectTemplate.findOne({ - where: { - deletedAt: { $eq: null }, - id: req.params.templateId, - }, - attributes: { exclude: ['deletedAt', 'deletedBy'] }, - }) - .then((projectTemplate) => { - // Not found - if (!projectTemplate) { - const apiErr = new Error(`Project template not found for template id ${req.params.templateId}`); - apiErr.status = 404; - return Promise.reject(apiErr); - } + const checkModel = (keyInfo, modelName, model) => { + let errorMessage = ''; + if (keyInfo == null) { + return Promise.resolve(null); + } + if ((keyInfo.version != null) && (keyInfo.key != null)) { + errorMessage = `${modelName} with key ${keyInfo.key} and version ${keyInfo.version}` + + ' referred in the project template is not found'; + return (model.findOne({ + where: { + key: keyInfo.key, + version: keyInfo.version, + }, + })).then((record) => { + if (record == null) { + return Promise.resolve(errorMessage); + } + return Promise.resolve(null); + }); + } else if ((keyInfo.version == null) && (keyInfo.key != null)) { + errorMessage = `${modelName} with key ${keyInfo.key}` + + ' referred in the project template is not found'; + return model.findOne({ + where: { + key: keyInfo.key, + }, + }).then((record) => { + if (record == null) { + return Promise.resolve(errorMessage); + } + return Promise.resolve(null); + }); + } + return Promise.resolve(null); + }; - // Merge JSON fields - entityToUpdate.scope = util.mergeJsonObjects( - projectTemplate.scope, - entityToUpdate.scope, - ['priceConfig', 'addonPriceConfig', 'preparedConditions', 'buildingBlocks'], - ); - entityToUpdate.phases = util.mergeJsonObjects(projectTemplate.phases, entityToUpdate.phases); - // removes null phase templates - entityToUpdate.phases = _.omitBy(entityToUpdate.phases, _.isNull); + return Promise.all([ + checkModel(form, 'Form', models.Form, next), + checkModel(priceConfig, 'PriceConfig', models.PriceConfig, next), + checkModel(planConfig, 'PlanConfig', models.PlanConfig, next), + ]) + .then((errorMessages) => { + const errorMessage = errorMessages.find(e => e && e.length > 0); + if (errorMessage) { + const apiErr = new Error(errorMessage); + apiErr.status = 422; + throw apiErr; + } + const entityToUpdate = _.assign(req.body.param, { + updatedBy: req.authUser.userId, + }); - return projectTemplate.update(entityToUpdate); - }) - .then((projectTemplate) => { - res.json(util.wrapResponse(req.id, projectTemplate)); - return Promise.resolve(); - }) - .catch(next); + return models.ProjectTemplate.findOne({ + where: { + deletedAt: { $eq: null }, + id: req.params.templateId, + }, + attributes: { exclude: ['deletedAt', 'deletedBy'] }, + + }) + .then((projectTemplate) => { + // Not found + if (!projectTemplate) { + const apiErr = new Error(`Project template not found for template id ${req.params.templateId}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + + // Merge JSON fields + entityToUpdate.scope = util.mergeJsonObjects( + projectTemplate.scope, + entityToUpdate.scope, + ['priceConfig', 'addonPriceConfig', 'preparedConditions', 'buildingBlocks'], + ); + entityToUpdate.phases = util.mergeJsonObjects(projectTemplate.phases, entityToUpdate.phases); + // removes null phase templates + entityToUpdate.phases = _.omitBy(entityToUpdate.phases, _.isNull); + + return projectTemplate.update(entityToUpdate); + }) + .then((projectTemplate) => { + res.json(util.wrapResponse(req.id, projectTemplate)); + return Promise.resolve(); + }) + .catch(next); + }).catch(next); }, ]; diff --git a/src/routes/projectTemplates/update.spec.js b/src/routes/projectTemplates/update.spec.js index ff051381..f62a8252 100644 --- a/src/routes/projectTemplates/update.spec.js +++ b/src/routes/projectTemplates/update.spec.js @@ -48,6 +48,7 @@ describe('UPDATE project template', () => { updatedBy: 1, }; + let templateId; beforeEach(() => testUtil.clearDb() @@ -117,6 +118,46 @@ describe('UPDATE project template', () => { }, }; + const newModelBody = { + param: { + name: 'template 1', + key: 'key 1', + category: 'generic', + icon: 'http://example.com/icon1.ico', + question: 'question 1', + info: 'info 1', + aliases: ['key-1', 'key_1'], + disabled: true, + hidden: true, + form: { + scope1: { + subScope1A: 1, + subScope1B: 2, + }, + scope2: [1, 2, 3], + }, + priceConfig: { + first: '$800', + }, + planConfig: { + phase1: { + name: 'phase 1', + details: { + anyDetails: 'any details 1', + }, + others: ['others 11', 'others 12'], + }, + phase2: { + name: 'phase 2', + details: { + anyDetails: 'any details 2', + }, + others: ['others 21', 'others 22'], + }, + }, + }, + }; + it('should return 403 if user is not authenticated', (done) => { request(server) .patch(`/v4/projects/metadata/projectTemplates/${templateId}`) @@ -254,6 +295,37 @@ describe('UPDATE project template', () => { }); }); + it('should return 200 for new model', (done) => { + request(server) + .patch(`/v4/projects/metadata/projectTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(newModelBody) + .expect(200) + .end((err, res) => { + const resJson = res.body.result.content; + resJson.id.should.be.eql(templateId); + resJson.name.should.be.eql(newModelBody.param.name); + resJson.key.should.be.eql(newModelBody.param.key); + resJson.category.should.be.eql(newModelBody.param.category); + resJson.form.should.be.eql(newModelBody.param.form); + resJson.priceConfig.should.be.eql(newModelBody.param.priceConfig); + resJson.planConfig.should.be.eql(newModelBody.param.planConfig); + + resJson.disabled.should.be.eql(true); + resJson.hidden.should.be.eql(true); + resJson.createdBy.should.be.eql(template.createdBy); + should.exist(resJson.createdAt); + resJson.updatedBy.should.be.eql(40051333); // admin + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + + done(); + }); + }); + it('should return 200 for connect admin', (done) => { request(server) .patch(`/v4/projects/metadata/projectTemplates/${templateId}`) diff --git a/src/routes/projectTemplates/upgrade.js b/src/routes/projectTemplates/upgrade.js new file mode 100644 index 00000000..f1eb4625 --- /dev/null +++ b/src/routes/projectTemplates/upgrade.js @@ -0,0 +1,151 @@ +/** + * API to add a new version of form + */ +import validate from 'express-validation'; +import _ from 'lodash'; +import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../util'; +import models from '../../models'; + +const permissions = tcMiddleware.permissions; + + +const schema = { + body: { + param: Joi.object().keys({ + form: Joi.object().keys({ + version: Joi.number().integer().positive().required(), + key: Joi.string().required(), + }).optional(), + priceConfig: Joi.object().keys({ + version: Joi.number().integer().positive().required(), + key: Joi.string().required(), + }).optional(), + planConfig: Joi.object().keys({ + version: Joi.number().integer().positive().required(), + key: Joi.string().required(), + }).optional(), + }).optional(), + }, +}; + + +module.exports = [ + permissions('projectTemplate.upgrade'), + validate(schema), + (req, res, next) => { + models.sequelize.transaction(() => models.ProjectTemplate.findOne({ + where: { + id: req.params.templateId, + }, + // eslint-disable-next-line consistent-return + }).then(async (pt) => { + if (pt == null) { + const apiErr = new Error(`project template not found for id ${req.body.param.templateId}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + if ((pt.scope == null) || (pt.phases == null)) { + const apiErr = new Error('Current project template\'s scope or phases is null'); + apiErr.status = 422; + return Promise.reject(apiErr); + } + + const checkModel = (keyInfo, modelName, model) => { + let errorMessage = ''; + errorMessage = `${modelName} with key ${keyInfo.key} and version ${keyInfo.version}` + + ' referred in param is not found'; + return (model.findOne({ + where: { + key: keyInfo.key, + version: keyInfo.version, + }, + })).then((record) => { + if (record == null) { + return Promise.resolve(errorMessage); + } + return Promise.resolve(null); + }); + }; + + const reportError = (errorMessage) => { + const apiErr = new Error(errorMessage); + apiErr.status = 422; + return Promise.reject(apiErr).catch(next); + }; + + // get form field + let newForm = {}; + if (req.body.param.form == null) { + const scope = { + sections: pt.scope ? pt.scope.sections : null, + wizard: pt.scope ? pt.scope.wizard : null, + preparedConditions: pt.scope ? pt.scope.preparedConditions : null, + }; + const form = await models.Form.createNewVersion(pt.key, scope, req.authUser.userId); + newForm = { + version: form.version, + key: pt.key, + }; + } else { + newForm = req.body.param.form; + const err = await checkModel(newForm, 'Form', models.Form); + if (err != null) { + reportError(err); + } + } + // get price config field + let newPriceConfig = {}; + if (req.body.param.priceConfig == null) { + const config = {}; + if (pt.scope) { + Object.keys(pt.scope).filter(key => (key !== 'wizard') && (key !== 'sections')).forEach((key) => { + config[key] = pt.scope[key]; + }); + } + const priceConfig = await models.PriceConfig.createNewVersion(pt.key, config, req.authUser.userId); + newPriceConfig = { + version: priceConfig.version, + key: pt.key, + }; + } else { + newPriceConfig = req.body.param.priceConfig; + const err = await checkModel(newPriceConfig, 'PriceConfig', models.PriceConfig); + if (err != null) { + reportError(err); + } + } + // get plan config field + let newPlanConfig = {}; + if (req.body.param.planConfig == null) { + const planConfig = await models.PlanConfig.createNewVersion(pt.key, pt.phases, req.authUser.userId); + newPlanConfig = { + version: planConfig.version, + key: pt.key, + }; + } else { + newPlanConfig = req.body.param.planConfig; + const err = await checkModel(newPlanConfig, 'PlanConfig', models.PlanConfig); + if (err != null) { + reportError(err); + } + } + + const updateInfo = { + scope: null, + phases: null, + form: newForm, + priceConfig: newPriceConfig, + planConfig: newPlanConfig, + updatedBy: req.authUser.userId, + }; + + const newPt = await pt.update(updateInfo); + + res.status(201).json(util.wrapResponse( + req.id, _.omit(newPt.toJSON(), 'deletedAt', 'deletedBy'), 1, 201)); + }) + .catch(next)); + }, +]; diff --git a/src/routes/projectTemplates/upgrade.spec.js b/src/routes/projectTemplates/upgrade.spec.js new file mode 100644 index 00000000..5688557d --- /dev/null +++ b/src/routes/projectTemplates/upgrade.spec.js @@ -0,0 +1,287 @@ +/** + * Tests for get.js + */ +import chai from 'chai'; +import request from 'supertest'; + +import models from '../../models'; +import server from '../../app'; +import testUtil from '../../tests/util'; + +const should = chai.should(); + +describe('Upgrade project template', () => { + const template = { + name: 'template 1', + key: 'key 1', + category: 'generic', + icon: 'http://example.com/icon1.ico', + question: 'question 1', + info: 'info 1', + aliases: ['key-1', 'key_1'], + disabled: true, + hidden: true, + scope: { + scope1: { + subScope1A: 1, + subScope1B: 2, + }, + scope2: [1, 2, 3], + }, + phases: { + phase1: { + name: 'phase 1', + details: { + anyDetails: 'any details 1', + }, + others: ['others 11', 'others 12'], + }, + phase2: { + name: 'phase 2', + details: { + anyDetails: 'any details 2', + }, + others: ['others 21', 'others 22'], + }, + }, + createdBy: 1, + updatedBy: 1, + }; + + let templateId; + + beforeEach(() => testUtil.clearDb() + .then(() => models.ProjectType.bulkCreate([ + { + key: 'generic', + displayName: 'Generic', + icon: 'http://example.com/icon1.ico', + question: 'question 1', + info: 'info 1', + aliases: ['key-1', 'key_1'], + metadata: {}, + createdBy: 1, + updatedBy: 1, + }, + { + key: 'concrete', + displayName: 'Concrete', + icon: 'http://example.com/icon1.ico', + question: 'question 2', + info: 'info 2', + aliases: ['key-2', 'key_2'], + metadata: {}, + createdBy: 1, + updatedBy: 1, + }, + ])) + .then(() => { + models.Form.bulkCreate([ + { + key: 'dev', + version: 1, + revision: 1, + scope: ['key-1', 'key_1'], + createdBy: 1, + updatedBy: 1, + }, + ]); + }) + .then(() => { + models.PriceConfig.bulkCreate([ + { + key: 'dev', + version: 1, + revision: 1, + config: ['key-1', 'key_1'], + createdBy: 1, + updatedBy: 1, + }, + ]); + }) + .then(() => { + models.PlanConfig.bulkCreate([ + { + key: 'dev', + version: 1, + revision: 1, + phases: ['key-1', 'key_1'], + createdBy: 1, + updatedBy: 1, + }, + ]); + }) + .then(() => models.ProjectTemplate.create(template)) + .then((createdTemplate) => { + templateId = createdTemplate.id; + return Promise.resolve(); + }), + ); + after(testUtil.clearDb); + + describe('POST /projects/metadata/projectTemplates/{templateId}/upgrade', () => { + const body = { + param: { + form: { + key: 'dev', + version: 1, + }, + priceConfig: { + key: 'dev', + version: 1, + }, + planConfig: { + key: 'dev', + version: 1, + }, + }, + }; + + const emptyBody = { + param: { + }, + }; + + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .post(`/v4/projects/metadata/projectTemplates/${templateId}/upgrade`) + .send(body) + .expect(403, done); + }); + + it('should return 403 for member', (done) => { + request(server) + .post(`/v4/projects/metadata/projectTemplates/${templateId}/upgrade`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send(body) + .expect(403, done); + }); + + it('should return 403 for copilot', (done) => { + request(server) + .post(`/v4/projects/metadata/projectTemplates/${templateId}/upgrade`) + .send(body) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(403, done); + }); + + it('should return 403 for connect manager', (done) => { + request(server) + .post(`/v4/projects/metadata/projectTemplates/${templateId}/upgrade`) + .send(body) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(403, done); + }); + + + it('should return 404 for non-existed template', (done) => { + request(server) + .post('/v4/projects/metadata/projectTemplates/123/upgrade') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(body) + .expect(404, done); + }); + + it('should return 404 for deleted template', (done) => { + models.ProjectTemplate.destroy({ where: { id: templateId } }) + .then(() => { + request(server) + .post(`/v4/projects/metadata/projectTemplates/${templateId}/upgrade`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(body) + .expect(404, done); + }); + }); + + it('should return 200 for admin', (done) => { + request(server) + .post(`/v4/projects/metadata/projectTemplates/${templateId}/upgrade`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(body) + .expect(200) + .end((err, res) => { + const resJson = res.body.result.content; + resJson.id.should.be.eql(templateId); + + should.not.exist(resJson.scope); + should.not.exist(resJson.phases); + + resJson.form.should.be.eql({ + key: 'dev', + version: 1, + }); + + resJson.priceConfig.should.be.eql({ + key: 'dev', + version: 1, + }); + + resJson.planConfig.should.be.eql({ + key: 'dev', + version: 1, + }); + + should.exist(resJson.createdAt); + resJson.updatedBy.should.be.eql(40051333); // admin + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + + done(); + }); + }); + + it('should create new version of model if param not given model key and version', (done) => { + request(server) + .post(`/v4/projects/metadata/projectTemplates/${templateId}/upgrade`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(emptyBody) + .expect(200) + .end((err, res) => { + const resJson = res.body.result.content; + + should.not.exist(resJson.scope); + should.not.exist(resJson.phases); + + resJson.form.should.be.eql({ + key: 'key 1', + version: 1, + }); + + resJson.priceConfig.should.be.eql({ + key: 'key 1', + version: 1, + }); + + resJson.planConfig.should.be.eql({ + key: 'key 1', + version: 1, + }); + + resJson.createdBy.should.be.eql(template.createdBy); + should.exist(resJson.createdAt); + resJson.updatedBy.should.be.eql(40051333); // admin + should.exist(resJson.updatedAt); + should.not.exist(resJson.deletedBy); + should.not.exist(resJson.deletedAt); + + done(); + }); + }); + }); +}); diff --git a/src/routes/timelines/create.js b/src/routes/timelines/create.js index 44ec10be..a19865b6 100644 --- a/src/routes/timelines/create.js +++ b/src/routes/timelines/create.js @@ -18,7 +18,7 @@ const schema = { body: { param: Joi.object().keys({ id: Joi.any().strip(), - name: Joi.string().max(255).required(), + name: Joi.string().max(45).required(), description: Joi.string().max(255), startDate: Joi.date().required(), endDate: Joi.date().min(Joi.ref('startDate')).allow(null), diff --git a/swagger.yaml b/swagger.yaml index fcd02b60..9f877904 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -1,24 +1,20 @@ -swagger: "2.0" +swagger: '2.0' info: - version: "v4" - title: "Projects API" -# during production,should point to your server machine -host: localhost:3000 -basePath: "/v4" -# during production, should use https + version: v4 + title: Projects API +host: 'localhost:3000' +basePath: /v4 schemes: -- "http" + - http produces: -- application/json + - application/json consumes: -- application/json - + - application/json securityDefinitions: Bearer: type: apiKey name: Authorization in: header - paths: /projects: get: @@ -29,21 +25,21 @@ paths: - Bearer: [] description: Retrieve projects that match the filter responses: - '422': - description: Invalid input + '200': + description: A list of projects schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ProjectListResponse' '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" - '200': - description: A list of projects + $ref: '#/definitions/ErrorModel' + '422': + description: Invalid input schema: - $ref: "#/definitions/ProjectListResponse" + $ref: '#/definitions/ErrorModel' parameters: - - $ref: "#/parameters/offsetParam" - - $ref: "#/parameters/limitParam" + - $ref: '#/parameters/offsetParam' + - $ref: '#/parameters/limitParam' - name: filter required: true type: string @@ -61,8 +57,9 @@ paths: - manager - name: sort required: false - description: | - sort projects by status, name, type, createdAt, updatedAt. Default is createdAt asc + description: > + sort projects by status, name, type, createdAt, updatedAt. Default + is createdAt asc in: query type: string post: @@ -77,135 +74,140 @@ paths: schema: $ref: '#/definitions/NewProjectBodyParam' responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" '201': description: Returns the newly created project schema: - $ref: "#/definitions/ProjectResponse" + $ref: '#/definitions/ProjectResponse' + '403': + description: No permission or wrong token + schema: + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" - - /projects/{projectId}: + $ref: '#/definitions/ErrorModel' + '/projects/{projectId}': get: description: Retrieve project by id security: - Bearer: [] responses: - '404': - description: Not found + '200': + description: a project schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ProjectResponse' '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" - '200': - description: a project + $ref: '#/definitions/ErrorModel' + '404': + description: Not found schema: - $ref: "#/definitions/ProjectResponse" + $ref: '#/definitions/ErrorModel' parameters: - - $ref: "#/parameters/projectIdParam" + - $ref: '#/parameters/projectIdParam' - name: fields required: false type: string in: query - description: | + description: > Comma separated list of project fields to return. - Can also specify project_members, attachments to get project members and project attachments. - Sub fields of project members and project attachments are also allowed. - operationId: getProject + Can also specify project_members, attachments to get project members + and project attachments. + + Sub fields of project members and project attachments are also + allowed. + operationId: getProject patch: operationId: updateProject security: - Bearer: [] - description: Update a project that user has access to. Managers and admin are able to pull out a project from cancelled state. + description: >- + Update a project that user has access to. Managers and admin are able to + pull out a project from cancelled state. responses: + '200': + description: >- + Successfully updated project. Returns original and updated project + object + schema: + $ref: '#/definitions/UpdateProjectResponse' '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '404': description: Not found schema: - $ref: "#/definitions/ErrorModel" - '200': - description: Successfully updated project. Returns original and updated project object - schema: - $ref: "#/definitions/UpdateProjectResponse" + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' default: description: error payload schema: $ref: '#/definitions/ErrorModel' parameters: - - $ref: "#/parameters/projectIdParam" + - $ref: '#/parameters/projectIdParam' - name: body in: body required: true - description: Only specify those properties that needs to be updated. `cancelReason` is mandatory if status is cancelled + description: >- + Only specify those properties that needs to be updated. + `cancelReason` is mandatory if status is cancelled schema: - $ref: "#/definitions/ProjectBodyParam" - + $ref: '#/definitions/ProjectBodyParam' delete: description: remove an existing project security: - Bearer: [] parameters: - - $ref: "#/parameters/projectIdParam" + - $ref: '#/parameters/projectIdParam' responses: + '204': + description: Project successfully removed '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '404': description: If project is not found schema: - $ref: "#/definitions/ErrorModel" - '204': - description: Project successfully removed - - /projects/{projectId}/attachments: + $ref: '#/definitions/ErrorModel' + '/projects/{projectId}/attachments': post: description: add a new project attachment security: - Bearer: [] parameters: - - $ref: "#/parameters/projectIdParam" + - $ref: '#/parameters/projectIdParam' - in: body name: body required: true schema: $ref: '#/definitions/NewProjectAttachmentBodyParam' responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" '201': description: Returns the newly created project attachment schema: - $ref: "#/definitions/NewProjectAttachmentResponse" + $ref: '#/definitions/NewProjectAttachmentResponse' + '403': + description: No permission or wrong token + schema: + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" - - /projects/{projectId}/attachments/{id}: + $ref: '#/definitions/ErrorModel' + '/projects/{projectId}/attachments/{id}': patch: description: Update an existing attachment security: - Bearer: [] parameters: - - $ref: "#/parameters/projectIdParam" + - $ref: '#/parameters/projectIdParam' - in: path name: id required: true @@ -218,137 +220,137 @@ paths: schema: $ref: '#/definitions/NewProjectAttachmentBodyParam' responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" '201': description: Returns the newly created project schema: - $ref: "#/definitions/NewProjectAttachmentResponse" + $ref: '#/definitions/NewProjectAttachmentResponse' + '403': + description: No permission or wrong token + schema: + $ref: '#/definitions/ErrorModel' '404': description: If project attachment is not found schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' delete: description: remove an existing attachment security: - Bearer: [] parameters: - - $ref: "#/parameters/projectIdParam" + - $ref: '#/parameters/projectIdParam' - in: path name: id required: true description: The id of attachment to delete type: integer responses: + '204': + description: Attachment successfully removed '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '404': description: If attachment is not found schema: - $ref: "#/definitions/ErrorModel" - '204': - description: Attachment successfully removed - - /projects/{projectId}/members: + $ref: '#/definitions/ErrorModel' + '/projects/{projectId}/members': post: description: add a new project member security: - Bearer: [] parameters: - - $ref: "#/parameters/projectIdParam" + - $ref: '#/parameters/projectIdParam' - in: body name: body required: true schema: $ref: '#/definitions/NewProjectMemberBodyParam' responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" '201': description: Returns the newly created project schema: - $ref: "#/definitions/NewProjectMemberResponse" + $ref: '#/definitions/NewProjectMemberResponse' + '403': + description: No permission or wrong token + schema: + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" - - /projects/{projectId}/members/{id}: + $ref: '#/definitions/ErrorModel' + '/projects/{projectId}/members/{id}': delete: description: Delete a project member security: - Bearer: [] parameters: - - $ref: "#/parameters/projectIdParam" + - $ref: '#/parameters/projectIdParam' - in: path name: id required: true type: integer - responses: + '204': + description: Member successfully removed '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" - '204': - description: Member successfully removed + $ref: '#/definitions/ErrorModel' patch: - security: - - Bearer: [] - description: Support editing project member roles & primary option. - responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" - '404': - description: Not found - schema: - $ref: "#/definitions/ErrorModel" - '200': - description: Successfully updated project member. Returns entire project member object - schema: - $ref: "#/definitions/UpdateProjectMemberResponse" - '422': - description: Invalid input - schema: - $ref: "#/definitions/ErrorModel" - default: - description: error payload - schema: - $ref: '#/definitions/ErrorModel' - parameters: - - $ref: "#/parameters/projectIdParam" - - in: path - name: id - required: true - type: integer - - name: body - in: body - required: true - schema: - $ref: "#/definitions/UpdateProjectMemberBodyParam" - - /projects/{projectId}/phases: + security: + - Bearer: [] + description: Support editing project member roles & primary option. + responses: + '200': + description: >- + Successfully updated project member. Returns entire project member + object + schema: + $ref: '#/definitions/UpdateProjectMemberResponse' + '403': + description: No permission or wrong token + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: Not found + schema: + $ref: '#/definitions/ErrorModel' + '422': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + default: + description: error payload + schema: + $ref: '#/definitions/ErrorModel' + parameters: + - $ref: '#/parameters/projectIdParam' + - in: path + name: id + required: true + type: integer + - name: body + in: body + required: true + schema: + $ref: '#/definitions/UpdateProjectMemberBodyParam' + '/projects/{projectId}/phases': parameters: - - $ref: "#/parameters/projectIdParam" + - $ref: '#/parameters/projectIdParam' get: tags: - phase operationId: findProjectPhases security: - Bearer: [] - description: Retrieve all project phases. All users who can edit project can access this endpoint. + description: >- + Retrieve all project phases. All users who can edit project can access + this endpoint. parameters: - name: fields required: false @@ -358,27 +360,30 @@ paths: Comma separated list of project phase fields to return. - name: sort required: false - description: | - sort project phases by startDate, endDate, status, order. Default is startDate asc + description: > + sort project phases by startDate, endDate, status, order. Default is + startDate asc in: query type: string responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" '200': description: A list of project phases schema: - $ref: "#/definitions/ProjectPhaseListResponse" + $ref: '#/definitions/ProjectPhaseListResponse' + '403': + description: No permission or wrong token + schema: + $ref: '#/definitions/ErrorModel' post: tags: - phase operationId: addProjectPhase security: - Bearer: [] - description: Create a project phase. - It also updates the `order` field of all other phases in the same project which have `order` greater than or equal to the `order` specified in the POST body. + description: >- + Create a project phase. It also updates the `order` field of all other + phases in the same project which have `order` greater than or equal to + the `order` specified in the POST body. parameters: - in: body name: body @@ -394,127 +399,132 @@ paths: productTemplateId: type: number format: long - description: the optional productTemplateId used to populate a new phase product for the created phase + description: >- + the optional productTemplateId used to populate a new + phase product for the created phase responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" '201': description: Returns the newly created project phase schema: - $ref: "#/definitions/ProjectPhaseResponse" + $ref: '#/definitions/ProjectPhaseResponse' + '403': + description: No permission or wrong token + schema: + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" - - /projects/{projectId}/phases/{phaseId}: + $ref: '#/definitions/ErrorModel' + '/projects/{projectId}/phases/{phaseId}': parameters: - - $ref: "#/parameters/projectIdParam" - - $ref: "#/parameters/phaseIdParam" + - $ref: '#/parameters/projectIdParam' + - $ref: '#/parameters/phaseIdParam' get: tags: - phase - description: Retrieve project phase by id. All users who can edit project can access this endpoint. + description: >- + Retrieve project phase by id. All users who can edit project can access + this endpoint. security: - Bearer: [] responses: - '404': - description: Not found + '200': + description: a project phase schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ProjectPhaseResponse' '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" - '200': - description: a project phase + $ref: '#/definitions/ErrorModel' + '404': + description: Not found schema: - $ref: "#/definitions/ProjectPhaseResponse" + $ref: '#/definitions/ErrorModel' parameters: - - $ref: "#/parameters/phaseIdParam" + - $ref: '#/parameters/phaseIdParam' operationId: getProjectPhase - patch: tags: - phase operationId: updateProjectPhase security: - Bearer: [] - description: Update a project phase. All users who can edit project can access this endpoint. - It also updates the `order` field of all other phases in the same project which have `order` greater than or equal to the `order` specified in the POST body. + description: >- + Update a project phase. All users who can edit project can access this + endpoint. It also updates the `order` field of all other phases in the + same project which have `order` greater than or equal to the `order` + specified in the POST body. responses: + '200': + description: Successfully updated project phase. + schema: + $ref: '#/definitions/ProjectPhaseResponse' '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '404': description: Not found schema: - $ref: "#/definitions/ErrorModel" - '200': - description: Successfully updated project phase. - schema: - $ref: "#/definitions/ProjectPhaseResponse" + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' default: description: error payload schema: $ref: '#/definitions/ErrorModel' parameters: - - $ref: "#/parameters/phaseIdParam" + - $ref: '#/parameters/phaseIdParam' - name: body in: body required: true schema: - $ref: "#/definitions/ProjectPhaseBodyParam" - + $ref: '#/definitions/ProjectPhaseBodyParam' delete: tags: - phase - description: Remove an existing project phase. All users who can edit project can access this endpoint. + description: >- + Remove an existing project phase. All users who can edit project can + access this endpoint. security: - Bearer: [] parameters: - - $ref: "#/parameters/phaseIdParam" + - $ref: '#/parameters/phaseIdParam' responses: + '204': + description: Project phase successfully removed '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '404': description: If project is not found schema: - $ref: "#/definitions/ErrorModel" - '204': - description: Project phase successfully removed - - - - /projects/{projectId}/phases/{phaseId}/products: + $ref: '#/definitions/ErrorModel' + '/projects/{projectId}/phases/{phaseId}/products': parameters: - - $ref: "#/parameters/projectIdParam" - - $ref: "#/parameters/phaseIdParam" + - $ref: '#/parameters/projectIdParam' + - $ref: '#/parameters/phaseIdParam' get: tags: - phase product operationId: findPhaseProducts security: - Bearer: [] - description: Retrieve all phase products. All users who can edit project can access this endpoint. + description: >- + Retrieve all phase products. All users who can edit project can access + this endpoint. responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" '200': description: A list of phase products schema: - $ref: "#/definitions/PhaseProductListResponse" + $ref: '#/definitions/PhaseProductListResponse' + '403': + description: No permission or wrong token + schema: + $ref: '#/definitions/ErrorModel' post: tags: - phase product @@ -529,141 +539,144 @@ paths: schema: $ref: '#/definitions/PhaseProductBodyParam' responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" '201': description: Returns the newly created phase product schema: - $ref: "#/definitions/PhaseProductResponse" + $ref: '#/definitions/PhaseProductResponse' + '403': + description: No permission or wrong token + schema: + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" - - /projects/{projectId}/phases/{phaseId}/products/{productId}: + $ref: '#/definitions/ErrorModel' + '/projects/{projectId}/phases/{phaseId}/products/{productId}': parameters: - - $ref: "#/parameters/projectIdParam" - - $ref: "#/parameters/phaseIdParam" - - $ref: "#/parameters/productIdParam" + - $ref: '#/parameters/projectIdParam' + - $ref: '#/parameters/phaseIdParam' + - $ref: '#/parameters/productIdParam' get: tags: - phase product - description: Retrieve phase product by id. All users who can edit project can access this endpoint. + description: >- + Retrieve phase product by id. All users who can edit project can access + this endpoint. security: - Bearer: [] responses: - '404': - description: Not found + '200': + description: a phase product schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/PhaseProductResponse' '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" - '200': - description: a phase product + $ref: '#/definitions/ErrorModel' + '404': + description: Not found schema: - $ref: "#/definitions/PhaseProductResponse" + $ref: '#/definitions/ErrorModel' parameters: - - $ref: "#/parameters/phaseIdParam" + - $ref: '#/parameters/phaseIdParam' operationId: getPhaseProduct - patch: tags: - phase product operationId: updatePhaseProduct security: - Bearer: [] - description: Update a phase product. All users who can edit project can access this endpoint. + description: >- + Update a phase product. All users who can edit project can access this + endpoint. responses: + '200': + description: Successfully updated phase product. + schema: + $ref: '#/definitions/PhaseProductResponse' '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '404': description: Not found schema: - $ref: "#/definitions/ErrorModel" - '200': - description: Successfully updated phase product. - schema: - $ref: "#/definitions/PhaseProductResponse" + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' default: description: error payload schema: $ref: '#/definitions/ErrorModel' parameters: - - $ref: "#/parameters/phaseIdParam" + - $ref: '#/parameters/phaseIdParam' - name: body in: body required: true schema: - $ref: "#/definitions/PhaseProductBodyParam" - + $ref: '#/definitions/PhaseProductBodyParam' delete: tags: - phase product - description: Remove an existing phase product. All users who can edit project can access this endpoint. + description: >- + Remove an existing phase product. All users who can edit project can + access this endpoint. security: - Bearer: [] parameters: - - $ref: "#/parameters/phaseIdParam" + - $ref: '#/parameters/phaseIdParam' responses: + '204': + description: Project phase successfully removed '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '404': description: If project is not found schema: - $ref: "#/definitions/ErrorModel" - '204': - description: Project phase successfully removed - - /projects/{projectId}/upgrade: + $ref: '#/definitions/ErrorModel' + '/projects/{projectId}/upgrade': post: tags: - project operationId: upgradeProject security: - Bearer: [] - description: Migrates a project to a target version. Only users with "administrator" or "Connect admin" roles can access to this endpoint + description: >- + Migrates a project to a target version. Only users with "administrator" + or "Connect admin" roles can access to this endpoint parameters: - - $ref: "#/parameters/projectIdParam" + - $ref: '#/parameters/projectIdParam' - name: body in: body required: true description: Project upgrade body schema: - $ref: "#/definitions/ProjectUpgradeBodyParam" + $ref: '#/definitions/ProjectUpgradeBodyParam' responses: + '200': + description: Project migrated successfully + schema: + $ref: '#/definitions/ProjectUpgradeResponse' '400': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '404': description: Project not found schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '500': description: Invalid server state or unknown error schema: - $ref: "#/definitions/ErrorModel" - '200': - description: Project migrated successfully - schema: - $ref: "#/definitions/ProjectUpgradeResponse" - + $ref: '#/definitions/ErrorModel' /projects/metadata: get: tags: @@ -671,17 +684,19 @@ paths: operationId: getAllMetadata security: - Bearer: [] - description: Retrieve all metadata including projectTemplates, productTemplates, milestoneTemplates, projectTypes, productCategories. All user roles can access this endpoint. + description: >- + Retrieve all metadata including projectTemplates, productTemplates, + milestoneTemplates, projectTypes, productCategories. All user roles can + access this endpoint. responses: '200': description: The metadata schema: - $ref: "#/definitions/AllMetadataResponse" + $ref: '#/definitions/AllMetadataResponse' '500': description: Invalid server state or unknown error schema: - $ref: "#/definitions/ErrorModel" - + $ref: '#/definitions/ErrorModel' /projects/metadata/projectTemplates: get: tags: @@ -691,14 +706,14 @@ paths: - Bearer: [] description: Retrieve all project templates. All user roles can access this endpoint. responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" '200': description: A list of project templates schema: - $ref: "#/definitions/ProjectTemplateListResponse" + $ref: '#/definitions/ProjectTemplateListResponse' + '403': + description: No permission or wrong token + schema: + $ref: '#/definitions/ErrorModel' post: tags: - projectTemplate @@ -713,101 +728,137 @@ paths: schema: $ref: '#/definitions/ProjectTemplateBodyParam' responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" '201': description: Returns the newly created project template schema: - $ref: "#/definitions/ProjectTemplateResponse" + $ref: '#/definitions/ProjectTemplateResponse' + '403': + description: No permission or wrong token + schema: + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" - - /projects/metadata/projectTemplates/{templateId}: + $ref: '#/definitions/ErrorModel' + '/projects/metadata/projectTemplates/{templateId}': get: tags: - projectTemplate - description: Retrieve project template by id. All user roles can access this endpoint. + description: >- + Retrieve project template by id. All user roles can access this + endpoint. security: - Bearer: [] responses: - '404': - description: Not found + '200': + description: a project template schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ProjectTemplateResponse' '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" - '200': - description: a project template + $ref: '#/definitions/ErrorModel' + '404': + description: Not found schema: - $ref: "#/definitions/ProjectTemplateResponse" + $ref: '#/definitions/ErrorModel' parameters: - - $ref: "#/parameters/templateIdParam" + - $ref: '#/parameters/templateIdParam' operationId: getProjectTemplate - patch: tags: - projectTemplate operationId: updateProjectTemplate security: - Bearer: [] - description: Update a project template. Only connect manager, connect admin, and admin can access this endpoint. - For attributes with JSON object type, it would overwrite the existing fields, or add new if the fields don't exist in the JSON object. + description: >- + Update a project template. Only connect manager, connect admin, and + admin can access this endpoint. For attributes with JSON object type, it + would overwrite the existing fields, or add new if the fields don't + exist in the JSON object. responses: - '403': + '200': + description: Successfully updated project template. + schema: + $ref: '#/definitions/ProjectTemplateResponse' + '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '404': description: Not found schema: - $ref: "#/definitions/ErrorModel" - '200': - description: Successfully updated project template. - schema: - $ref: "#/definitions/ProjectTemplateResponse" + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' default: description: error payload schema: $ref: '#/definitions/ErrorModel' parameters: - - $ref: "#/parameters/templateIdParam" + - $ref: '#/parameters/templateIdParam' - name: body in: body required: true schema: - $ref: "#/definitions/ProjectTemplateBodyParam" - + $ref: '#/definitions/ProjectTemplateBodyParam' delete: tags: - projectTemplate - description: Remove an existing project template. Only connect manager, connect admin, and admin can access this endpoint. + description: >- + Remove an existing project template. Only connect manager, connect + admin, and admin can access this endpoint. security: - Bearer: [] parameters: - - $ref: "#/parameters/templateIdParam" + - $ref: '#/parameters/templateIdParam' responses: + '204': + description: Project template successfully removed '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '404': description: If project is not found schema: - $ref: "#/definitions/ErrorModel" - '204': - description: Project template successfully removed - - + $ref: '#/definitions/ErrorModel' + '/projects/metadata/productTemplates/{templateId}/upgrade': + post: + tags: + - productTemplate + description: >- + upgrade projectTemplate model, + security: + - Bearer: [] + parameters: + - $ref: '#/parameters/templateIdParam' + - in: body + name: body + required: true + schema: + $ref: '#/definitions/ProjectTemplateUpgradeBodyParam' + responses: + '200': + description: Product template successfully upgrade + '403': + description: No permission or wrong token + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: If product template is not found + schema: + $ref: '#/definitions/ErrorModel' + '422': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Server Error + schema: + $ref: '#/definitions/ErrorModel' /projects/metadata/productTemplates: get: tags: @@ -817,14 +868,14 @@ paths: - Bearer: [] description: Retrieve all product templates. All user roles can access this endpoint. responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" '200': description: A list of product templates schema: - $ref: "#/definitions/ProductTemplateListResponse" + $ref: '#/definitions/ProductTemplateListResponse' + '403': + description: No permission or wrong token + schema: + $ref: '#/definitions/ErrorModel' post: tags: - productTemplate @@ -839,101 +890,103 @@ paths: schema: $ref: '#/definitions/ProductTemplateBodyParam' responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" '201': description: Returns the newly created product template schema: - $ref: "#/definitions/ProductTemplateResponse" + $ref: '#/definitions/ProductTemplateResponse' + '403': + description: No permission or wrong token + schema: + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" - - /projects/metadata/productTemplates/{templateId}: + $ref: '#/definitions/ErrorModel' + '/projects/metadata/productTemplates/{templateId}': get: tags: - productTemplate - description: Retrieve product template by id. All user roles can access this endpoint. + description: >- + Retrieve product template by id. All user roles can access this + endpoint. security: - Bearer: [] responses: - '404': - description: Not found + '200': + description: a product template schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ProductTemplateResponse' '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" - '200': - description: a product template + $ref: '#/definitions/ErrorModel' + '404': + description: Not found schema: - $ref: "#/definitions/ProductTemplateResponse" + $ref: '#/definitions/ErrorModel' parameters: - - $ref: "#/parameters/templateIdParam" + - $ref: '#/parameters/templateIdParam' operationId: getProductTemplate - patch: tags: - productTemplate operationId: updateProductTemplate security: - Bearer: [] - description: Update a product template. Only connect manager, connect admin, and admin can access this endpoint. - For attributes with JSON object type, it would overwrite the existing fields, or add new if the fields don't exist in the JSON object. + description: >- + Update a product template. Only connect manager, connect admin, and + admin can access this endpoint. For attributes with JSON object type, it + would overwrite the existing fields, or add new if the fields don't + exist in the JSON object. responses: + '200': + description: Successfully updated product template. + schema: + $ref: '#/definitions/ProductTemplateResponse' '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '404': description: Not found schema: - $ref: "#/definitions/ErrorModel" - '200': - description: Successfully updated product template. - schema: - $ref: "#/definitions/ProductTemplateResponse" + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' default: description: error payload schema: $ref: '#/definitions/ErrorModel' parameters: - - $ref: "#/parameters/templateIdParam" + - $ref: '#/parameters/templateIdParam' - name: body in: body required: true schema: - $ref: "#/definitions/ProductTemplateBodyParam" - + $ref: '#/definitions/ProductTemplateBodyParam' delete: tags: - productTemplate - description: Remove an existing product template. Only connect manager, connect admin, and admin can access this endpoint. + description: >- + Remove an existing product template. Only connect manager, connect + admin, and admin can access this endpoint. security: - Bearer: [] parameters: - - $ref: "#/parameters/templateIdParam" + - $ref: '#/parameters/templateIdParam' responses: + '204': + description: Product template successfully removed '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '404': description: If product is not found schema: - $ref: "#/definitions/ErrorModel" - '204': - description: Product template successfully removed - - + $ref: '#/definitions/ErrorModel' /projects/metadata/productCategories: get: tags: @@ -941,23 +994,27 @@ paths: operationId: findProductCategories security: - Bearer: [] - description: Retrieve all product categories. All user roles can access this endpoint. + description: >- + Retrieve all product categories. All user roles can access this + endpoint. responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" '200': description: A list of product categories schema: - $ref: "#/definitions/ProductCategoryListResponse" + $ref: '#/definitions/ProductCategoryListResponse' + '403': + description: No permission or wrong token + schema: + $ref: '#/definitions/ErrorModel' post: tags: - productCategory operationId: addProductCategory security: - Bearer: [] - description: Create a product category. Only admin or connect admin can access this endpoint. + description: >- + Create a product category. Only admin or connect admin can access this + endpoint. parameters: - in: body name: body @@ -965,41 +1022,42 @@ paths: schema: $ref: '#/definitions/ProductCategoryCreateBodyParam' responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" '201': description: Returns the newly created product category schema: - $ref: "#/definitions/ProductCategoryResponse" + $ref: '#/definitions/ProductCategoryResponse' + '403': + description: No permission or wrong token + schema: + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" - - /projects/metadata/productCategories/{key}: + $ref: '#/definitions/ErrorModel' + '/projects/metadata/productCategories/{key}': get: tags: - productCategory - description: Retrieve product category by id. All user roles can access this endpoint. + description: >- + Retrieve product category by id. All user roles can access this + endpoint. security: - Bearer: [] responses: - '404': - description: Not found + '200': + description: a product category schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ProductCategoryResponse' '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" - '200': - description: a product category + $ref: '#/definitions/ErrorModel' + '404': + description: Not found schema: - $ref: "#/definitions/ProductCategoryResponse" + $ref: '#/definitions/ErrorModel' parameters: - - $ref: "#/parameters/keyParam" + - $ref: '#/parameters/keyParam' operationId: getProductCategory patch: tags: @@ -1007,56 +1065,58 @@ paths: operationId: updateProductCategory security: - Bearer: [] - description: Update a product category. Only admin or connect admin can access this endpoint. + description: >- + Update a product category. Only admin or connect admin can access this + endpoint. responses: + '200': + description: Successfully updated product category. + schema: + $ref: '#/definitions/ProductCategoryResponse' '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '404': description: Not found schema: - $ref: "#/definitions/ErrorModel" - '200': - description: Successfully updated product category. - schema: - $ref: "#/definitions/ProductCategoryResponse" + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' default: description: error payload schema: $ref: '#/definitions/ErrorModel' parameters: - - $ref: "#/parameters/keyParam" + - $ref: '#/parameters/keyParam' - name: body in: body required: true schema: - $ref: "#/definitions/ProductCategoryBodyParam" + $ref: '#/definitions/ProductCategoryBodyParam' delete: tags: - productCategory - description: Remove an existing product category. Only admin or connect admin can access this endpoint. + description: >- + Remove an existing product category. Only admin or connect admin can + access this endpoint. security: - Bearer: [] parameters: - - $ref: "#/parameters/keyParam" + - $ref: '#/parameters/keyParam' responses: + '204': + description: Product category successfully removed '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '404': description: If product category is not found schema: - $ref: "#/definitions/ErrorModel" - '204': - description: Product category successfully removed - - + $ref: '#/definitions/ErrorModel' /projects/metadata/projectTypes: get: tags: @@ -1066,21 +1126,23 @@ paths: - Bearer: [] description: Retrieve all project types. All user roles can access this endpoint. responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" '200': description: A list of project types schema: - $ref: "#/definitions/ProjectTypeListResponse" + $ref: '#/definitions/ProjectTypeListResponse' + '403': + description: No permission or wrong token + schema: + $ref: '#/definitions/ErrorModel' post: tags: - projectType operationId: addProjectType security: - Bearer: [] - description: Create a project type. Only admin or connect admin can access this endpoint. + description: >- + Create a project type. Only admin or connect admin can access this + endpoint. parameters: - in: body name: body @@ -1088,20 +1150,19 @@ paths: schema: $ref: '#/definitions/ProjectTypeCreateBodyParam' responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" '201': description: Returns the newly created project type schema: - $ref: "#/definitions/ProjectTypeResponse" + $ref: '#/definitions/ProjectTypeResponse' + '403': + description: No permission or wrong token + schema: + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" - - /projects/metadata/projectTypes/{key}: + $ref: '#/definitions/ErrorModel' + '/projects/metadata/projectTypes/{key}': get: tags: - projectType @@ -1109,78 +1170,79 @@ paths: security: - Bearer: [] responses: - '404': - description: Not found + '200': + description: a project type schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ProjectTypeResponse' '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" - '200': - description: a project type + $ref: '#/definitions/ErrorModel' + '404': + description: Not found schema: - $ref: "#/definitions/ProjectTypeResponse" + $ref: '#/definitions/ErrorModel' parameters: - - $ref: "#/parameters/keyParam" + - $ref: '#/parameters/keyParam' operationId: getProjectType - patch: tags: - projectType operationId: updateProjectType security: - Bearer: [] - description: Update a project type. Only admin or connect admin can access this endpoint. + description: >- + Update a project type. Only admin or connect admin can access this + endpoint. responses: + '200': + description: Successfully updated project type. + schema: + $ref: '#/definitions/ProjectTypeResponse' '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '404': description: Not found schema: - $ref: "#/definitions/ErrorModel" - '200': - description: Successfully updated project type. - schema: - $ref: "#/definitions/ProjectTypeResponse" + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' default: description: error payload schema: $ref: '#/definitions/ErrorModel' parameters: - - $ref: "#/parameters/keyParam" + - $ref: '#/parameters/keyParam' - name: body in: body required: true schema: - $ref: "#/definitions/ProjectTypeBodyParam" - + $ref: '#/definitions/ProjectTypeBodyParam' delete: tags: - projectType - description: Remove an existing project type. Only admin or connect admin can access this endpoint. + description: >- + Remove an existing project type. Only admin or connect admin can access + this endpoint. security: - Bearer: [] parameters: - - $ref: "#/parameters/keyParam" + - $ref: '#/parameters/keyParam' responses: + '204': + description: Project type successfully removed '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '404': description: If project is not found schema: - $ref: "#/definitions/ErrorModel" - '204': - description: Project type successfully removed - + $ref: '#/definitions/ErrorModel' /projects/metadata/orgConfig: get: tags: @@ -1188,7 +1250,9 @@ paths: operationId: findOrgConfigs security: - Bearer: [] - description: Retrieve all organization configs. All user roles can access this endpoint. + description: >- + Retrieve all organization configs. All user roles can access this + endpoint. parameters: - name: filter required: true @@ -1199,21 +1263,23 @@ paths: - orgId (required) - configName responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" '200': description: A list of organization configs schema: - $ref: "#/definitions/OrgConfigListResponse" + $ref: '#/definitions/OrgConfigListResponse' + '403': + description: No permission or wrong token + schema: + $ref: '#/definitions/ErrorModel' post: tags: - orgConfig operationId: addOrgConfig security: - Bearer: [] - description: Create a organization config. Only admin or connect admin can access this endpoint. + description: >- + Create a organization config. Only admin or connect admin can access + this endpoint. parameters: - in: body name: body @@ -1221,20 +1287,19 @@ paths: schema: $ref: '#/definitions/OrgConfigCreateBodyParam' responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" '201': description: Returns the newly created organization config schema: - $ref: "#/definitions/OrgConfigResponse" + $ref: '#/definitions/OrgConfigResponse' + '403': + description: No permission or wrong token + schema: + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" - - /projects/metadata/orgConfig/{id}: + $ref: '#/definitions/ErrorModel' + '/projects/metadata/orgConfig/{id}': get: tags: - orgConfig @@ -1242,20 +1307,20 @@ paths: security: - Bearer: [] responses: - '404': - description: Not found + '200': + description: a project type schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/OrgConfigResponse' '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" - '200': - description: a project type + $ref: '#/definitions/ErrorModel' + '404': + description: Not found schema: - $ref: "#/definitions/OrgConfigResponse" + $ref: '#/definitions/ErrorModel' parameters: - - $ref: "#/parameters/idParam" + - $ref: '#/parameters/idParam' operationId: getOrgConfig patch: tags: @@ -1263,56 +1328,58 @@ paths: operationId: updateOrgConfig security: - Bearer: [] - description: Update a organization config. Only admin or connect admin can access this endpoint. + description: >- + Update a organization config. Only admin or connect admin can access + this endpoint. responses: + '200': + description: Successfully updated organization config. + schema: + $ref: '#/definitions/OrgConfigResponse' '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '404': description: Not found schema: - $ref: "#/definitions/ErrorModel" - '200': - description: Successfully updated organization config. - schema: - $ref: "#/definitions/OrgConfigResponse" + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' default: description: error payload schema: $ref: '#/definitions/ErrorModel' parameters: - - $ref: "#/parameters/idParam" + - $ref: '#/parameters/idParam' - name: body in: body required: true schema: - $ref: "#/definitions/OrgConfigCreateBodyParam" - + $ref: '#/definitions/OrgConfigCreateBodyParam' delete: tags: - orgConfig - description: Remove an existing organization config. Only admin or connect admin can access this endpoint. + description: >- + Remove an existing organization config. Only admin or connect admin can + access this endpoint. security: - Bearer: [] parameters: - - $ref: "#/parameters/idParam" + - $ref: '#/parameters/idParam' responses: + '204': + description: Organization config successfully removed '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '404': description: If organization config is not found schema: - $ref: "#/definitions/ErrorModel" - '204': - description: Organization config successfully removed - + $ref: '#/definitions/ErrorModel' /timelines: get: tags: @@ -1331,25 +1398,27 @@ paths: - reference - referenceId responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" '200': description: A list of timelines schema: - $ref: "#/definitions/TimelineListResponse" + $ref: '#/definitions/TimelineListResponse' + '403': + description: No permission or wrong token + schema: + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' post: tags: - timeline operationId: addTimeline security: - Bearer: [] - description: Create a timeline. All users who can edit the project can access this endpoint. + description: >- + Create a timeline. All users who can edit the project can access this + endpoint. parameters: - in: body name: body @@ -1357,117 +1426,121 @@ paths: schema: $ref: '#/definitions/TimelineBodyParam' responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" '201': description: Returns the newly created timeline schema: - $ref: "#/definitions/TimelineResponse" + $ref: '#/definitions/TimelineResponse' + '403': + description: No permission or wrong token + schema: + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" - - /timelines/{timelineId}: + $ref: '#/definitions/ErrorModel' + '/timelines/{timelineId}': get: tags: - timeline - description: Retrieve timeline by id. All users who can view the project can access this endpoint. + description: >- + Retrieve timeline by id. All users who can view the project can access + this endpoint. security: - Bearer: [] responses: - '404': - description: Not found + '200': + description: a timeline schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/TimelineResponse' '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' + '404': + description: Not found + schema: + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" - '200': - description: a timeline - schema: - $ref: "#/definitions/TimelineResponse" + $ref: '#/definitions/ErrorModel' parameters: - - $ref: "#/parameters/timelineIdParam" + - $ref: '#/parameters/timelineIdParam' operationId: getTimeline - patch: tags: - timeline operationId: updateTimeline security: - Bearer: [] - description: Update a timeline. All users who can edit the project can access this endpoint. + description: >- + Update a timeline. All users who can edit the project can access this + endpoint. responses: - '403': - description: No permission or wrong token + '200': + description: Successfully updated timeline. + schema: + $ref: '#/definitions/TimelineResponse' + '403': + description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '404': description: Not found schema: - $ref: "#/definitions/ErrorModel" - '200': - description: Successfully updated timeline. - schema: - $ref: "#/definitions/TimelineResponse" + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' default: description: error payload schema: $ref: '#/definitions/ErrorModel' parameters: - - $ref: "#/parameters/timelineIdParam" + - $ref: '#/parameters/timelineIdParam' - name: body in: body required: true schema: - $ref: "#/definitions/TimelineBodyParam" - + $ref: '#/definitions/TimelineBodyParam' delete: tags: - timeline - description: Remove an existing timeline. All users who can edit the project can access this endpoint. + description: >- + Remove an existing timeline. All users who can edit the project can + access this endpoint. security: - Bearer: [] parameters: - - $ref: "#/parameters/timelineIdParam" + - $ref: '#/parameters/timelineIdParam' responses: + '204': + description: Timeline successfully removed '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '404': description: Not found schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" - '204': - description: Timeline successfully removed - - /timelines/{timelineId}/milestones: + $ref: '#/definitions/ErrorModel' + '/timelines/{timelineId}/milestones': parameters: - - $ref: "#/parameters/timelineIdParam" + - $ref: '#/parameters/timelineIdParam' get: tags: - milestone operationId: findMilestones security: - Bearer: [] - description: Retrieve all milestones. All users who can view the timeline can access this endpoint. + description: >- + Retrieve all milestones. All users who can view the timeline can access + this endpoint. parameters: - name: sort required: false @@ -1475,26 +1548,29 @@ paths: in: query type: string responses: + '200': + description: A list of milestones + schema: + $ref: '#/definitions/MilestoneListResponse' '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" - '200': - description: A list of milestones - schema: - $ref: "#/definitions/MilestoneListResponse" + $ref: '#/definitions/ErrorModel' post: tags: - milestone operationId: addMilestone security: - Bearer: [] - description: Create a milestone. All users who can edit the timeline can access this endpoint. - It also updates the `order` field of all other milestones in the same timeline which have `order` greater than or equal to the `order` specified in the POST body. + description: >- + Create a milestone. All users who can edit the timeline can access this + endpoint. It also updates the `order` field of all other milestones in + the same timeline which have `order` greater than or equal to the + `order` specified in the POST body. parameters: - in: body name: body @@ -1502,73 +1578,76 @@ paths: schema: $ref: '#/definitions/MilestonePostBodyParam' responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" '201': description: Returns the newly created milestone schema: - $ref: "#/definitions/MilestoneResponse" + $ref: '#/definitions/MilestoneResponse' + '403': + description: No permission or wrong token + schema: + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" - - /timelines/{timelineId}/milestones/{milestoneId}: + $ref: '#/definitions/ErrorModel' + '/timelines/{timelineId}/milestones/{milestoneId}': parameters: - - $ref: "#/parameters/timelineIdParam" - - $ref: "#/parameters/milestoneIdParam" + - $ref: '#/parameters/timelineIdParam' + - $ref: '#/parameters/milestoneIdParam' get: tags: - milestone - description: Retrieve milestone by id. All users who can view the timeline can access this endpoint. + description: >- + Retrieve milestone by id. All users who can view the timeline can access + this endpoint. security: - Bearer: [] responses: - '404': - description: Not found + '200': + description: a milestone schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/MilestoneResponse' '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' + '404': + description: Not found + schema: + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" - '200': - description: a milestone - schema: - $ref: "#/definitions/MilestoneResponse" + $ref: '#/definitions/ErrorModel' operationId: getMilestone - patch: tags: - milestone operationId: updateMilestone security: - Bearer: [] - description: Update a milestone. All users who can edit the timeline can access this endpoint. - For attributes with JSON object type, it would overwrite the existing fields, or add new if the fields don't exist in the JSON object. + description: >- + Update a milestone. All users who can edit the timeline can access this + endpoint. For attributes with JSON object type, it would overwrite the + existing fields, or add new if the fields don't exist in the JSON + object. responses: + '200': + description: Successfully updated milestone. + schema: + $ref: '#/definitions/MilestoneResponse' '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '404': description: Not found schema: - $ref: "#/definitions/ErrorModel" - '200': - description: Successfully updated milestone. - schema: - $ref: "#/definitions/MilestoneResponse" + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' default: description: error payload schema: @@ -1578,31 +1657,30 @@ paths: in: body required: true schema: - $ref: "#/definitions/MilestonePatchBodyParam" - + $ref: '#/definitions/MilestonePatchBodyParam' delete: tags: - milestone - description: Remove an existing milestone. All users who can edit the timeline can access this endpoint. + description: >- + Remove an existing milestone. All users who can edit the timeline can + access this endpoint. security: - Bearer: [] responses: + '204': + description: Milestone successfully removed '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '404': description: Not found schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" - '204': - description: Milestone successfully removed - - + $ref: '#/definitions/ErrorModel' /timelines/metadata/milestoneTemplates: get: tags: @@ -1610,7 +1688,9 @@ paths: operationId: findMilestoneTemplates security: - Bearer: [] - description: Retrieve all milestone templates. All user roles can access this endpoint. + description: >- + Retrieve all milestone templates. All user roles can access this + endpoint. parameters: - name: sort required: false @@ -1626,25 +1706,29 @@ paths: - reference - referenceId responses: + '200': + description: A list of milestone templates + schema: + $ref: '#/definitions/MilestoneTemplateListResponse' '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" - '200': - description: A list of milestone templates - schema: - $ref: "#/definitions/MilestoneTemplateListResponse" + $ref: '#/definitions/ErrorModel' post: tags: - milestoneTemplates operationId: addMilestoneTemplate security: - Bearer: [] - description: Create a milestone template. Only connect manager, connect admin, and admin can access this endpoint. It also updates the `order` field of all other milestone templates in the same product template which have `order` greater than or equal to the `order` specified in the POST body. + description: >- + Create a milestone template. Only connect manager, connect admin, and + admin can access this endpoint. It also updates the `order` field of all + other milestone templates in the same product template which have + `order` greater than or equal to the `order` specified in the POST body. parameters: - in: body name: body @@ -1652,19 +1736,18 @@ paths: schema: $ref: '#/definitions/MilestoneTemplateBodyParam' responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" '201': description: Returns the newly created milestone template schema: - $ref: "#/definitions/MilestoneTemplateResponse" + $ref: '#/definitions/MilestoneTemplateResponse' + '403': + description: No permission or wrong token + schema: + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" - + $ref: '#/definitions/ErrorModel' /timelines/metadata/milestoneTemplates/clone: post: tags: @@ -1672,7 +1755,9 @@ paths: operationId: cloneMilestoneTemplate security: - Bearer: [] - description: Clone milestone templates from one product template to the other. Only connect manager, connect admin, and admin can access this endpoint. + description: >- + Clone milestone templates from one product template to the other. Only + connect manager, connect admin, and admin can access this endpoint. parameters: - in: body name: body @@ -1683,199 +1768,1025 @@ paths: '201': description: Returns the list of cloned milestone templates schema: - $ref: "#/definitions/MilestoneTemplateListResponse" + $ref: '#/definitions/MilestoneTemplateListResponse' '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '404': description: Not found schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" - - - /timelines/metadata/milestoneTemplates/{milestoneTemplateId}: + $ref: '#/definitions/ErrorModel' + '/timelines/metadata/milestoneTemplates/{milestoneTemplateId}': parameters: - - $ref: "#/parameters/milestoneTemplateIdParam" + - $ref: '#/parameters/milestoneTemplateIdParam' get: tags: - milestoneTemplates - description: Retrieve milestone template by id. All user roles can access this endpoint. + description: >- + Retrieve milestone template by id. All user roles can access this + endpoint. + security: + - Bearer: [] + responses: + '200': + description: a milestone template + schema: + $ref: '#/definitions/MilestoneTemplateResponse' + '403': + description: No permission or wrong token + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: Not found + schema: + $ref: '#/definitions/ErrorModel' + '422': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + operationId: getMilestoneTemplate + patch: + tags: + - milestoneTemplates + operationId: updateMilestoneTemplate + security: + - Bearer: [] + description: >- + Update a milestone template. Only connect manager, connect admin, and + admin can access this endpoint. + responses: + '200': + description: Successfully updated milestone template. + schema: + $ref: '#/definitions/MilestoneTemplateResponse' + '403': + description: No permission or wrong token + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: Not found + schema: + $ref: '#/definitions/ErrorModel' + '422': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + default: + description: error payload + schema: + $ref: '#/definitions/ErrorModel' + parameters: + - name: body + in: body + required: true + schema: + $ref: '#/definitions/MilestoneTemplateBodyParam' + delete: + tags: + - milestoneTemplates + description: >- + Remove an existing milestone template. Only connect manager, connect + admin, and admin can access this endpoint. security: - Bearer: [] responses: + '204': + description: Milestone template successfully removed + '403': + description: No permission or wrong token + schema: + $ref: '#/definitions/ErrorModel' '404': description: Not found schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' + '422': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + '/projects/{projectId}/members/invite': + get: + tags: + - project member invite + operationId: getCurrentUserInvite + security: + - Bearer: [] + description: Retrieve the invite for current user. + parameters: + - $ref: '#/parameters/projectIdParam' + responses: + '200': + description: The invite for current user + schema: + $ref: '#/definitions/ProjectMemberInviteResponse' + '400': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + '403': + description: No permission or wrong token + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: Invite not found + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Invalid server state or unknown error + schema: + $ref: '#/definitions/ErrorModel' + post: + tags: + - project member invite + operationId: addProjectMemberInvite + security: + - Bearer: [] + description: >- + Create an invite. All users who can access this endpoint, however more + restriction will be applied based on role to be added. + parameters: + - $ref: '#/parameters/projectIdParam' + - in: body + name: body + required: true + schema: + $ref: '#/definitions/AddProjectMemberInvitesRequest' + responses: + '201': + description: Returns the newly created invite + schema: + $ref: '#/definitions/ProjectMemberInviteResponse' + '400': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + '403': + description: No permission or wrong token + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Invalid server state or unknown error + schema: + $ref: '#/definitions/ErrorModel' + put: + tags: + - project member invite + operationId: updateProjectMemberInvite + security: + - Bearer: [] + description: >- + Update an invite. All users who can access this endpoint, however more + restriction will be applied based on role to be updated. + parameters: + - $ref: '#/parameters/projectIdParam' + - in: body + name: body + required: true + schema: + $ref: '#/definitions/UpdateProjectMemberInviteRequest' + responses: + '200': + description: Returns the newly updated invite + schema: + $ref: '#/definitions/ProjectMemberInviteResponse' + '400': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' '403': description: No permission or wrong token schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' + '500': + description: Invalid server state or unknown error + schema: + $ref: '#/definitions/ErrorModel' + + '/projects/metadata/form/{key}': + get: + tags: + - form version + security: + - Bearer: [] + description: get the latest revision of latest version for key. + parameters: + - $ref: '#/parameters/modelKeyParam' + responses: + '200': + description: The model for the latest revision of latest version + schema: + $ref: '#/definitions/FormResponse' + '422': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: key not found + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Invalid server state or unknown error + schema: + $ref: '#/definitions/ErrorModel' + '/projects/metadata/form/{key}/versions': + get: + tags: + - form version + security: + - Bearer: [] + description: get all versions for key. + parameters: + - $ref: '#/parameters/modelKeyParam' + responses: + '200': + description: The model list for the all version + schema: + $ref: '#/definitions/FormListResponse' + '422': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: key not found + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Invalid server state or unknown error + schema: + $ref: '#/definitions/ErrorModel' + post: + tags: + - form version + security: + - Bearer: [] + description: create version for key + parameters: + - $ref: '#/parameters/modelKeyParam' + - in: body + name: body + required: true + schema: + $ref: '#/definitions/NewFormParam' + responses: + '201': + description: The model created + schema: + $ref: '#/definitions/FormResponse' + '422': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: key not found + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Invalid server state or unknown error + schema: + $ref: '#/definitions/ErrorModel' + '/projects/metadata/form/{key}/versions/{version}': + get: + tags: + - form version + security: + - Bearer: [] + description: get particular version for key. + parameters: + - $ref: '#/parameters/modelKeyParam' + - $ref: '#/parameters/modelVersionParam' + responses: + '200': + description: The model for the particular version + schema: + $ref: '#/definitions/FormResponse' + '422': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: key not found + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Invalid server state or unknown error + schema: + $ref: '#/definitions/ErrorModel' + patch: + tags: + - form version + security: + - Bearer: [] + description: update version for key + parameters: + - $ref: '#/parameters/modelKeyParam' + - $ref: '#/parameters/modelVersionParam' + - in: body + name: body + required: true + schema: + $ref: '#/definitions/NewFormParam' + responses: + '201': + description: The model updated + schema: + $ref: '#/definitions/FormResponse' + '422': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: key not found + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Invalid server state or unknown error + schema: + $ref: '#/definitions/ErrorModel' + delete: + tags: + - form version + security: + - Bearer: [] + description: delete version for key + parameters: + - $ref: '#/parameters/modelKeyParam' + - $ref: '#/parameters/modelVersionParam' + responses: + '204': + description: Delete succuessful + '422': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: key not found + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Invalid server state or unknown error + schema: + $ref: '#/definitions/ErrorModel' + '/projects/metadata/form/{key}/versions/{version}/revisions': + get: + tags: + - form revision + security: + - Bearer: [] + description: get all revision for version. + parameters: + - $ref: '#/parameters/modelKeyParam' + - $ref: '#/parameters/modelVersionParam' + responses: + '200': + description: The model for the particular version + schema: + $ref: '#/definitions/FormListResponse' + '422': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: Model not found + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Invalid server state or unknown error + schema: + $ref: '#/definitions/ErrorModel' + post: + tags: + - form revision + security: + - Bearer: [] + description: create revision for key + parameters: + - $ref: '#/parameters/modelKeyParam' + - $ref: '#/parameters/modelVersionParam' + - in: body + name: body + required: true + schema: + $ref: '#/definitions/NewFormParam' + responses: + '201': + description: The model created + schema: + $ref: '#/definitions/FormResponse' + '422': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: Model not found + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Invalid server state or unknown error + schema: + $ref: '#/definitions/ErrorModel' + '/projects/metadata/form/{key}/versions/{version}/revisions/{revision}': + get: + tags: + - form revision + security: + - Bearer: [] + description: get particular revision for key. + parameters: + - $ref: '#/parameters/modelKeyParam' + - $ref: '#/parameters/modelVersionParam' + - $ref: '#/parameters/modelRevisionParam' + responses: + '200': + description: The model for the particular version + schema: + $ref: '#/definitions/FormResponse' + '422': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: Model not found + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Invalid server state or unknown error + schema: + $ref: '#/definitions/ErrorModel' + delete: + tags: + - form revision + security: + - Bearer: [] + description: delete particular revision + parameters: + - $ref: '#/parameters/modelKeyParam' + - $ref: '#/parameters/modelVersionParam' + - $ref: '#/parameters/modelRevisionParam' + responses: + '204': + description: Delete succuessful + '422': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: Model not found + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Invalid server state or unknown error + schema: + $ref: '#/definitions/ErrorModel' + + '/projects/metadata/priceConfig/{key}': + get: + tags: + - priceConfig version + security: + - Bearer: [] + description: get the latest revision of latest version for key. + parameters: + - $ref: '#/parameters/modelKeyParam' + responses: + '200': + description: The model for the latest revision of latest version + schema: + $ref: '#/definitions/PriceConfigResponse' + '422': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: Model not found + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Invalid server state or unknown error + schema: + $ref: '#/definitions/ErrorModel' + '/projects/metadata/priceConfig/{key}/versions': + get: + tags: + - priceConfig version + security: + - Bearer: [] + description: get all versions for key. + parameters: + - $ref: '#/parameters/modelKeyParam' + responses: + '200': + description: The model list for the all version + schema: + $ref: '#/definitions/PriceConfigListResponse' + '422': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: Model not found + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Invalid server state or unknown error + schema: + $ref: '#/definitions/ErrorModel' + post: + tags: + - priceConfig version + security: + - Bearer: [] + description: create version for key + parameters: + - $ref: '#/parameters/modelKeyParam' + - in: body + name: body + required: true + schema: + $ref: '#/definitions/NewPriceConfigParam' + responses: + '201': + description: The model created + schema: + $ref: '#/definitions/PriceConfigResponse' + '422': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Invalid server state or unknown error + schema: + $ref: '#/definitions/ErrorModel' + '/projects/metadata/priceConfig/{key}/versions/{version}': + get: + tags: + - priceConfig version + security: + - Bearer: [] + description: get particular version for key. + parameters: + - $ref: '#/parameters/modelKeyParam' + - $ref: '#/parameters/modelVersionParam' + responses: + '200': + description: The model for the particular version + schema: + $ref: '#/definitions/PriceConfigResponse' + '422': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: Model not found + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Invalid server state or unknown error + schema: + $ref: '#/definitions/ErrorModel' + patch: + tags: + - priceConfig version + security: + - Bearer: [] + description: update version for key + parameters: + - $ref: '#/parameters/modelKeyParam' + - $ref: '#/parameters/modelVersionParam' + - in: body + name: body + required: true + schema: + $ref: '#/definitions/NewPriceConfigParam' + responses: + '201': + description: The model updated + schema: + $ref: '#/definitions/PriceConfigResponse' + '422': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: Model not found + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Invalid server state or unknown error + schema: + $ref: '#/definitions/ErrorModel' + delete: + tags: + - priceConfig version + security: + - Bearer: [] + description: delete version for key + parameters: + - $ref: '#/parameters/modelKeyParam' + - $ref: '#/parameters/modelVersionParam' + responses: + '204': + description: Delete succuessful + '422': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: Model not found + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Invalid server state or unknown error + schema: + $ref: '#/definitions/ErrorModel' + '/projects/metadata/priceConfig/{key}/versions/{version}/revisions': + get: + tags: + - priceConfig revision + security: + - Bearer: [] + description: get all revision for version. + parameters: + - $ref: '#/parameters/modelKeyParam' + - $ref: '#/parameters/modelVersionParam' + responses: + '200': + description: The model for the particular version + schema: + $ref: '#/definitions/PriceConfigListResponse' + '422': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: Model not found + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Invalid server state or unknown error + schema: + $ref: '#/definitions/ErrorModel' + post: + tags: + - priceConfig revision + security: + - Bearer: [] + description: create revision for key + parameters: + - $ref: '#/parameters/modelKeyParam' + - $ref: '#/parameters/modelVersionParam' + - in: body + name: body + required: true + schema: + $ref: '#/definitions/NewPriceConfigParam' + responses: + '201': + description: The model created + schema: + $ref: '#/definitions/PriceConfigResponse' + '422': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: Model not found + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Invalid server state or unknown error + schema: + $ref: '#/definitions/ErrorModel' + '/projects/metadata/priceConfig/{key}/versions/{version}/revisions/{revision}': + get: + tags: + - priceConfig revision + security: + - Bearer: [] + description: get particular revision for key. + parameters: + - $ref: '#/parameters/modelKeyParam' + - $ref: '#/parameters/modelVersionParam' + - $ref: '#/parameters/modelRevisionParam' + responses: + '200': + description: The model for the particular version + schema: + $ref: '#/definitions/PriceConfigResponse' + '422': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: Model not found + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Invalid server state or unknown error + schema: + $ref: '#/definitions/ErrorModel' + delete: + tags: + - priceConfig revision + security: + - Bearer: [] + description: delete particular revision + parameters: + - $ref: '#/parameters/modelKeyParam' + - $ref: '#/parameters/modelVersionParam' + - $ref: '#/parameters/modelRevisionParam' + responses: + '204': + description: Delete succuessful + '422': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: Model not found + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Invalid server state or unknown error + schema: + $ref: '#/definitions/ErrorModel' + + '/projects/metadata/planConfig/{key}': + get: + tags: + - planConfig version + security: + - Bearer: [] + description: get the latest revision of latest version for key. + parameters: + - $ref: '#/parameters/modelKeyParam' + responses: + '200': + description: The model for the latest revision of latest version + schema: + $ref: '#/definitions/PlanConfigResponse' + '422': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: Model not found + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Invalid server state or unknown error + schema: + $ref: '#/definitions/ErrorModel' + '/projects/metadata/planConfig/{key}/versions': + get: + tags: + - planConfig version + security: + - Bearer: [] + description: get all versions for key. + parameters: + - $ref: '#/parameters/modelKeyParam' + responses: + '200': + description: The model list for the all version + schema: + $ref: '#/definitions/PlanConfigListResponse' + '422': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: Model not found + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Invalid server state or unknown error + schema: + $ref: '#/definitions/ErrorModel' + post: + tags: + - planConfig version + security: + - Bearer: [] + description: create version for key + parameters: + - $ref: '#/parameters/modelKeyParam' + - in: body + name: body + required: true + schema: + $ref: '#/definitions/NewPlanConfigParam' + responses: + '201': + description: The model created + schema: + $ref: '#/definitions/PlanConfigResponse' + '422': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Invalid server state or unknown error + schema: + $ref: '#/definitions/ErrorModel' + '/projects/metadata/planConfig/{key}/versions/{version}': + get: + tags: + - planConfig version + security: + - Bearer: [] + description: get particular version for key. + parameters: + - $ref: '#/parameters/modelKeyParam' + - $ref: '#/parameters/modelVersionParam' + responses: + '200': + description: The model for the particular version + schema: + $ref: '#/definitions/PlanConfigResponse' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" - '200': - description: a milestone template + $ref: '#/definitions/ErrorModel' + '404': + description: Model not found schema: - $ref: "#/definitions/MilestoneTemplateResponse" - operationId: getMilestoneTemplate - + $ref: '#/definitions/ErrorModel' + '500': + description: Invalid server state or unknown error + schema: + $ref: '#/definitions/ErrorModel' patch: tags: - - milestoneTemplates - operationId: updateMilestoneTemplate + - planConfig version security: - Bearer: [] - description: Update a milestone template. Only connect manager, connect admin, and admin can access this endpoint. - responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" - '404': - description: Not found + description: update version for key + parameters: + - $ref: '#/parameters/modelKeyParam' + - $ref: '#/parameters/modelVersionParam' + - in: body + name: body + required: true schema: - $ref: "#/definitions/ErrorModel" - '200': - description: Successfully updated milestone template. + $ref: '#/definitions/NewPlanConfigParam' + responses: + '201': + description: The model updated schema: - $ref: "#/definitions/MilestoneTemplateResponse" + $ref: '#/definitions/PlanConfigResponse' '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" - default: - description: error payload + $ref: '#/definitions/ErrorModel' + '404': + description: Model not found schema: $ref: '#/definitions/ErrorModel' - parameters: - - name: body - in: body - required: true + '500': + description: Invalid server state or unknown error schema: - $ref: "#/definitions/MilestoneTemplateBodyParam" - + $ref: '#/definitions/ErrorModel' delete: tags: - - milestoneTemplates - description: Remove an existing milestone template. Only connect manager, connect admin, and admin can access this endpoint. + - planConfig version security: - Bearer: [] + description: delete version for key + parameters: + - $ref: '#/parameters/modelKeyParam' + - $ref: '#/parameters/modelVersionParam' responses: - '403': - description: No permission or wrong token + '204': + description: Delete succuessful + '422': + description: Invalid input schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '404': - description: Not found + description: Model not found schema: - $ref: "#/definitions/ErrorModel" - '422': - description: Invalid input + $ref: '#/definitions/ErrorModel' + '500': + description: Invalid server state or unknown error schema: - $ref: "#/definitions/ErrorModel" - '204': - description: Milestone template successfully removed - - /projects/{projectId}/members/invite: + $ref: '#/definitions/ErrorModel' + '/projects/metadata/planConfig/{key}/versions/{version}/revisions': get: tags: - - project member invite - operationId: getCurrentUserInvite + - planConfig revision security: - Bearer: [] - description: Retrieve the invite for current user. + description: get all revision for version. parameters: - - $ref: "#/parameters/projectIdParam" + - $ref: '#/parameters/modelKeyParam' + - $ref: '#/parameters/modelVersionParam' responses: '200': - description: The invite for current user - schema: - $ref: "#/definitions/ProjectMemberInviteResponse" - '403': - description: No permission or wrong token + description: The model for the particular version schema: - $ref: "#/definitions/ErrorModel" - '400': + $ref: '#/definitions/PlanConfigListResponse' + '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '404': - description: Invite not found + description: Model not found schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '500': description: Invalid server state or unknown error schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' post: tags: - - project member invite - operationId: addProjectMemberInvite + - planConfig revision security: - Bearer: [] - description: Create an invite. All users who can access this endpoint, however more restriction will be applied based on role to be added. + description: create revision for key parameters: - - $ref: "#/parameters/projectIdParam" + - $ref: '#/parameters/modelKeyParam' + - $ref: '#/parameters/modelVersionParam' - in: body name: body required: true schema: - $ref: '#/definitions/AddProjectMemberInvitesRequest' + $ref: '#/definitions/NewPlanConfigParam' responses: '201': - description: Returns the newly created invite - schema: - $ref: "#/definitions/ProjectMemberInviteResponse" - '403': - description: No permission or wrong token + description: The model created schema: - $ref: "#/definitions/ErrorModel" - '400': + $ref: '#/definitions/PlanConfigResponse' + '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' + '404': + description: Model not found + schema: + $ref: '#/definitions/ErrorModel' '500': description: Invalid server state or unknown error schema: - $ref: "#/definitions/ErrorModel" - put: + $ref: '#/definitions/ErrorModel' + '/projects/metadata/planConfig/{key}/versions/{version}/revisions/{revision}': + get: tags: - - project member invite - operationId: updateProjectMemberInvite + - planConfig revision security: - Bearer: [] - description: Update an invite. All users who can access this endpoint, however more restriction will be applied based on role to be updated. + description: get particular revision for key. parameters: - - $ref: "#/parameters/projectIdParam" - - in: body - name: body - required: true - schema: - $ref: '#/definitions/UpdateProjectMemberInviteRequest' + - $ref: '#/parameters/modelKeyParam' + - $ref: '#/parameters/modelVersionParam' + - $ref: '#/parameters/modelRevisionParam' responses: '200': - description: Returns the newly updated invite + description: The model for the particular version schema: - $ref: "#/definitions/ProjectMemberInviteResponse" - '400': + $ref: '#/definitions/PlanConfigResponse' + '422': description: Invalid input schema: - $ref: "#/definitions/ErrorModel" - '403': - description: No permission or wrong token + $ref: '#/definitions/ErrorModel' + '404': + description: Model not found + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Invalid server state or unknown error + schema: + $ref: '#/definitions/ErrorModel' + delete: + tags: + - planConfig revision + security: + - Bearer: [] + description: delete particular revision + parameters: + - $ref: '#/parameters/modelKeyParam' + - $ref: '#/parameters/modelVersionParam' + - $ref: '#/parameters/modelRevisionParam' + responses: + '204': + description: Delete succuessful + '422': + description: Invalid input + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: Model not found schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' '500': description: Invalid server state or unknown error schema: - $ref: "#/definitions/ErrorModel" + $ref: '#/definitions/ErrorModel' + parameters: projectIdParam: name: projectId @@ -1955,19 +2866,52 @@ parameters: format: int64 offsetParam: name: offset - description: "number of items to skip. Defaults to 0" + description: number of items to skip. Defaults to 0 in: query required: false type: integer format: int32 limitParam: name: limit - description: "max records to return. Defaults to 20" + description: max records to return. Defaults to 20 in: query required: false type: integer format: int32 - + modelKeyParam: + name: key + in: path + description: model key identifier + required: true + type: string + modelVersionParam: + name: version + in: path + description: model version identifier + required: true + type: integer + format: int32 + modelRevisionParam: + name: revision + in: path + description: model revision identifier + required: true + type: integer + format: int32 + versionIdParam: + name: versionId + in: path + description: version identifier + required: true + type: integer + format: int64 + revisionIdParam: + name: revisionId + in: path + description: revision identifier + required: true + type: integer + format: int64 definitions: ResponseMetadata: title: Metadata object for a response @@ -1977,7 +2921,6 @@ definitions: type: integer format: int64 description: Total count of the objects - ErrorModel: type: object properties: @@ -1999,7 +2942,6 @@ definitions: type: object content: type: object - ProjectBookMark: title: Project bookmark type: object @@ -2008,25 +2950,22 @@ definitions: type: string address: type: string - ProjectBodyParam: type: object properties: param: - $ref: "#/definitions/Project" - + $ref: '#/definitions/Project' ProjectUpgradeBodyParam: type: object properties: param: - $ref: "#/definitions/ProjectUpgrade" - + $ref: '#/definitions/ProjectUpgrade' NewProject: type: object required: - - name - - description - - type + - name + - description + - type properties: name: type: string @@ -2049,7 +2988,7 @@ definitions: format: integer external: type: object - description: READ-ONLY, OPTIONAL. Refernce to external task/issue. + description: 'READ-ONLY, OPTIONAL. Refernce to external task/issue.' properties: id: type: string @@ -2057,24 +2996,28 @@ definitions: type: type: string description: external source type - enum: [ "github", "jira", "asana", "other"] + enum: + - github + - jira + - asana + - other data: type: string - description: "300 Char length text blob for customer provided data" + description: 300 Char length text blob for customer provided data type: type: string description: project type bookmarks: type: array items: - $ref: "#/definitions/ProjectBookMark" + $ref: '#/definitions/ProjectBookMark' challengeEligibility: description: List of eligibility criteria (one entry per role) type: array items: - $ref: "#/definitions/ChallengeEligibility" + $ref: '#/definitions/ChallengeEligibility' details: - $ref: "#/definitions/ProjectDetails" + $ref: '#/definitions/ProjectDetails' utm: description: READ-ONLY. Used for tracking type: object @@ -2089,32 +3032,31 @@ definitions: description: the project template identifier type: number format: long - - NewProjectBodyParam: type: object properties: param: - $ref: "#/definitions/NewProject" - + $ref: '#/definitions/NewProject' ChallengeEligibility: description: Object describing who is eligible to work on this task type: object properties: - role: - type: string - enum: ["submitter", "reviewer", "copilot"] - users: - type: array - items: - type: integer - format: int64 - groups: - type: array - items: - type: integer - format: int64 - + role: + type: string + enum: + - submitter + - reviewer + - copilot + users: + type: array + items: + type: integer + format: int64 + groups: + type: array + items: + type: integer + format: int64 Project: type: object @@ -2160,10 +3102,9 @@ definitions: description: type: string description: Project description - external: type: object - description: READ-ONLY, OPTIONAL. Refernce to external task/issue. + description: 'READ-ONLY, OPTIONAL. Refernce to external task/issue.' properties: id: type: string @@ -2171,50 +3112,60 @@ definitions: type: type: string description: external source type - enum: [ "github", "jira", "asana", "other"] + enum: + - github + - jira + - asana + - other data: type: string - description: "300 Char length text blob for customer provided data" + description: 300 Char length text blob for customer provided data type: type: string description: project type status: type: string description: current state of the task - enum: ["draft", "in_review", "reviewed", "active", "paused", "cancelled", "completed"] + enum: + - draft + - in_review + - reviewed + - active + - paused + - cancelled + - completed cancelReason: type: string - description: If a project is cancelled, define the reason of cancellation + description: 'If a project is cancelled, define the reason of cancellation' challengeEligibility: description: List of eligibility criteria (one entry per role) type: array items: - $ref: "#/definitions/ChallengeEligibility" + $ref: '#/definitions/ChallengeEligibility' bookmarks: type: array items: - $ref: "#/definitions/ProjectBookMark" + $ref: '#/definitions/ProjectBookMark' members: description: | READ-ONLY. List of project members. Use project member api to add/remove members type: array items: - $ref: "#/definitions/ProjectMember" + $ref: '#/definitions/ProjectMember' attachments: description: | READ-ONLY. List of project attachmens. Use project attachment api to add/remove attachments type: array items: - $ref: "#/definitions/ProjectAttachment" + $ref: '#/definitions/ProjectAttachment' details: - $ref: "#/definitions/ProjectDetails" + $ref: '#/definitions/ProjectDetails' templateId: description: the project template identifier type: number format: long - createdAt: type: string description: Datetime (GMT) when task was created @@ -2233,7 +3184,6 @@ definitions: format: int64 description: READ-ONLY. User that last updated this task readOnly: true - ProjectDetails: description: Project details type: object @@ -2255,7 +3205,6 @@ definitions: type: string isCustom: type: boolean - ProjectUpgrade: title: Project Upgrade object type: object @@ -2269,11 +3218,15 @@ definitions: defaultProductTemplateId: type: number format: int64 - description: Default product template id, used when the associated project template is not found, or there's no matching phase with the project's product id + description: >- + Default product template id, used when the associated project template + is not found, or there's no matching phase with the project's product + id phaseName: type: string - description: This value will be used instead of the product template's name for the created ProjectPhase - + description: >- + This value will be used instead of the product template's name for the + created ProjectPhase NewProjectMember: title: Project Member object type: object @@ -2291,34 +3244,36 @@ definitions: role: type: string description: member role on specified project - enum: ["customer", "manager", "copilot"] - + enum: + - customer + - manager + - copilot NewProjectMemberBodyParam: type: object properties: param: - $ref: "#/definitions/NewProjectMember" - + $ref: '#/definitions/NewProjectMember' UpdateProjectMember: - title: Project Member object - type: object - required: - - role - properties: - isPrimary: - type: boolean - description: primary option - role: - type: string - description: member role on specified project - enum: ["customer", "manager", "copilot"] - + title: Project Member object + type: object + required: + - role + properties: + isPrimary: + type: boolean + description: primary option + role: + type: string + description: member role on specified project + enum: + - customer + - manager + - copilot UpdateProjectMemberBodyParam: type: object properties: param: - $ref: "#/definitions/UpdateProjectMember" - + $ref: '#/definitions/UpdateProjectMember' NewProjectAttachment: title: Project attachment request type: object @@ -2356,13 +3311,11 @@ definitions: type: integer format: int64 description: Users allowed to access the attachment - NewProjectAttachmentBodyParam: type: object properties: param: - $ref: "#/definitions/NewProjectAttachment" - + $ref: '#/definitions/NewProjectAttachment' NewProjectAttachmentResponse: title: Project attachment object response type: object @@ -2381,8 +3334,7 @@ definitions: type: string description: http status code content: - $ref: "#/definitions/ProjectAttachment" - + $ref: '#/definitions/ProjectAttachment' ProjectAttachment: title: Project attachment type: object @@ -2427,7 +3379,6 @@ definitions: format: int64 description: READ-ONLY. User that last updated this task readOnly: true - ProjectMember: title: Project Member object type: object @@ -2449,7 +3400,10 @@ definitions: role: type: string description: member role on specified project - enum: ["customer", "manager", "copilot"] + enum: + - customer + - manager + - copilot createdAt: type: string description: Datetime (GMT) when task was created @@ -2468,9 +3422,6 @@ definitions: format: int64 description: READ-ONLY. User that last updated this task readOnly: true - - - NewProjectMemberResponse: title: Project member object response type: object @@ -2489,8 +3440,7 @@ definitions: type: string description: http status code content: - $ref: "#/definitions/ProjectMember" - + $ref: '#/definitions/ProjectMember' UpdateProjectMemberResponse: title: Project member object response type: object @@ -2509,9 +3459,7 @@ definitions: type: string description: http status code content: - $ref: "#/definitions/ProjectMember" - - + $ref: '#/definitions/ProjectMember' ProjectResponse: title: Single project object type: object @@ -2530,8 +3478,7 @@ definitions: type: string description: http status code content: - $ref: "#/definitions/Project" - + $ref: '#/definitions/Project' UpdateProjectResponse: title: response with original and updated project object type: object @@ -2553,10 +3500,9 @@ definitions: type: object properties: original: - $ref: "#/definitions/Project" + $ref: '#/definitions/Project' updated: - $ref: "#/definitions/Project" - + $ref: '#/definitions/Project' ProjectListResponse: title: List response type: object @@ -2576,12 +3522,11 @@ definitions: type: string description: http status code metadata: - $ref: "#/definitions/ResponseMetadata" + $ref: '#/definitions/ResponseMetadata' content: type: array items: - $ref: "#/definitions/Project" - + $ref: '#/definitions/Project' ProjectTemplateRequest: title: Project template request object type: object @@ -2589,8 +3534,6 @@ definitions: - name - key - category - - scope - - phases properties: name: type: string @@ -2607,7 +3550,35 @@ definitions: phases: type: object description: the project template phases - + form: + $ref: '#/definitions/VersionModelParam' + priceConfig: + $ref: '#/definitions/VersionModelParam' + planConfig: + $ref: '#/definitions/VersionModelParam' + ProjectTemplateUpgradeBodyParam: + title: Project template + type: object + properties: + param: + type: object + properties: + form: + $ref: '#/definitions/VersionModelParam' + priceConfig: + $ref: '#/definitions/VersionModelParam' + planConfig: + $ref: '#/definitions/VersionModelParam' + VersionModelParam: + title: version model param + type: object + properties: + key: + type: string + description: the key for model + version: + type: number + description: the version for model ProjectTemplateBodyParam: title: Project template body param type: object @@ -2615,8 +3586,7 @@ definitions: - param properties: param: - $ref: "#/definitions/ProjectTemplateRequest" - + $ref: '#/definitions/ProjectTemplateRequest' ProjectTemplate: title: Project template object allOf: @@ -2650,9 +3620,7 @@ definitions: format: int64 description: READ-ONLY. User that last updated this object readOnly: true - - $ref: "#/definitions/ProjectTemplateRequest" - - + - $ref: '#/definitions/ProjectTemplateRequest' ProjectTemplateResponse: title: Single project template response object type: object @@ -2671,10 +3639,9 @@ definitions: type: string description: http status code metadata: - $ref: "#/definitions/ResponseMetadata" + $ref: '#/definitions/ResponseMetadata' content: - $ref: "#/definitions/ProjectTemplate" - + $ref: '#/definitions/ProjectTemplate' ProjectTemplateListResponse: title: Project template list response object type: object @@ -2694,12 +3661,11 @@ definitions: type: string description: http status code metadata: - $ref: "#/definitions/ResponseMetadata" + $ref: '#/definitions/ResponseMetadata' content: type: array items: - $ref: "#/definitions/ProjectTemplate" - + $ref: '#/definitions/ProjectTemplate' ProductTemplateRequest: title: Product template request object type: object @@ -2709,6 +3675,9 @@ definitions: - category - scope - phases + - form + - priceConfig + - phaseConfig properties: name: type: string @@ -2734,7 +3703,6 @@ definitions: template: type: object description: the product template template - ProductTemplateBodyParam: title: Product template body param type: object @@ -2742,8 +3710,7 @@ definitions: - param properties: param: - $ref: "#/definitions/ProductTemplateRequest" - + $ref: '#/definitions/ProductTemplateRequest' ProductTemplate: title: Product template object allOf: @@ -2781,8 +3748,7 @@ definitions: category: type: string description: The product category of the product template - - $ref: "#/definitions/ProductTemplateRequest" - + - $ref: '#/definitions/ProductTemplateRequest' ProjectUpgradeResponse: title: Project upgrade response object type: object @@ -2801,8 +3767,7 @@ definitions: type: string description: http status code metadata: - $ref: "#/definitions/ResponseMetadata" - + $ref: '#/definitions/ResponseMetadata' ProductTemplateResponse: title: Single product template response object type: object @@ -2821,10 +3786,9 @@ definitions: type: string description: http status code metadata: - $ref: "#/definitions/ResponseMetadata" + $ref: '#/definitions/ResponseMetadata' content: - $ref: "#/definitions/ProductTemplate" - + $ref: '#/definitions/ProductTemplate' ProductTemplateListResponse: title: Product template list response object type: object @@ -2844,12 +3808,11 @@ definitions: type: string description: http status code metadata: - $ref: "#/definitions/ResponseMetadata" + $ref: '#/definitions/ResponseMetadata' content: type: array items: - $ref: "#/definitions/ProductTemplate" - + $ref: '#/definitions/ProductTemplate' ProjectPhaseRequest: title: Project phase request object type: object @@ -2886,7 +3849,6 @@ definitions: type: number format: integer description: the project phase order - ProjectPhaseBodyParam: title: Project phase body param type: object @@ -2894,8 +3856,7 @@ definitions: - param properties: param: - $ref: "#/definitions/ProjectPhaseRequest" - + $ref: '#/definitions/ProjectPhaseRequest' ProjectPhase: title: Project phase object allOf: @@ -2929,9 +3890,7 @@ definitions: format: int64 description: READ-ONLY. User that last updated this object readOnly: true - - $ref: "#/definitions/ProjectPhaseRequest" - - + - $ref: '#/definitions/ProjectPhaseRequest' ProjectPhaseResponse: title: Single project phase response object type: object @@ -2950,10 +3909,9 @@ definitions: type: string description: http status code metadata: - $ref: "#/definitions/ResponseMetadata" + $ref: '#/definitions/ResponseMetadata' content: - $ref: "#/definitions/ProjectPhase" - + $ref: '#/definitions/ProjectPhase' ProjectPhaseListResponse: title: Project phase list response object type: object @@ -2973,13 +3931,11 @@ definitions: type: string description: http status code metadata: - $ref: "#/definitions/ResponseMetadata" + $ref: '#/definitions/ResponseMetadata' content: type: array items: - $ref: "#/definitions/ProjectPhase" - - + $ref: '#/definitions/ProjectPhase' PhaseProductRequest: title: Phase product request object type: object @@ -3008,7 +3964,6 @@ definitions: details: type: object description: the phase product details - PhaseProductBodyParam: title: Phase product body param type: object @@ -3016,8 +3971,7 @@ definitions: - param properties: param: - $ref: "#/definitions/PhaseProductRequest" - + $ref: '#/definitions/PhaseProductRequest' PhaseProduct: title: Phase product object allOf: @@ -3051,9 +4005,7 @@ definitions: format: int64 description: READ-ONLY. User that last updated this object readOnly: true - - $ref: "#/definitions/PhaseProductRequest" - - + - $ref: '#/definitions/PhaseProductRequest' PhaseProductResponse: title: Single phase product response object type: object @@ -3072,10 +4024,9 @@ definitions: type: string description: http status code metadata: - $ref: "#/definitions/ResponseMetadata" + $ref: '#/definitions/ResponseMetadata' content: - $ref: "#/definitions/PhaseProduct" - + $ref: '#/definitions/PhaseProduct' PhaseProductListResponse: title: Phase product list response object type: object @@ -3095,14 +4046,11 @@ definitions: type: string description: http status code metadata: - $ref: "#/definitions/ResponseMetadata" + $ref: '#/definitions/ResponseMetadata' content: type: array items: - $ref: "#/definitions/PhaseProduct" - - - + $ref: '#/definitions/PhaseProduct' ProductCategoryRequest: title: Product category request object type: object @@ -3112,7 +4060,6 @@ definitions: displayName: type: string description: the product category display name - ProductCategoryBodyParam: title: Product category body param type: object @@ -3120,8 +4067,7 @@ definitions: - param properties: param: - $ref: "#/definitions/ProductCategoryRequest" - + $ref: '#/definitions/ProductCategoryRequest' ProductCategoryCreateRequest: title: Product category creation request object type: object @@ -3133,8 +4079,7 @@ definitions: key: type: string description: the product category key - - $ref: "#/definitions/ProductCategoryRequest" - + - $ref: '#/definitions/ProductCategoryRequest' ProductCategoryCreateBodyParam: title: Product category creation body param type: object @@ -3142,8 +4087,7 @@ definitions: - param properties: param: - $ref: "#/definitions/ProductCategoryCreateRequest" - + $ref: '#/definitions/ProductCategoryCreateRequest' ProductCategory: title: Product category object allOf: @@ -3175,9 +4119,7 @@ definitions: format: int64 description: READ-ONLY. User that last updated this object readOnly: true - - $ref: "#/definitions/ProductCategoryCreateRequest" - - + - $ref: '#/definitions/ProductCategoryCreateRequest' ProductCategoryResponse: title: Single product category response object type: object @@ -3196,10 +4138,9 @@ definitions: type: string description: http status code metadata: - $ref: "#/definitions/ResponseMetadata" + $ref: '#/definitions/ResponseMetadata' content: - $ref: "#/definitions/ProductCategory" - + $ref: '#/definitions/ProductCategory' ProductCategoryListResponse: title: Product category list response object type: object @@ -3219,13 +4160,11 @@ definitions: type: string description: http status code metadata: - $ref: "#/definitions/ResponseMetadata" + $ref: '#/definitions/ResponseMetadata' content: type: array items: - $ref: "#/definitions/ProductCategory" - - + $ref: '#/definitions/ProductCategory' ProjectTypeRequest: title: Project type request object type: object @@ -3235,7 +4174,6 @@ definitions: displayName: type: string description: the project type display name - ProjectTypeBodyParam: title: Project type body param type: object @@ -3243,8 +4181,7 @@ definitions: - param properties: param: - $ref: "#/definitions/ProjectTypeRequest" - + $ref: '#/definitions/ProjectTypeRequest' ProjectTypeCreateRequest: title: Project type creation request object type: object @@ -3256,8 +4193,7 @@ definitions: key: type: string description: the project type key - - $ref: "#/definitions/ProjectTypeRequest" - + - $ref: '#/definitions/ProjectTypeRequest' ProjectTypeCreateBodyParam: title: Project type creation body param type: object @@ -3265,8 +4201,7 @@ definitions: - param properties: param: - $ref: "#/definitions/ProjectTypeCreateRequest" - + $ref: '#/definitions/ProjectTypeCreateRequest' ProjectType: title: Project type object allOf: @@ -3298,8 +4233,7 @@ definitions: format: int64 description: READ-ONLY. User that last updated this object readOnly: true - - $ref: "#/definitions/ProjectTypeCreateRequest" - + - $ref: '#/definitions/ProjectTypeCreateRequest' OrgConfigRequest: title: Organization config request object type: object @@ -3313,7 +4247,6 @@ definitions: configName: type: string description: the organization config name - OrgConfigCreateRequest: title: Organization config creation request object type: object @@ -3323,8 +4256,7 @@ definitions: configValue: type: string description: the organization config id - - $ref: "#/definitions/OrgConfigRequest" - + - $ref: '#/definitions/OrgConfigRequest' OrgConfigCreateBodyParam: title: Organization config creation body param type: object @@ -3332,8 +4264,7 @@ definitions: - param properties: param: - $ref: "#/definitions/OrgConfigCreateRequest" - + $ref: '#/definitions/OrgConfigCreateRequest' OrgConfig: title: Organization config object allOf: @@ -3378,8 +4309,7 @@ definitions: format: int64 description: READ-ONLY. User that last updated this object readOnly: true - - $ref: "#/definitions/OrgConfigCreateRequest" - + - $ref: '#/definitions/OrgConfigCreateRequest' OrgConfigResponse: title: Single organization config response object type: object @@ -3398,10 +4328,9 @@ definitions: type: string description: http status code metadata: - $ref: "#/definitions/ResponseMetadata" + $ref: '#/definitions/ResponseMetadata' content: - $ref: "#/definitions/OrgConfig" - + $ref: '#/definitions/OrgConfig' OrgConfigListResponse: title: Organization confige list response object type: object @@ -3421,12 +4350,11 @@ definitions: type: string description: http status code metadata: - $ref: "#/definitions/ResponseMetadata" + $ref: '#/definitions/ResponseMetadata' content: type: array items: - $ref: "#/definitions/OrgConfig" - + $ref: '#/definitions/OrgConfig' ProjectTypeResponse: title: Single project type response object type: object @@ -3445,10 +4373,9 @@ definitions: type: string description: http status code metadata: - $ref: "#/definitions/ResponseMetadata" + $ref: '#/definitions/ResponseMetadata' content: - $ref: "#/definitions/ProjectType" - + $ref: '#/definitions/ProjectType' ProjectTypeListResponse: title: Project type list response object type: object @@ -3468,13 +4395,11 @@ definitions: type: string description: http status code metadata: - $ref: "#/definitions/ResponseMetadata" + $ref: '#/definitions/ResponseMetadata' content: type: array items: - $ref: "#/definitions/ProjectType" - - + $ref: '#/definitions/ProjectType' TimelineRequest: title: Timeline request object type: object @@ -3507,8 +4432,9 @@ definitions: referenceId: type: number format: long - description: the timeline reference id (project id or phase id, corresponding to the `reference`) - + description: >- + the timeline reference id (project id or phase id, corresponding to + the `reference`) TimelineBodyParam: title: Timeline body param type: object @@ -3516,8 +4442,7 @@ definitions: - param properties: param: - $ref: "#/definitions/TimelineRequest" - + $ref: '#/definitions/TimelineRequest' Timeline: title: Timeline object allOf: @@ -3551,8 +4476,7 @@ definitions: format: int64 description: READ-ONLY. User that last updated this object readOnly: true - - $ref: "#/definitions/TimelineRequest" - + - $ref: '#/definitions/TimelineRequest' TimelineResponse: title: Single timeline response object type: object @@ -3571,10 +4495,9 @@ definitions: type: string description: http status code metadata: - $ref: "#/definitions/ResponseMetadata" + $ref: '#/definitions/ResponseMetadata' content: - $ref: "#/definitions/Timeline" - + $ref: '#/definitions/Timeline' TimelineListResponse: title: Timeline list response object type: object @@ -3594,12 +4517,11 @@ definitions: type: string description: http status code metadata: - $ref: "#/definitions/ResponseMetadata" + $ref: '#/definitions/ResponseMetadata' content: type: array items: - $ref: "#/definitions/Timeline" - + $ref: '#/definitions/Timeline' MilestonePostRequest: title: Milestone request object type: object @@ -3662,7 +4584,6 @@ definitions: blockedText: type: string description: the milestone blocked text - MilestonePatchRequest: title: Milestone request object type: object @@ -3716,7 +4637,6 @@ definitions: blockedText: type: string description: the milestone blocked text - MilestonePostBodyParam: title: Milestone body param type: object @@ -3724,8 +4644,7 @@ definitions: - param properties: param: - $ref: "#/definitions/MilestonePostRequest" - + $ref: '#/definitions/MilestonePostRequest' MilestonePatchBodyParam: title: Milestone body param type: object @@ -3733,8 +4652,7 @@ definitions: - param properties: param: - $ref: "#/definitions/MilestonePatchRequest" - + $ref: '#/definitions/MilestonePatchRequest' Milestone: title: Milestone object allOf: @@ -3768,8 +4686,7 @@ definitions: format: int64 description: READ-ONLY. User that last updated this object readOnly: true - - $ref: "#/definitions/MilestonePostRequest" - + - $ref: '#/definitions/MilestonePostRequest' MilestoneResponse: title: Single milestone response object type: object @@ -3788,10 +4705,9 @@ definitions: type: string description: http status code metadata: - $ref: "#/definitions/ResponseMetadata" + $ref: '#/definitions/ResponseMetadata' content: - $ref: "#/definitions/Milestone" - + $ref: '#/definitions/Milestone' MilestoneListResponse: title: Milestone list response object type: object @@ -3811,13 +4727,11 @@ definitions: type: string description: http status code metadata: - $ref: "#/definitions/ResponseMetadata" + $ref: '#/definitions/ResponseMetadata' content: type: array items: - $ref: "#/definitions/Milestone" - - + $ref: '#/definitions/Milestone' MilestoneTemplateRequest: title: Milestone template request object type: object @@ -3850,7 +4764,7 @@ definitions: reference: type: string enum: - - productTemplate + - productTemplate description: the milestone template reference refereneceId: type: number @@ -3860,7 +4774,6 @@ definitions: metadata: type: object description: the milestone template metadata - MilestoneTemplateBodyParam: title: Milestone template body param type: object @@ -3868,8 +4781,7 @@ definitions: - param properties: param: - $ref: "#/definitions/MilestoneTemplateRequest" - + $ref: '#/definitions/MilestoneTemplateRequest' MilestoneCloneTemplateRequest: title: Milestone clone template request object type: object @@ -3882,7 +4794,7 @@ definitions: sourceReference: type: string enum: - - productTemplate + - productTemplate description: the source reference to clone the milestone templates from sourceReferenceId: type: number @@ -3892,14 +4804,13 @@ definitions: reference: type: string enum: - - productTemplate + - productTemplate description: the target reference to clone the milestone templates to refereneceId: type: number format: long minimum: 1 description: the target reference id to clone the milestone templates to - MilestoneTemplate: title: Milestone template object allOf: @@ -3933,8 +4844,7 @@ definitions: format: int64 description: READ-ONLY. User that last updated this object readOnly: true - - $ref: "#/definitions/MilestoneTemplateRequest" - + - $ref: '#/definitions/MilestoneTemplateRequest' MilestoneTemplateResponse: title: Single milestone template response object type: object @@ -3953,10 +4863,9 @@ definitions: type: string description: http status code metadata: - $ref: "#/definitions/ResponseMetadata" + $ref: '#/definitions/ResponseMetadata' content: - $ref: "#/definitions/MilestoneTemplate" - + $ref: '#/definitions/MilestoneTemplate' MilestoneTemplateListResponse: title: Milestone template list response object type: object @@ -3976,12 +4885,11 @@ definitions: type: string description: http status code metadata: - $ref: "#/definitions/ResponseMetadata" + $ref: '#/definitions/ResponseMetadata' content: type: array items: - $ref: "#/definitions/MilestoneTemplate" - + $ref: '#/definitions/MilestoneTemplate' AllMetadataResponse: title: All metadata response object type: object @@ -4001,31 +4909,30 @@ definitions: type: string description: http status code metadata: - $ref: "#/definitions/ResponseMetadata" + $ref: '#/definitions/ResponseMetadata' content: type: object properties: projectTemplates: type: array items: - $ref: "#/definitions/ProjectTemplate" + $ref: '#/definitions/ProjectTemplate' productTemplates: type: array items: - $ref: "#/definitions/ProductTemplate" + $ref: '#/definitions/ProductTemplate' milestoneTemplates: type: array items: - $ref: "#/definitions/MilestoneTemplate" + $ref: '#/definitions/MilestoneTemplate' projectTypes: type: array items: - $ref: "#/definitions/ProjectType" + $ref: '#/definitions/ProjectType' productCategories: type: array items: - $ref: "#/definitions/ProductCategory" - + $ref: '#/definitions/ProductCategory' ProjectMemberInvite: type: object properties: @@ -4047,11 +4954,18 @@ definitions: role: description: The user role in the project type: string - enum: ["manager", "customer", "copilot"] + enum: + - manager + - customer + - copilot status: description: The invite status type: string - enum: ["pending", "accepted", "refused", "canceled"] + enum: + - pending + - accepted + - refused + - canceled createdAt: type: string description: Datetime (GMT) when task was created @@ -4070,26 +4984,24 @@ definitions: format: int64 description: READ-ONLY. User that last updated this task readOnly: true - ProjectMemberInviteSuccessAndFailure: type: object properties: success: - $ref: "#/definitions/ProjectMemberInvite" + $ref: '#/definitions/ProjectMemberInvite' failed: type: array items: type: object - AddProjectMemberInvitesRequest: title: Add project member invites request object type: object properties: - param: + param: type: object properties: userIds: - description: The user Id list, could not present with emails + description: 'The user Id list, could not present with emails' type: array items: type: integer @@ -4098,12 +5010,14 @@ definitions: type: array items: type: string - description: The user email list, could not present with userIds + description: 'The user email list, could not present with userIds' role: description: The target role in the project type: string - enum: ["manager", "customer", "copilot"] - + enum: + - manager + - customer + - copilot UpdateProjectMemberInviteRequest: title: Update project member invite request object type: object @@ -4114,15 +5028,18 @@ definitions: userId: type: integer format: int64 - description: The user Id, could not present with email + description: 'The user Id, could not present with email' email: type: string - description: The user email, could not present with userId + description: 'The user email, could not present with userId' status: description: The invite status type: string - enum: ["pending", "accepted", "refused", "canceled"] - + enum: + - pending + - accepted + - refused + - canceled ProjectMemberInviteResponse: title: Project member invite response object type: object @@ -4141,6 +5058,269 @@ definitions: type: string description: http status code metadata: - $ref: "#/definitions/ResponseMetadata" + $ref: '#/definitions/ResponseMetadata' + content: + $ref: '#/definitions/ProjectMemberInviteSuccessAndFailure' + Form: + type: object + properties: + id: + description: unique identifier + type: integer + format: int64 + version: + description: version identifier + type: integer + format: int64 + revision: + description: revision identifier + type: integer + format: int64 + scope: + description: scope json + type: object + key: + description: key of form + format: string + createdAt: + type: string + description: Datetime (GMT) when task was created + readOnly: true + createdBy: + type: integer + format: int64 + description: READ-ONLY. User who created this task + readOnly: true + updatedAt: + type: string + description: READ-ONLY. Datetime (GMT) when task was updated + readOnly: true + updatedBy: + type: integer + format: int64 + description: READ-ONLY. User that last updated this task + readOnly: true + FormResponse: + title: Form response + type: object + properties: + id: + type: string + description: unique id identifying the request + version: + type: string + result: + type: object + properties: + success: + type: boolean + status: + type: string + description: http status code + content: + $ref: '#/definitions/Form' + FormListResponse: + title: From list response + type: object + properties: + id: + type: string + description: unique id identifying the request + version: + type: string + result: + type: object + properties: + success: + type: boolean + status: + type: string + description: http status code + content: + type: array + items: + $ref: '#/definitions/Form' + NewFormParam: + type: object + properties: + param: + type: object + properties: + scope: + type: object + PriceConfig: + type: object + properties: + id: + description: unique identifier + type: integer + format: int64 + version: + description: version identifier + type: integer + format: int64 + revision: + description: revision identifier + type: integer + format: int64 + config: + description: content json + type: object + key: + description: key + format: string + createdAt: + type: string + description: Datetime (GMT) when task was created + readOnly: true + createdBy: + type: integer + format: int64 + description: READ-ONLY. User who created this task + readOnly: true + updatedAt: + type: string + description: READ-ONLY. Datetime (GMT) when task was updated + readOnly: true + updatedBy: + type: integer + format: int64 + description: READ-ONLY. User that last updated this task + readOnly: true + PriceConfigResponse: + title: PriceConfig response + type: object + properties: + id: + type: string + description: unique id identifying the request + version: + type: string + result: + type: object + properties: + success: + type: boolean + status: + type: string + description: http status code + content: + $ref: '#/definitions/PriceConfig' + PriceConfigListResponse: + title: PriceConfig list response + type: object + properties: + id: + type: string + description: unique id identifying the request + version: + type: string + result: + type: object + properties: + success: + type: boolean + status: + type: string + description: http status code + content: + type: array + items: + $ref: '#/definitions/Form' + NewPriceConfigParam: + type: object + properties: + param: + type: object + properties: + config: + description: config json + type: object + PlanConfig: + type: object + properties: + id: + description: unique identifier + type: integer + format: int64 + version: + description: version identifier + type: integer + format: int64 + revision: + description: revision identifier + type: integer + format: int64 + phases: + description: content json + type: object + key: + description: key + format: string + createdAt: + type: string + description: Datetime (GMT) when task was created + readOnly: true + createdBy: + type: integer + format: int64 + description: READ-ONLY. User who created this task + readOnly: true + updatedAt: + type: string + description: READ-ONLY. Datetime (GMT) when task was updated + readOnly: true + updatedBy: + type: integer + format: int64 + description: READ-ONLY. User that last updated this task + readOnly: true + PlanConfigResponse: + title: PlanConfig response + type: object + properties: + id: + type: string + description: unique id identifying the request + version: + type: string + result: + type: object + properties: + success: + type: boolean + status: + type: string + description: http status code + content: + $ref: '#/definitions/PlanConfig' + PlanConfigListResponse: + title: PlanConfig list response + type: object + properties: + id: + type: string + description: unique id identifying the request + version: + type: string + result: + type: object + properties: + success: + type: boolean + status: + type: string + description: http status code content: - $ref: "#/definitions/ProjectMemberInviteSuccessAndFailure" + type: array + items: + $ref: '#/definitions/PlanConfig' + NewPlanConfigParam: + type: object + properties: + param: + type: object + properties: + phases: + description: config json + type: object From 74a3f2477173f412dcd2c763a5229eb323590940 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Wed, 27 Mar 2019 14:58:01 +0800 Subject: [PATCH 34/48] fixed few spelling issues --- src/models/form.js | 2 +- src/models/planConfig.js | 2 +- src/models/priceConfig.js | 2 +- src/models/versionModelClassMethods.js | 2 +- src/routes/form/version/get.js | 2 +- src/routes/form/version/update.js | 4 ++-- src/routes/planConfig/version/get.js | 2 +- src/routes/planConfig/version/update.js | 4 ++-- src/routes/priceConfig/version/get.js | 2 +- src/routes/priceConfig/version/update.js | 4 ++-- 10 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/models/form.js b/src/models/form.js index 60d4a00c..30913d78 100644 --- a/src/models/form.js +++ b/src/models/form.js @@ -40,7 +40,7 @@ module.exports = (sequelize, DataTypes) => { Form.newVersionNumber = classMethods.newVersionNumber; Form.createNewVersion = classMethods.createNewVersion; Form.latestVersion = classMethods.latestVersion; - Form.latestRevisionofLatestVersion = classMethods.latestRevisionofLatestVersion; + Form.latestRevisionOfLatestVersion = classMethods.latestRevisionOfLatestVersion; Form.latestVersionIncludeUsed = classMethods.latestVersionIncludeUsed; return Form; diff --git a/src/models/planConfig.js b/src/models/planConfig.js index 6ec763da..99affa4d 100644 --- a/src/models/planConfig.js +++ b/src/models/planConfig.js @@ -40,7 +40,7 @@ module.exports = (sequelize, DataTypes) => { PlanConfig.newVersionNumber = classMethods.newVersionNumber; PlanConfig.createNewVersion = classMethods.createNewVersion; PlanConfig.latestVersion = classMethods.latestVersion; - PlanConfig.latestRevisionofLatestVersion = classMethods.latestRevisionofLatestVersion; + PlanConfig.latestRevisionOfLatestVersion = classMethods.latestRevisionOfLatestVersion; PlanConfig.latestVersionIncludeUsed = classMethods.latestVersionIncludeUsed; return PlanConfig; diff --git a/src/models/priceConfig.js b/src/models/priceConfig.js index e324965d..a954344d 100644 --- a/src/models/priceConfig.js +++ b/src/models/priceConfig.js @@ -39,7 +39,7 @@ module.exports = (sequelize, DataTypes) => { PriceConfig.newVersionNumber = classMethods.newVersionNumber; PriceConfig.createNewVersion = classMethods.createNewVersion; PriceConfig.latestVersion = classMethods.latestVersion; - PriceConfig.latestRevisionofLatestVersion = classMethods.latestRevisionofLatestVersion; + PriceConfig.latestRevisionOfLatestVersion = classMethods.latestRevisionOfLatestVersion; PriceConfig.latestVersionIncludeUsed = classMethods.latestVersionIncludeUsed; return PriceConfig; diff --git a/src/models/versionModelClassMethods.js b/src/models/versionModelClassMethods.js index 3a143e23..13d79d70 100644 --- a/src/models/versionModelClassMethods.js +++ b/src/models/versionModelClassMethods.js @@ -65,7 +65,7 @@ function versionModelClassMethods(model, jsonField) { return Promise.resolve(Object.values(keys)); }); }, - latestRevisionofLatestVersion(key) { + latestRevisionOfLatestVersion(key) { return model.findAll({ where: { key, diff --git a/src/routes/form/version/get.js b/src/routes/form/version/get.js index a4f87081..e68c6ac3 100644 --- a/src/routes/form/version/get.js +++ b/src/routes/form/version/get.js @@ -20,7 +20,7 @@ module.exports = [ validate(schema), permissions('form.view'), (req, res, next) => - models.Form.latestRevisionofLatestVersion(req.params.key) + models.Form.latestRevisionOfLatestVersion(req.params.key) .then((form) => { if (form == null) { const apiErr = new Error(`Form not found for key ${req.params.key} version ${req.params.version}`); diff --git a/src/routes/form/version/update.js b/src/routes/form/version/update.js index 649e2d28..024706f6 100644 --- a/src/routes/form/version/update.js +++ b/src/routes/form/version/update.js @@ -53,10 +53,10 @@ module.exports = [ return Promise.resolve(forms[0]); }) .then((form) => { - const revisison = form.revision + 1; + const revision = form.revision + 1; const entity = { version: req.params.version, - revision: revisison, + revision, createdBy: req.authUser.userId, updatedBy: req.authUser.userId, key: req.params.key, diff --git a/src/routes/planConfig/version/get.js b/src/routes/planConfig/version/get.js index cedc728b..4b336122 100644 --- a/src/routes/planConfig/version/get.js +++ b/src/routes/planConfig/version/get.js @@ -19,7 +19,7 @@ const schema = { module.exports = [ validate(schema), permissions('planConfig.view'), - (req, res, next) => models.PlanConfig.latestRevisionofLatestVersion(req.params.key) + (req, res, next) => models.PlanConfig.latestRevisionOfLatestVersion(req.params.key) .then((form) => { if (form == null) { const apiErr = new Error(`PlanConfig not found for key ${req.params.key} version ${req.params.version}`); diff --git a/src/routes/planConfig/version/update.js b/src/routes/planConfig/version/update.js index 77c67a32..bdf9c5c5 100644 --- a/src/routes/planConfig/version/update.js +++ b/src/routes/planConfig/version/update.js @@ -53,10 +53,10 @@ module.exports = [ return Promise.resolve(planConfigs[0]); }) .then((planConfig) => { - const revisison = planConfig.revision + 1; + const revision = planConfig.revision + 1; const entity = { version: req.params.version, - revision: revisison, + revision, createdBy: req.authUser.userId, updatedBy: req.authUser.userId, key: req.params.key, diff --git a/src/routes/priceConfig/version/get.js b/src/routes/priceConfig/version/get.js index b997b9b7..da624c3b 100644 --- a/src/routes/priceConfig/version/get.js +++ b/src/routes/priceConfig/version/get.js @@ -19,7 +19,7 @@ const schema = { module.exports = [ validate(schema), permissions('priceConfig.view'), - (req, res, next) => models.PriceConfig.latestRevisionofLatestVersion(req.params.key) + (req, res, next) => models.PriceConfig.latestRevisionOfLatestVersion(req.params.key) .then((form) => { if (form == null) { const apiErr = new Error(`PriceConfig not found for key ${req.params.key} version ${req.params.version}`); diff --git a/src/routes/priceConfig/version/update.js b/src/routes/priceConfig/version/update.js index 39753f07..6b7a7e44 100644 --- a/src/routes/priceConfig/version/update.js +++ b/src/routes/priceConfig/version/update.js @@ -53,10 +53,10 @@ module.exports = [ return Promise.resolve(priceConfigs[0]); }) .then((priceConfig) => { - const revisison = priceConfig.revision + 1; + const revision = priceConfig.revision + 1; const entity = { version: req.params.version, - revision: revisison, + revision, createdBy: req.authUser.userId, updatedBy: req.authUser.userId, key: req.params.key, From b61edd1100ab9e7281ac49f7ae9002b703cfe82a Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Thu, 28 Mar 2019 18:28:51 +0800 Subject: [PATCH 35/48] winning submission from challenge 30087528 - Topcoder Project Service - Improve DB/ES queries --- migrations/elasticsearch_sync.js | 33 +++++++ src/models/project.js | 39 ++++++--- src/routes/admin/project-create-index.js | 33 +++++++ src/routes/projects/list-db.js | 30 ++----- src/routes/projects/list.js | 106 ++++++++++++++++------- 5 files changed, 177 insertions(+), 64 deletions(-) diff --git a/migrations/elasticsearch_sync.js b/migrations/elasticsearch_sync.js index 349be0b6..b4734c3c 100644 --- a/migrations/elasticsearch_sync.js +++ b/migrations/elasticsearch_sync.js @@ -260,6 +260,39 @@ function getRequestBody(indexName) { }, }, }, + invites: { + type: 'nested', + properties: { + createdAt: { + type: 'date', + format: 'strict_date_optional_time||epoch_millis', + }, + createdBy: { + type: 'integer', + }, + email: { + type: 'string', + index: 'not_analyzed', + }, + id: { + type: 'long', + }, + role: { + type: 'string', + index: 'not_analyzed', + }, + updatedAt: { + type: 'date', + format: 'strict_date_optional_time||epoch_millis', + }, + updatedBy: { + type: 'integer', + }, + userId: { + type: 'long', + }, + }, + }, name: { type: 'string', }, diff --git a/src/models/project.js b/src/models/project.js index 946289f2..786d0224 100644 --- a/src/models/project.js +++ b/src/models/project.js @@ -98,7 +98,7 @@ module.exports = function defineProject(sequelize, DataTypes) { if (parameters.filters.id.$in.length === 0) { parameters.filters.id.$in.push(-1); } - query += `AND id IN (${parameters.filters.id.$in}) `; + query += `AND projects.id IN (${parameters.filters.id.$in}) `; } else if (_.isString(parameters.filters.id) || _.isNumber(parameters.filters.id)) { query += `AND id = ${parameters.filters.id} `; } @@ -107,32 +107,51 @@ module.exports = function defineProject(sequelize, DataTypes) { const statusFilter = parameters.filters.status; if (_.isObject(statusFilter)) { const statuses = statusFilter.$in.join("','"); - query += `AND status IN ('${statuses}') `; + query += `AND projects.status IN ('${statuses}') `; } else if (_.isString(statusFilter)) { - query += `AND status ='${statusFilter}'`; + query += `AND projects.status ='${statusFilter}'`; } } if (_.has(parameters.filters, 'type')) { - query += `AND type = '${parameters.filters.type}' `; + query += `AND projects.type = '${parameters.filters.type}' `; } if (_.has(parameters.filters, 'keyword')) { - query += `AND "projectFullText" ~ lower('${parameters.filters.keyword}')`; + query += `AND projects."projectFullText" ~ lower('${parameters.filters.keyword}')`; } - const attributesStr = `"${parameters.attributes.join('","')}"`; + let innerQuery = ''; + if (_.has(parameters.filters, 'userId') || _.has(parameters.filters, 'email')) { + query += ` AND (members."userId" = ${parameters.filters.userId} + OR invites."userId" = ${parameters.filters.userId} + OR invites."email" = '${parameters.filters.email}') GROUP BY projects.id`; + + innerQuery = `LEFT OUTER JOIN project_members AS members ON projects.id = members."projectId" + LEFT OUTER JOIN project_member_invites AS invites ON projects.id = invites."projectId"`; + } + + let attributesStr = _.map(parameters.attributes, attr => `projects."${attr}"`); + attributesStr = `${attributesStr.join(',')}`; const orderStr = `"${parameters.order[0][0]}" ${parameters.order[0][1]}`; // select count of projects - return sequelize.query(`SELECT COUNT(1) FROM projects WHERE ${query}`, + return sequelize.query(`SELECT COUNT(1) FROM projects AS projects + ${innerQuery} + WHERE ${query}`, { type: sequelize.QueryTypes.SELECT, logging: (str) => { log.debug(str); }, raw: true, }) .then((fcount) => { - const count = fcount[0].count; + let count = fcount.length; + if (fcount.length === 1) { + count = fcount[0].count; + } + // select project attributes - return sequelize.query(`SELECT ${attributesStr} FROM projects WHERE ${query} ORDER BY ` + - ` ${orderStr} LIMIT ${parameters.limit} OFFSET ${parameters.offset}`, + return sequelize.query(`SELECT ${attributesStr} FROM projects AS projects + ${innerQuery} + WHERE ${query} ORDER BY ` + + ` projects.${orderStr} LIMIT ${parameters.limit} OFFSET ${parameters.offset}`, { type: sequelize.QueryTypes.SELECT, logging: (str) => { log.debug(str); }, raw: true, diff --git a/src/routes/admin/project-create-index.js b/src/routes/admin/project-create-index.js index d23738ea..d4a00d4b 100644 --- a/src/routes/admin/project-create-index.js +++ b/src/routes/admin/project-create-index.js @@ -265,6 +265,39 @@ function getRequestBody(indexName, docType) { }, }, }, + invites: { + type: 'nested', + properties: { + createdAt: { + type: 'date', + format: 'strict_date_optional_time||epoch_millis', + }, + createdBy: { + type: 'integer', + }, + email: { + type: 'string', + index: 'not_analyzed', + }, + id: { + type: 'long', + }, + role: { + type: 'string', + index: 'not_analyzed', + }, + updatedAt: { + type: 'date', + format: 'strict_date_optional_time||epoch_millis', + }, + updatedBy: { + type: 'integer', + }, + userId: { + type: 'long', + }, + }, + }, name: { type: 'string', }, diff --git a/src/routes/projects/list-db.js b/src/routes/projects/list-db.js index c01b138f..187eb6b8 100644 --- a/src/routes/projects/list-db.js +++ b/src/routes/projects/list-db.js @@ -133,30 +133,10 @@ module.exports = [ } // regular users can only see projects they are members of (or invited, handled bellow) - const getProjectIds = models.ProjectMember.getProjectIdsForUser(req.authUser.userId); - - return getProjectIds - .then((accessibleProjectIds) => { - let allowedProjectIds = accessibleProjectIds; - // get projects with pending invite for current user - const invites = models.ProjectMemberInvite.getProjectInvitesForUser( - req.authUser.email, - req.authUser.userId); - if (invites) { - allowedProjectIds = _.union(allowedProjectIds, invites); - } - // filter based on accessible - if (_.get(criteria.filters, 'id', null)) { - criteria.filters.id.$in = _.intersection( - allowedProjectIds, - criteria.filters.id.$in, - ); - } else { - criteria.filters.id = { $in: allowedProjectIds }; - } - return retrieveProjects(req, criteria, sort, req.query.fields); - }) - .then(result => res.json(util.wrapResponse(req.id, result.rows, result.count))) - .catch(err => next(err)); + criteria.filters.userId = req.authUser.userId; + criteria.filters.email = req.authUser.email; + return retrieveProjects(req, criteria, sort, req.query.fields) + .then(result => res.json(util.wrapResponse(req.id, result.rows, result.count))) + .catch(err => next(err)); }, ]; diff --git a/src/routes/projects/list.js b/src/routes/projects/list.js index 73cd3f5d..52bf3b25 100755 --- a/src/routes/projects/list.js +++ b/src/routes/projects/list.js @@ -102,6 +102,56 @@ const buildEsFullTextQuery = (keyword, matchType, singleFieldName) => { }; }; +/** + * Build ES query search request body based on userId and email + * + * @param {String} userId the user id + * @param {String} email the email + * @return {Array} query + */ +const buildEsShouldQuery = (userId, email) => { + const should = []; + if (userId) { + should.push({ + nested: { + path: 'members', + query: { + query_string: { + query: userId, + fields: ['members.userId'], + }, + }, + }, + }); + should.push({ + nested: { + path: 'invites', + query: { + query_string: { + query: userId, + fields: ['invites.userId'], + }, + }, + }, + }); + } + + if (email) { + should.push({ + nested: { + path: 'invites', + query: { + query_string: { + query: email, + fields: ['invites.email'], + }, + }, + }, + }); + } + return should; +}; + /** * Build ES query search request body based on value, keyword, matchType and fieldName * @@ -234,6 +284,7 @@ const parseElasticSearchCriteria = (criteria, fields, order) => { // prepare the elasticsearch filter criteria const boolQuery = []; let mustQuery = []; + let shouldQuery = []; let fullTextQuery; if (_.has(criteria, 'filters.id.$in')) { boolQuery.push({ @@ -269,6 +320,10 @@ const parseElasticSearchCriteria = (criteria, fields, order) => { ['members.firstName', 'members.lastName'])); } + if (_.has(criteria, 'filters.userId') || _.has(criteria, 'filters.email')) { + shouldQuery = buildEsShouldQuery(criteria.filters.userId, criteria.filters.email); + } + if (_.has(criteria, 'filters.status.$in')) { // status is an array boolQuery.push({ @@ -348,6 +403,21 @@ const parseElasticSearchCriteria = (criteria, fields, order) => { must: mustQuery, }); } + + if (shouldQuery.length > 0) { + const newBody = { query: { bool: { must: [] } } }; + newBody.query.bool.must.push({ + bool: { + should: shouldQuery, + }, + }); + if (mustQuery.length > 0 || boolQuery.length > 0) { + newBody.query.bool.must.push(body.query); + } + + body.query = newBody.query; + } + if (fullTextQuery) { body.query = _.merge(body.query, fullTextQuery); if (body.query.bool) { @@ -355,7 +425,7 @@ const parseElasticSearchCriteria = (criteria, fields, order) => { } } - if (fullTextQuery || boolQuery.length > 0 || mustQuery.length > 0) { + if (fullTextQuery || boolQuery.length > 0 || mustQuery.length > 0 || shouldQuery.length > 0) { searchCriteria.body = body; } return searchCriteria; @@ -427,7 +497,6 @@ module.exports = [ offset: req.query.offset || 0, }; req.log.info(criteria); - if (!memberOnly && (util.hasAdminRole(req) || util.hasRoles(req, MANAGER_ROLES))) { @@ -437,32 +506,11 @@ module.exports = [ .catch(err => next(err)); } - // regular users can only see projects they are members of (or invited, handled bellow) - const getProjectIds = models.ProjectMember.getProjectIdsForUser(req.authUser.userId); - - return getProjectIds - .then((accessibleProjectIds) => { - const allowedProjectIds = accessibleProjectIds; - // get projects with pending invite for current user - const invites = models.ProjectMemberInvite.getProjectInvitesForUser( - req.authUser.email, - req.authUser.userId); - - return invites.then((ids => _.union(allowedProjectIds, ids))); - }) - .then((allowedProjectIds) => { - // filter based on accessible - if (_.get(criteria.filters, 'id', null)) { - criteria.filters.id.$in = _.intersection( - allowedProjectIds, - criteria.filters.id.$in, - ); - } else { - criteria.filters.id = { $in: allowedProjectIds }; - } - return retrieveProjects(req, criteria, sort, req.query.fields); - }) - .then(result => res.json(util.wrapResponse(req.id, result.rows, result.count))) - .catch(err => next(err)); + // regular users can only see projects they are members of (or invited, handled below) + criteria.filters.email = req.authUser.email; + criteria.filters.userId = req.authUser.userId; + return retrieveProjects(req, criteria, sort, req.query.fields) + .then(result => res.json(util.wrapResponse(req.id, result.rows, result.count))) + .catch(err => next(err)); }, ]; From 6e462123996905cd9baf290216e1f0e01667e92a Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Thu, 28 Mar 2019 18:57:57 +0800 Subject: [PATCH 36/48] reverted unrelated change --- src/routes/timelines/create.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/timelines/create.js b/src/routes/timelines/create.js index a19865b6..44ec10be 100644 --- a/src/routes/timelines/create.js +++ b/src/routes/timelines/create.js @@ -18,7 +18,7 @@ const schema = { body: { param: Joi.object().keys({ id: Joi.any().strip(), - name: Joi.string().max(45).required(), + name: Joi.string().max(255).required(), description: Joi.string().max(255), startDate: Joi.date().required(), endDate: Joi.date().min(Joi.ref('startDate')).allow(null), From 1774ebe2e983db58c657093dddc98f3ba7faf223 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Thu, 28 Mar 2019 20:15:20 +0800 Subject: [PATCH 37/48] renamed variable `innerQuery` => `joinQuery` --- src/models/project.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/models/project.js b/src/models/project.js index 786d0224..31a984f1 100644 --- a/src/models/project.js +++ b/src/models/project.js @@ -119,13 +119,13 @@ module.exports = function defineProject(sequelize, DataTypes) { query += `AND projects."projectFullText" ~ lower('${parameters.filters.keyword}')`; } - let innerQuery = ''; + let joinQuery = ''; if (_.has(parameters.filters, 'userId') || _.has(parameters.filters, 'email')) { query += ` AND (members."userId" = ${parameters.filters.userId} OR invites."userId" = ${parameters.filters.userId} OR invites."email" = '${parameters.filters.email}') GROUP BY projects.id`; - innerQuery = `LEFT OUTER JOIN project_members AS members ON projects.id = members."projectId" + joinQuery = `LEFT OUTER JOIN project_members AS members ON projects.id = members."projectId" LEFT OUTER JOIN project_member_invites AS invites ON projects.id = invites."projectId"`; } @@ -135,7 +135,7 @@ module.exports = function defineProject(sequelize, DataTypes) { // select count of projects return sequelize.query(`SELECT COUNT(1) FROM projects AS projects - ${innerQuery} + ${joinQuery} WHERE ${query}`, { type: sequelize.QueryTypes.SELECT, logging: (str) => { log.debug(str); }, @@ -149,7 +149,7 @@ module.exports = function defineProject(sequelize, DataTypes) { // select project attributes return sequelize.query(`SELECT ${attributesStr} FROM projects AS projects - ${innerQuery} + ${joinQuery} WHERE ${query} ORDER BY ` + ` projects.${orderStr} LIMIT ${parameters.limit} OFFSET ${parameters.offset}`, { type: sequelize.QueryTypes.SELECT, From 07ef7d600e07fb67bcc761d7fdc64476f79603fc Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Mon, 1 Apr 2019 09:16:47 +0800 Subject: [PATCH 38/48] submission from challenge 30087679 - refactored project.searchText DB query --- src/models/project.js | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/models/project.js b/src/models/project.js index 31a984f1..a59dc0c4 100644 --- a/src/models/project.js +++ b/src/models/project.js @@ -93,40 +93,49 @@ module.exports = function defineProject(sequelize, DataTypes) { searchText(parameters, log) { // special handling for keyword filter let query = '1=1 '; + const replacements = {}; if (_.has(parameters.filters, 'id')) { if (_.isObject(parameters.filters.id)) { if (parameters.filters.id.$in.length === 0) { parameters.filters.id.$in.push(-1); } - query += `AND projects.id IN (${parameters.filters.id.$in}) `; + query += 'AND projects.id IN(:id) '; + replacements.id = parameters.filters.id.$in; } else if (_.isString(parameters.filters.id) || _.isNumber(parameters.filters.id)) { - query += `AND id = ${parameters.filters.id} `; + query += 'AND id = :id '; + replacements.id = parameters.filters.id; } } if (_.has(parameters.filters, 'status')) { const statusFilter = parameters.filters.status; if (_.isObject(statusFilter)) { - const statuses = statusFilter.$in.join("','"); - query += `AND projects.status IN ('${statuses}') `; + query += 'AND projects.status IN (:status) '; + replacements.status = statusFilter.$in; } else if (_.isString(statusFilter)) { - query += `AND projects.status ='${statusFilter}'`; + query += 'AND projects.status = :status'; + replacements.status = statusFilter; } } if (_.has(parameters.filters, 'type')) { - query += `AND projects.type = '${parameters.filters.type}' `; + query += 'AND projects.type = :type '; + replacements.type = parameters.filters.type; } if (_.has(parameters.filters, 'keyword')) { - query += `AND projects."projectFullText" ~ lower('${parameters.filters.keyword}')`; + query += 'AND projects."projectFullText" ~ lower(:keyword)'; + replacements.keyword = parameters.filters.keyword; } let joinQuery = ''; if (_.has(parameters.filters, 'userId') || _.has(parameters.filters, 'email')) { - query += ` AND (members."userId" = ${parameters.filters.userId} - OR invites."userId" = ${parameters.filters.userId} - OR invites."email" = '${parameters.filters.email}') GROUP BY projects.id`; + query += ` AND (members."userId" = :userId + OR invites."userId" = :userId + OR invites."email" = :email) GROUP BY projects.id`; joinQuery = `LEFT OUTER JOIN project_members AS members ON projects.id = members."projectId" LEFT OUTER JOIN project_member_invites AS invites ON projects.id = invites."projectId"`; + + replacements.userId = parameters.filters.userId; + replacements.email = parameters.filters.email; } let attributesStr = _.map(parameters.attributes, attr => `projects."${attr}"`); @@ -138,6 +147,7 @@ module.exports = function defineProject(sequelize, DataTypes) { ${joinQuery} WHERE ${query}`, { type: sequelize.QueryTypes.SELECT, + replacements, logging: (str) => { log.debug(str); }, raw: true, }) @@ -147,12 +157,15 @@ module.exports = function defineProject(sequelize, DataTypes) { count = fcount[0].count; } + replacements.limit = parameters.limit; + replacements.offset = parameters.offset; // select project attributes return sequelize.query(`SELECT ${attributesStr} FROM projects AS projects ${joinQuery} WHERE ${query} ORDER BY ` + - ` projects.${orderStr} LIMIT ${parameters.limit} OFFSET ${parameters.offset}`, + ` projects.${orderStr} LIMIT :limit OFFSET :offset`, { type: sequelize.QueryTypes.SELECT, + replacements, logging: (str) => { log.debug(str); }, raw: true, }) From e8df7e08c54dc5500fa16442cd7f37e353437079 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Fri, 5 Apr 2019 08:57:35 +0800 Subject: [PATCH 39/48] winning submission from challenge 30087746 - Topcoder Project Service - Rename Form.scope and PlanConfig.phases --- ...16_extract_scope_from_project_templates.sql | 4 ++-- postman.json | 18 +++++++++--------- src/models/form.js | 4 ++-- src/models/planConfig.js | 4 ++-- src/routes/form/revision/create.js | 4 ++-- src/routes/form/revision/create.spec.js | 12 ++++++------ src/routes/form/revision/delete.spec.js | 4 ++-- src/routes/form/revision/get.spec.js | 6 +++--- src/routes/form/revision/list.spec.js | 6 +++--- src/routes/form/version/create.js | 4 ++-- src/routes/form/version/create.spec.js | 12 ++++++------ src/routes/form/version/delete.spec.js | 4 ++-- src/routes/form/version/get.spec.js | 10 +++++----- src/routes/form/version/getVersion.spec.js | 8 ++++---- src/routes/form/version/list.spec.js | 6 +++--- src/routes/form/version/update.js | 4 ++-- src/routes/form/version/update.spec.js | 12 ++++++------ src/routes/metadata/list.spec.js | 8 ++++---- src/routes/planConfig/revision/create.js | 4 ++-- src/routes/planConfig/revision/create.spec.js | 12 ++++++------ src/routes/planConfig/revision/delete.spec.js | 4 ++-- src/routes/planConfig/revision/get.spec.js | 6 +++--- src/routes/planConfig/revision/list.spec.js | 6 +++--- src/routes/planConfig/version/create.js | 4 ++-- src/routes/planConfig/version/create.spec.js | 12 ++++++------ src/routes/planConfig/version/delete.spec.js | 4 ++-- src/routes/planConfig/version/get.spec.js | 10 +++++----- .../planConfig/version/getVersion.spec.js | 8 ++++---- src/routes/planConfig/version/list.spec.js | 6 +++--- src/routes/planConfig/version/update.js | 4 ++-- src/routes/planConfig/version/update.spec.js | 12 ++++++------ src/routes/projectTemplates/upgrade.spec.js | 4 ++-- swagger.yaml | 10 +++++----- 33 files changed, 118 insertions(+), 118 deletions(-) diff --git a/migrations/20190316_extract_scope_from_project_templates.sql b/migrations/20190316_extract_scope_from_project_templates.sql index 6f5e34e9..63feb984 100644 --- a/migrations/20190316_extract_scope_from_project_templates.sql +++ b/migrations/20190316_extract_scope_from_project_templates.sql @@ -7,7 +7,7 @@ CREATE TABLE form ( "key" character varying(45) NOT NULL, "version" bigint DEFAULT 1 NOT NULL, "revision" bigint DEFAULT 1 NOT NULL, - "scope" json DEFAULT '{}'::json NOT NULL, + "config" json DEFAULT '{}'::json NOT NULL, "deletedAt" timestamp with time zone, "createdAt" timestamp with time zone, "updatedAt" timestamp with time zone, @@ -67,7 +67,7 @@ CREATE TABLE plan_config ( "key" character varying(45) NOT NULL, "version" bigint DEFAULT 1 NOT NULL, "revision" bigint DEFAULT 1 NOT NULL, - "phases" json DEFAULT '{}'::json NOT NULL, + "config" json DEFAULT '{}'::json NOT NULL, "deletedAt" timestamp with time zone, "createdAt" timestamp with time zone, "updatedAt" timestamp with time zone, diff --git a/postman.json b/postman.json index 536c3124..a157e443 100644 --- a/postman.json +++ b/postman.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "b2fedaf2-e077-4351-ac5b-72be7f46e3ed", + "_postman_id": "db83f8a1-5b3f-4276-a371-aa3c3497542d", "name": "tc-project-service", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -5306,7 +5306,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \t\"scope\": {\r\n \t\t\"hello\": \"test\"\r\n \t}\r\n }\r\n}" + "raw": "{\r\n \"param\":{\r\n \t\"config\": {\r\n \t\t\"hello\": \"test\"\r\n \t}\r\n }\r\n}" }, "url": { "raw": "{{api-url}}/v4/projects/metadata/form/dev/versions", @@ -5349,7 +5349,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \t\"scope\": {\r\n \t\t\"hello\": \"test111\"\r\n \t}\r\n }\r\n}" + "raw": "{\r\n \"param\":{\r\n \t\"config\": {\r\n \t\t\"hello\": \"test111\"\r\n \t}\r\n }\r\n}" }, "url": { "raw": "{{api-url}}/v4/projects/metadata/form/dev/versions/1", @@ -5519,7 +5519,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \t\"scope\": {\r\n \t\t\"hello\": \"test\"\r\n \t}\r\n }\r\n}" + "raw": "{\r\n \"param\":{\r\n \t\"config\": {\r\n \t\t\"hello\": \"test\"\r\n \t}\r\n }\r\n}" }, "url": { "raw": "{{api-url}}/v4/projects/metadata/form/dev/versions/1/revisions", @@ -5564,7 +5564,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \t\"scope\": {\r\n \t\t\"hello\": \"test\"\r\n \t}\r\n }\r\n}" + "raw": "{\r\n \"param\":{\r\n \t\"config\": {\r\n \t\t\"hello\": \"test\"\r\n \t}\r\n }\r\n}" }, "url": { "raw": "{{api-url}}/v4/projects/metadata/form/no-exist-2222key36/versions/1/revisions", @@ -6252,7 +6252,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \t\"phases\": {\r\n \t\t\"hello\": \"test\"\r\n \t}\r\n }\r\n}" + "raw": "{\r\n \"param\":{\r\n \t\"config\": {\r\n \t\t\"hello\": \"test\"\r\n \t}\r\n }\r\n}" }, "url": { "raw": "{{api-url}}/v4/projects/metadata/planConfig/dev/versions", @@ -6295,7 +6295,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \t\"phases\": {\r\n \t\t\"hello\": \"test111\"\r\n \t}\r\n }\r\n}" + "raw": "{\r\n \"param\":{\r\n \t\"config\": {\r\n \t\t\"hello\": \"test111\"\r\n \t}\r\n }\r\n}" }, "url": { "raw": "{{api-url}}/v4/projects/metadata/planConfig/dev/versions/1", @@ -6465,7 +6465,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \t\"phases\": {\r\n \t\t\"hello\": \"test\"\r\n \t}\r\n }\r\n}" + "raw": "{\r\n \"param\":{\r\n \t\"config\": {\r\n \t\t\"hello\": \"test\"\r\n \t}\r\n }\r\n}" }, "url": { "raw": "{{api-url}}/v4/projects/metadata/planConfig/dev/versions/1/revisions", @@ -6510,7 +6510,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \t\"phases\": {\r\n \t\t\"hello\": \"test\"\r\n \t}\r\n }\r\n}" + "raw": "{\r\n \"param\":{\r\n \t\"config\": {\r\n \t\t\"hello\": \"test\"\r\n \t}\r\n }\r\n}" }, "url": { "raw": "{{api-url}}/v4/projects/metadata/planConfig/no-exist-key/versions/1/revisions", diff --git a/src/models/form.js b/src/models/form.js index 30913d78..1c41b53c 100644 --- a/src/models/form.js +++ b/src/models/form.js @@ -12,7 +12,7 @@ module.exports = (sequelize, DataTypes) => { key: { type: DataTypes.STRING(45), allowNull: false }, version: { type: DataTypes.BIGINT, allowNull: false, defaultValue: 1 }, revision: { type: DataTypes.BIGINT, allowNull: false, defaultValue: 1 }, - scope: { type: DataTypes.JSON, allowNull: false }, + config: { type: DataTypes.JSON, allowNull: false }, deletedAt: { type: DataTypes.DATE, allowNull: true }, createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, @@ -35,7 +35,7 @@ module.exports = (sequelize, DataTypes) => { ], }); - const classMethods = versionModelClassMethods(Form, 'scope'); + const classMethods = versionModelClassMethods(Form, 'config'); Form.deleteOldestRevision = classMethods.deleteOldestRevision; Form.newVersionNumber = classMethods.newVersionNumber; Form.createNewVersion = classMethods.createNewVersion; diff --git a/src/models/planConfig.js b/src/models/planConfig.js index 99affa4d..8231a686 100644 --- a/src/models/planConfig.js +++ b/src/models/planConfig.js @@ -12,7 +12,7 @@ module.exports = (sequelize, DataTypes) => { key: { type: DataTypes.STRING(45), allowNull: false }, version: { type: DataTypes.BIGINT, allowNull: false, defaultValue: 1 }, revision: { type: DataTypes.BIGINT, allowNull: false, defaultValue: 1 }, - phases: { type: DataTypes.JSON, allowNull: false }, + config: { type: DataTypes.JSON, allowNull: false }, deletedAt: { type: DataTypes.DATE, allowNull: true }, createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, @@ -35,7 +35,7 @@ module.exports = (sequelize, DataTypes) => { ], }); - const classMethods = versionModelClassMethods(PlanConfig, 'phases'); + const classMethods = versionModelClassMethods(PlanConfig, 'config'); PlanConfig.deleteOldestRevision = classMethods.deleteOldestRevision; PlanConfig.newVersionNumber = classMethods.newVersionNumber; PlanConfig.createNewVersion = classMethods.createNewVersion; diff --git a/src/routes/form/revision/create.js b/src/routes/form/revision/create.js index 6a6a2aa0..c1f570a7 100644 --- a/src/routes/form/revision/create.js +++ b/src/routes/form/revision/create.js @@ -17,7 +17,7 @@ const schema = { }, body: { param: Joi.object().keys({ - scope: Joi.object().required(), + config: Joi.object().required(), createdAt: Joi.any().strip(), updatedAt: Joi.any().strip(), @@ -49,7 +49,7 @@ module.exports = [ createdBy: req.authUser.userId, updatedBy: req.authUser.userId, key: req.params.key, - scope: req.body.param.scope, + config: req.body.param.config, }); return models.Form.create(entity); } diff --git a/src/routes/form/revision/create.spec.js b/src/routes/form/revision/create.spec.js index 8bba544b..d23deaf7 100644 --- a/src/routes/form/revision/create.spec.js +++ b/src/routes/form/revision/create.spec.js @@ -15,7 +15,7 @@ describe('CREATE Form Revision', () => { const forms = [ { key: 'dev', - scope: { + config: { test: 'test1', }, version: 1, @@ -25,7 +25,7 @@ describe('CREATE Form Revision', () => { }, { key: 'dev', - scope: { + config: { test: 'test2', }, version: 1, @@ -45,7 +45,7 @@ describe('CREATE Form Revision', () => { describe('Post /projects/metadata/form/{key}/versions/{version}/revision', () => { const body = { param: { - scope: { + config: { 'test create': 'test create', }, }, @@ -80,10 +80,10 @@ describe('CREATE Form Revision', () => { .expect(404, done); }); - it('should return 422 if missing scope', (done) => { + it('should return 422 if missing config', (done) => { const invalidBody = { param: _.assign({}, body.param, { - scope: undefined, + config: undefined, }), }; @@ -109,7 +109,7 @@ describe('CREATE Form Revision', () => { .end((err, res) => { const resJson = res.body.result.content; should.exist(resJson.id); - resJson.scope.should.be.eql(body.param.scope); + resJson.config.should.be.eql(body.param.config); resJson.key.should.be.eql('dev'); resJson.revision.should.be.eql(3); resJson.version.should.be.eql(1); diff --git a/src/routes/form/revision/delete.spec.js b/src/routes/form/revision/delete.spec.js index 439e4fa0..6bcd95c6 100644 --- a/src/routes/form/revision/delete.spec.js +++ b/src/routes/form/revision/delete.spec.js @@ -40,7 +40,7 @@ describe('DELETE form revision', () => { const forms = [ { key: 'dev', - scope: { + config: { test: 'test1', }, version: 1, @@ -50,7 +50,7 @@ describe('DELETE form revision', () => { }, { key: 'dev', - scope: { + config: { test: 'test2', }, version: 1, diff --git a/src/routes/form/revision/get.spec.js b/src/routes/form/revision/get.spec.js index b95a4c97..9e2b218f 100644 --- a/src/routes/form/revision/get.spec.js +++ b/src/routes/form/revision/get.spec.js @@ -14,7 +14,7 @@ describe('GET a particular revision of specific version Form', () => { const forms = [ { key: 'dev', - scope: { + config: { test: 'test1', }, version: 1, @@ -24,7 +24,7 @@ describe('GET a particular revision of specific version Form', () => { }, { key: 'dev', - scope: { + config: { test: 'test2', }, version: 1, @@ -54,7 +54,7 @@ describe('GET a particular revision of specific version Form', () => { const resJson = res.body.result.content; resJson.key.should.be.eql(form.key); - resJson.scope.should.be.eql(form.scope); + resJson.config.should.be.eql(form.config); resJson.version.should.be.eql(form.version); resJson.revision.should.be.eql(form.revision); should.exist(resJson.createdAt); diff --git a/src/routes/form/revision/list.spec.js b/src/routes/form/revision/list.spec.js index 715fc38a..b2be931d 100644 --- a/src/routes/form/revision/list.spec.js +++ b/src/routes/form/revision/list.spec.js @@ -15,7 +15,7 @@ describe('LIST form revisions', () => { const forms = [ { key: 'dev', - scope: { + config: { 'test': 'test1', }, version: 1, @@ -25,7 +25,7 @@ describe('LIST form revisions', () => { }, { key: 'dev', - scope: { + config: { test: 'test2', }, version: 1, @@ -56,7 +56,7 @@ describe('LIST form revisions', () => { resJson.should.have.length(2); resJson[0].key.should.be.eql(form.key); - resJson[0].scope.should.be.eql(form.scope); + resJson[0].config.should.be.eql(form.config); resJson[0].version.should.be.eql(form.version); resJson[0].revision.should.be.eql(form.revision); should.exist(resJson[0].createdAt); diff --git a/src/routes/form/version/create.js b/src/routes/form/version/create.js index c53f1f7a..2a16d113 100644 --- a/src/routes/form/version/create.js +++ b/src/routes/form/version/create.js @@ -16,7 +16,7 @@ const schema = { }, body: { param: Joi.object().keys({ - scope: Joi.object().required(), + config: Joi.object().required(), createdAt: Joi.any().strip(), updatedAt: Joi.any().strip(), @@ -51,7 +51,7 @@ module.exports = [ createdBy: req.authUser.userId, updatedBy: req.authUser.userId, key: req.params.key, - scope: req.body.param.scope, + config: req.body.param.config, }); return models.Form.create(entity); }).then((createdEntity) => { diff --git a/src/routes/form/version/create.spec.js b/src/routes/form/version/create.spec.js index d8680972..68a27ba5 100644 --- a/src/routes/form/version/create.spec.js +++ b/src/routes/form/version/create.spec.js @@ -15,7 +15,7 @@ describe('CREATE Form version', () => { const forms = [ { key: 'dev', - scope: { + config: { test: 'test1', }, version: 1, @@ -25,7 +25,7 @@ describe('CREATE Form version', () => { }, { key: 'dev', - scope: { + config: { test: 'test2', }, version: 1, @@ -45,7 +45,7 @@ describe('CREATE Form version', () => { describe('Post /projects/metadata/form/{key}/versions/', () => { const body = { param: { - scope: { + config: { 'test create': 'test create', }, }, @@ -58,10 +58,10 @@ describe('CREATE Form version', () => { .expect(403, done); }); - it('should return 422 if missing scope', (done) => { + it('should return 422 if missing config', (done) => { const invalidBody = { param: _.assign({}, body.param, { - scope: undefined, + config: undefined, }), }; @@ -87,7 +87,7 @@ describe('CREATE Form version', () => { .end((err, res) => { const resJson = res.body.result.content; should.exist(resJson.id); - resJson.scope.should.be.eql(body.param.scope); + resJson.config.should.be.eql(body.param.config); resJson.key.should.be.eql('dev'); resJson.revision.should.be.eql(1); resJson.version.should.be.eql(2); diff --git a/src/routes/form/version/delete.spec.js b/src/routes/form/version/delete.spec.js index 92bfaaf8..0f3fd95e 100644 --- a/src/routes/form/version/delete.spec.js +++ b/src/routes/form/version/delete.spec.js @@ -36,7 +36,7 @@ describe('DELETE form version', () => { const forms = [ { key: 'dev', - scope: { + config: { test: 'test1', }, version: 1, @@ -46,7 +46,7 @@ describe('DELETE form version', () => { }, { key: 'dev', - scope: { + config: { test: 'test2', }, version: 1, diff --git a/src/routes/form/version/get.spec.js b/src/routes/form/version/get.spec.js index 0be71262..64abd727 100644 --- a/src/routes/form/version/get.spec.js +++ b/src/routes/form/version/get.spec.js @@ -14,7 +14,7 @@ describe('GET a latest version of specific key of Form', () => { const forms = [ { key: 'dev', - scope: { + config: { test: 'test1', }, version: 1, @@ -24,7 +24,7 @@ describe('GET a latest version of specific key of Form', () => { }, { key: 'dev', - scope: { + config: { test: 'test2', }, version: 2, @@ -34,7 +34,7 @@ describe('GET a latest version of specific key of Form', () => { }, { key: 'dev', - scope: { + config: { test: 'test2', }, version: 2, @@ -44,7 +44,7 @@ describe('GET a latest version of specific key of Form', () => { }, { key: 'dev', - scope: { + config: { test: 'test3', }, version: 1, @@ -74,7 +74,7 @@ describe('GET a latest version of specific key of Form', () => { const form = forms[2]; const resJson = res.body.result.content; resJson.key.should.be.eql(form.key); - resJson.scope.should.be.eql(form.scope); + resJson.config.should.be.eql(form.config); resJson.version.should.be.eql(form.version); resJson.revision.should.be.eql(form.revision); should.exist(resJson.createdAt); diff --git a/src/routes/form/version/getVersion.spec.js b/src/routes/form/version/getVersion.spec.js index 2415e7d4..60b3f2ec 100644 --- a/src/routes/form/version/getVersion.spec.js +++ b/src/routes/form/version/getVersion.spec.js @@ -14,7 +14,7 @@ describe('GET a particular version of specific key of Form', () => { const forms = [ { key: 'dev', - scope: { + config: { test: 'test1', }, version: 1, @@ -24,7 +24,7 @@ describe('GET a particular version of specific key of Form', () => { }, { key: 'dev', - scope: { + config: { test: 'test2', }, version: 2, @@ -34,7 +34,7 @@ describe('GET a particular version of specific key of Form', () => { }, { key: 'dev', - scope: { + config: { test: 'test3', }, version: 2, @@ -65,7 +65,7 @@ describe('GET a particular version of specific key of Form', () => { const resJson = res.body.result.content; resJson.key.should.be.eql(form.key); - resJson.scope.should.be.eql(form.scope); + resJson.config.should.be.eql(form.config); resJson.version.should.be.eql(form.version); resJson.revision.should.be.eql(form.revision); should.exist(resJson.createdAt); diff --git a/src/routes/form/version/list.spec.js b/src/routes/form/version/list.spec.js index 1d785a5e..019a0a72 100644 --- a/src/routes/form/version/list.spec.js +++ b/src/routes/form/version/list.spec.js @@ -15,7 +15,7 @@ describe('LIST form versions', () => { const forms = [ { key: 'dev', - scope: { + config: { 'test': 'test1', }, version: 1, @@ -25,7 +25,7 @@ describe('LIST form versions', () => { }, { key: 'dev', - scope: { + config: { test: 'test2', }, version: 2, @@ -56,7 +56,7 @@ describe('LIST form versions', () => { resJson.should.have.length(2); resJson[0].key.should.be.eql(form.key); - resJson[0].scope.should.be.eql(form.scope); + resJson[0].config.should.be.eql(form.config); resJson[0].version.should.be.eql(form.version); resJson[0].revision.should.be.eql(form.revision); should.exist(resJson[0].createdAt); diff --git a/src/routes/form/version/update.js b/src/routes/form/version/update.js index 024706f6..ebb7294d 100644 --- a/src/routes/form/version/update.js +++ b/src/routes/form/version/update.js @@ -19,7 +19,7 @@ const schema = { }, body: { param: Joi.object().keys({ - scope: Joi.object().required(), + config: Joi.object().required(), createdAt: Joi.any().strip(), updatedAt: Joi.any().strip(), @@ -60,7 +60,7 @@ module.exports = [ createdBy: req.authUser.userId, updatedBy: req.authUser.userId, key: req.params.key, - scope: req.body.param.scope, + config: req.body.param.config, }; return models.Form.create(entity); }) diff --git a/src/routes/form/version/update.spec.js b/src/routes/form/version/update.spec.js index a8249aa2..4f66e975 100644 --- a/src/routes/form/version/update.spec.js +++ b/src/routes/form/version/update.spec.js @@ -15,7 +15,7 @@ describe('UPDATE Form version', () => { const forms = [ { key: 'dev', - scope: { + config: { test: 'test1', }, version: 1, @@ -25,7 +25,7 @@ describe('UPDATE Form version', () => { }, { key: 'dev', - scope: { + config: { test: 'test2', }, version: 1, @@ -45,7 +45,7 @@ describe('UPDATE Form version', () => { describe('Post /projects/metadata/form/{key}/versions/{version}', () => { const body = { param: { - scope: { + config: { 'test create': 'test create', }, }, @@ -58,10 +58,10 @@ describe('UPDATE Form version', () => { .expect(403, done); }); - it('should return 422 if missing scope', (done) => { + it('should return 422 if missing config', (done) => { const invalidBody = { param: _.assign({}, body.param, { - scope: undefined, + config: undefined, }), }; request(server) @@ -86,7 +86,7 @@ describe('UPDATE Form version', () => { .end((err, res) => { const resJson = res.body.result.content; should.exist(resJson.id); - resJson.scope.should.be.eql(body.param.scope); + resJson.config.should.be.eql(body.param.config); resJson.key.should.be.eql('dev'); resJson.revision.should.be.eql(3); resJson.version.should.be.eql(1); diff --git a/src/routes/metadata/list.spec.js b/src/routes/metadata/list.spec.js index 7880114b..6e037309 100644 --- a/src/routes/metadata/list.spec.js +++ b/src/routes/metadata/list.spec.js @@ -89,7 +89,7 @@ const productCategories = [ const forms = [ { key: 'key1', - scope: { + config: { hello: 'world', }, version: 1, @@ -99,7 +99,7 @@ const forms = [ }, { key: 'key1', - scope: { + config: { hello: 'world', }, version: 2, @@ -133,7 +133,7 @@ const priceConfigs = [ const planConfigs = [ { key: 'key1', - phases: { + config: { hello: 'world', }, version: 1, @@ -143,7 +143,7 @@ const planConfigs = [ }, { key: 'key1', - phases: { + config: { hello: 'world', }, version: 2, diff --git a/src/routes/planConfig/revision/create.js b/src/routes/planConfig/revision/create.js index 2ad88b48..00bbd164 100644 --- a/src/routes/planConfig/revision/create.js +++ b/src/routes/planConfig/revision/create.js @@ -17,7 +17,7 @@ const schema = { }, body: { param: Joi.object().keys({ - phases: Joi.object().required(), + config: Joi.object().required(), createdAt: Joi.any().strip(), updatedAt: Joi.any().strip(), @@ -49,7 +49,7 @@ module.exports = [ createdBy: req.authUser.userId, updatedBy: req.authUser.userId, key: req.params.key, - phases: req.body.param.phases, + config: req.body.param.config, }); return models.PlanConfig.create(entity); } diff --git a/src/routes/planConfig/revision/create.spec.js b/src/routes/planConfig/revision/create.spec.js index 227b5eee..66a8121c 100644 --- a/src/routes/planConfig/revision/create.spec.js +++ b/src/routes/planConfig/revision/create.spec.js @@ -15,7 +15,7 @@ describe('CREATE PlanConfig Revision', () => { const planConfigs = [ { key: 'dev', - phases: { + config: { test: 'test1', }, version: 1, @@ -25,7 +25,7 @@ describe('CREATE PlanConfig Revision', () => { }, { key: 'dev', - phases: { + config: { test: 'test2', }, version: 1, @@ -45,7 +45,7 @@ describe('CREATE PlanConfig Revision', () => { describe('Post /projects/metadata/planConfig/{key}/versions/{version}/revision', () => { const body = { param: { - phases: { + config: { 'test create': 'test create', }, }, @@ -80,10 +80,10 @@ describe('CREATE PlanConfig Revision', () => { .expect(404, done); }); - it('should return 422 if missing phases', (done) => { + it('should return 422 if missing config', (done) => { const invalidBody = { param: _.assign({}, body.param, { - phases: undefined, + config: undefined, }), }; @@ -109,7 +109,7 @@ describe('CREATE PlanConfig Revision', () => { .end((err, res) => { const resJson = res.body.result.content; should.exist(resJson.id); - resJson.phases.should.be.eql(body.param.phases); + resJson.config.should.be.eql(body.param.config); resJson.key.should.be.eql('dev'); resJson.revision.should.be.eql(3); resJson.version.should.be.eql(1); diff --git a/src/routes/planConfig/revision/delete.spec.js b/src/routes/planConfig/revision/delete.spec.js index 3a668eba..80802235 100644 --- a/src/routes/planConfig/revision/delete.spec.js +++ b/src/routes/planConfig/revision/delete.spec.js @@ -40,7 +40,7 @@ describe('DELETE planConfig revision', () => { const planConfigs = [ { key: 'dev', - phases: { + config: { test: 'test1', }, version: 1, @@ -50,7 +50,7 @@ describe('DELETE planConfig revision', () => { }, { key: 'dev', - phases: { + config: { test: 'test2', }, version: 1, diff --git a/src/routes/planConfig/revision/get.spec.js b/src/routes/planConfig/revision/get.spec.js index e38faf1e..874aefd8 100644 --- a/src/routes/planConfig/revision/get.spec.js +++ b/src/routes/planConfig/revision/get.spec.js @@ -14,7 +14,7 @@ describe('GET a particular revision of specific version PlanConfig', () => { const planConfigs = [ { key: 'dev', - phases: { + config: { test: 'test1', }, version: 1, @@ -24,7 +24,7 @@ describe('GET a particular revision of specific version PlanConfig', () => { }, { key: 'dev', - phases: { + config: { test: 'test2', }, version: 1, @@ -54,7 +54,7 @@ describe('GET a particular revision of specific version PlanConfig', () => { const resJson = res.body.result.content; resJson.key.should.be.eql(planConfig.key); - resJson.phases.should.be.eql(planConfig.phases); + resJson.config.should.be.eql(planConfig.config); resJson.version.should.be.eql(planConfig.version); resJson.revision.should.be.eql(planConfig.revision); should.exist(resJson.createdAt); diff --git a/src/routes/planConfig/revision/list.spec.js b/src/routes/planConfig/revision/list.spec.js index a69d49fb..0781018c 100644 --- a/src/routes/planConfig/revision/list.spec.js +++ b/src/routes/planConfig/revision/list.spec.js @@ -15,7 +15,7 @@ describe('LIST planConfig revisions', () => { const planConfigs = [ { key: 'dev', - phases: { + config: { 'test': 'test1', }, version: 1, @@ -25,7 +25,7 @@ describe('LIST planConfig revisions', () => { }, { key: 'dev', - phases: { + config: { test: 'test2', }, version: 1, @@ -56,7 +56,7 @@ describe('LIST planConfig revisions', () => { resJson.should.have.length(2); resJson[0].key.should.be.eql(planConfig.key); - resJson[0].phases.should.be.eql(planConfig.phases); + resJson[0].config.should.be.eql(planConfig.config); resJson[0].version.should.be.eql(planConfig.version); resJson[0].revision.should.be.eql(planConfig.revision); should.exist(resJson[0].createdAt); diff --git a/src/routes/planConfig/version/create.js b/src/routes/planConfig/version/create.js index ac5c9be9..50d4c09c 100644 --- a/src/routes/planConfig/version/create.js +++ b/src/routes/planConfig/version/create.js @@ -16,7 +16,7 @@ const schema = { }, body: { param: Joi.object().keys({ - phases: Joi.object().required(), + config: Joi.object().required(), createdAt: Joi.any().strip(), updatedAt: Joi.any().strip(), @@ -51,7 +51,7 @@ module.exports = [ createdBy: req.authUser.userId, updatedBy: req.authUser.userId, key: req.params.key, - phases: req.body.param.phases, + config: req.body.param.config, }); return models.PlanConfig.create(entity); }).then((createdEntity) => { diff --git a/src/routes/planConfig/version/create.spec.js b/src/routes/planConfig/version/create.spec.js index 298a6cd9..80de2b60 100644 --- a/src/routes/planConfig/version/create.spec.js +++ b/src/routes/planConfig/version/create.spec.js @@ -15,7 +15,7 @@ describe('CREATE PlanConfig version', () => { const planConfigs = [ { key: 'dev', - phases: { + config: { test: 'test1', }, version: 1, @@ -25,7 +25,7 @@ describe('CREATE PlanConfig version', () => { }, { key: 'dev', - phases: { + config: { test: 'test2', }, version: 1, @@ -45,7 +45,7 @@ describe('CREATE PlanConfig version', () => { describe('Post /projects/metadata/planConfig/{key}/versions/', () => { const body = { param: { - phases: { + config: { 'test create': 'test create', }, }, @@ -58,10 +58,10 @@ describe('CREATE PlanConfig version', () => { .expect(403, done); }); - it('should return 422 if missing phases', (done) => { + it('should return 422 if missing config', (done) => { const invalidBody = { param: _.assign({}, body.param, { - phases: undefined, + config: undefined, }), }; @@ -87,7 +87,7 @@ describe('CREATE PlanConfig version', () => { .end((err, res) => { const resJson = res.body.result.content; should.exist(resJson.id); - resJson.phases.should.be.eql(body.param.phases); + resJson.config.should.be.eql(body.param.config); resJson.key.should.be.eql('dev'); resJson.revision.should.be.eql(1); resJson.version.should.be.eql(2); diff --git a/src/routes/planConfig/version/delete.spec.js b/src/routes/planConfig/version/delete.spec.js index 33cb7581..c147fd8f 100644 --- a/src/routes/planConfig/version/delete.spec.js +++ b/src/routes/planConfig/version/delete.spec.js @@ -36,7 +36,7 @@ describe('DELETE planConfig version', () => { const planConfigs = [ { key: 'dev', - phases: { + config: { test: 'test1', }, version: 1, @@ -46,7 +46,7 @@ describe('DELETE planConfig version', () => { }, { key: 'dev', - phases: { + config: { test: 'test2', }, version: 1, diff --git a/src/routes/planConfig/version/get.spec.js b/src/routes/planConfig/version/get.spec.js index ca04907c..f77d5cb3 100644 --- a/src/routes/planConfig/version/get.spec.js +++ b/src/routes/planConfig/version/get.spec.js @@ -14,7 +14,7 @@ describe('GET a latest version of specific key of PlanConfig', () => { const planConfigs = [ { key: 'dev', - phases: { + config: { test: 'test1', }, version: 1, @@ -24,7 +24,7 @@ describe('GET a latest version of specific key of PlanConfig', () => { }, { key: 'dev', - phases: { + config: { test: 'test2', }, version: 2, @@ -34,7 +34,7 @@ describe('GET a latest version of specific key of PlanConfig', () => { }, { key: 'dev', - phases: { + config: { test: 'test2', }, version: 2, @@ -44,7 +44,7 @@ describe('GET a latest version of specific key of PlanConfig', () => { }, { key: 'dev', - phases: { + config: { test: 'test3', }, version: 1, @@ -75,7 +75,7 @@ describe('GET a latest version of specific key of PlanConfig', () => { const resJson = res.body.result.content; resJson.key.should.be.eql(planConfig.key); - resJson.phases.should.be.eql(planConfig.phases); + resJson.config.should.be.eql(planConfig.config); resJson.version.should.be.eql(planConfig.version); resJson.revision.should.be.eql(planConfig.revision); should.exist(resJson.createdAt); diff --git a/src/routes/planConfig/version/getVersion.spec.js b/src/routes/planConfig/version/getVersion.spec.js index e73e45a7..ffca3809 100644 --- a/src/routes/planConfig/version/getVersion.spec.js +++ b/src/routes/planConfig/version/getVersion.spec.js @@ -14,7 +14,7 @@ describe('GET a particular version of specific key of PlanConfig', () => { const planConfigs = [ { key: 'dev', - phases: { + config: { test: 'test1', }, version: 1, @@ -24,7 +24,7 @@ describe('GET a particular version of specific key of PlanConfig', () => { }, { key: 'dev', - phases: { + config: { test: 'test2', }, version: 2, @@ -34,7 +34,7 @@ describe('GET a particular version of specific key of PlanConfig', () => { }, { key: 'dev', - phases: { + config: { test: 'test3', }, version: 2, @@ -65,7 +65,7 @@ describe('GET a particular version of specific key of PlanConfig', () => { const resJson = res.body.result.content; resJson.key.should.be.eql(planConfig.key); - resJson.phases.should.be.eql(planConfig.phases); + resJson.config.should.be.eql(planConfig.config); resJson.version.should.be.eql(planConfig.version); resJson.revision.should.be.eql(planConfig.revision); should.exist(resJson.createdAt); diff --git a/src/routes/planConfig/version/list.spec.js b/src/routes/planConfig/version/list.spec.js index a5197ee8..851a9577 100644 --- a/src/routes/planConfig/version/list.spec.js +++ b/src/routes/planConfig/version/list.spec.js @@ -15,7 +15,7 @@ describe('LIST planConfig versions', () => { const planConfigs = [ { key: 'dev', - phases: { + config: { 'test': 'test1', }, version: 1, @@ -25,7 +25,7 @@ describe('LIST planConfig versions', () => { }, { key: 'dev', - phases: { + config: { test: 'test2', }, version: 2, @@ -56,7 +56,7 @@ describe('LIST planConfig versions', () => { resJson.should.have.length(2); resJson[0].key.should.be.eql(planConfig.key); - resJson[0].phases.should.be.eql(planConfig.phases); + resJson[0].config.should.be.eql(planConfig.config); resJson[0].version.should.be.eql(planConfig.version); resJson[0].revision.should.be.eql(planConfig.revision); should.exist(resJson[0].createdAt); diff --git a/src/routes/planConfig/version/update.js b/src/routes/planConfig/version/update.js index bdf9c5c5..44591e6c 100644 --- a/src/routes/planConfig/version/update.js +++ b/src/routes/planConfig/version/update.js @@ -19,7 +19,7 @@ const schema = { }, body: { param: Joi.object().keys({ - phases: Joi.object().required(), + config: Joi.object().required(), createdAt: Joi.any().strip(), updatedAt: Joi.any().strip(), @@ -60,7 +60,7 @@ module.exports = [ createdBy: req.authUser.userId, updatedBy: req.authUser.userId, key: req.params.key, - phases: req.body.param.phases, + config: req.body.param.config, }; return models.PlanConfig.create(entity); }) diff --git a/src/routes/planConfig/version/update.spec.js b/src/routes/planConfig/version/update.spec.js index 7a8ea328..537b8c57 100644 --- a/src/routes/planConfig/version/update.spec.js +++ b/src/routes/planConfig/version/update.spec.js @@ -15,7 +15,7 @@ describe('UPDATE PlanConfig version', () => { const planConfigs = [ { key: 'dev', - phases: { + config: { test: 'test1', }, version: 1, @@ -25,7 +25,7 @@ describe('UPDATE PlanConfig version', () => { }, { key: 'dev', - phases: { + config: { test: 'test2', }, version: 1, @@ -45,7 +45,7 @@ describe('UPDATE PlanConfig version', () => { describe('Post /projects/metadata/planConfig/{key}/versions/{version}', () => { const body = { param: { - phases: { + config: { 'test create': 'test create', }, }, @@ -58,10 +58,10 @@ describe('UPDATE PlanConfig version', () => { .expect(403, done); }); - it('should return 422 if missing phases', (done) => { + it('should return 422 if missing config', (done) => { const invalidBody = { param: _.assign({}, body.param, { - phases: undefined, + config: undefined, }), }; request(server) @@ -86,7 +86,7 @@ describe('UPDATE PlanConfig version', () => { .end((err, res) => { const resJson = res.body.result.content; should.exist(resJson.id); - resJson.phases.should.be.eql(body.param.phases); + resJson.config.should.be.eql(body.param.config); resJson.key.should.be.eql('dev'); resJson.revision.should.be.eql(3); resJson.version.should.be.eql(1); diff --git a/src/routes/projectTemplates/upgrade.spec.js b/src/routes/projectTemplates/upgrade.spec.js index 5688557d..71a34aca 100644 --- a/src/routes/projectTemplates/upgrade.spec.js +++ b/src/routes/projectTemplates/upgrade.spec.js @@ -81,7 +81,7 @@ describe('Upgrade project template', () => { key: 'dev', version: 1, revision: 1, - scope: ['key-1', 'key_1'], + config: ['key-1', 'key_1'], createdBy: 1, updatedBy: 1, }, @@ -105,7 +105,7 @@ describe('Upgrade project template', () => { key: 'dev', version: 1, revision: 1, - phases: ['key-1', 'key_1'], + config: ['key-1', 'key_1'], createdBy: 1, updatedBy: 1, }, diff --git a/swagger.yaml b/swagger.yaml index 9f877904..e8e33389 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -5076,8 +5076,8 @@ definitions: description: revision identifier type: integer format: int64 - scope: - description: scope json + config: + description: config json type: object key: description: key of form @@ -5146,7 +5146,7 @@ definitions: param: type: object properties: - scope: + config: type: object PriceConfig: type: object @@ -5251,7 +5251,7 @@ definitions: description: revision identifier type: integer format: int64 - phases: + config: description: content json type: object key: @@ -5321,6 +5321,6 @@ definitions: param: type: object properties: - phases: + config: description: config json type: object From c1478ab21f3b78ba8007324350948cc02baef9e1 Mon Sep 17 00:00:00 2001 From: Gunasekar-K Date: Fri, 5 Apr 2019 13:01:22 +0530 Subject: [PATCH 40/48] Update build.sh [skip ci] --- build.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.sh b/build.sh index 6e97ae07..2cd75801 100755 --- a/build.sh +++ b/build.sh @@ -4,12 +4,12 @@ JQ="jq --raw-output --exit-status" ENV=$1 -AWS_REGION=$(eval "echo \$${ENV}_AWS_REGION") -ACCOUNT_ID=$(eval "echo \$${ENV}_AWS_ACCOUNT_ID") -AWS_REPOSITORY=$(eval "echo \$${ENV}_AWS_REPOSITORY") +#AWS_REGION=$(eval "echo \$${ENV}_AWS_REGION") +#ACCOUNT_ID=$(eval "echo \$${ENV}_AWS_ACCOUNT_ID") +#AWS_REPOSITORY=$(eval "echo \$${ENV}_AWS_REPOSITORY") build() { - docker build -t $ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$AWS_REPOSITORY:$CIRCLE_SHA1 . + docker build -t $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$AWS_REPOSITORY:$CIRCLE_SHA1 . } -build \ No newline at end of file +build From a8a64d5302a0091cd39b9482abf3990343ad9d1f Mon Sep 17 00:00:00 2001 From: Gunasekar-K Date: Fri, 5 Apr 2019 13:06:46 +0530 Subject: [PATCH 41/48] Update deploy.sh [skip ci] --- deploy.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/deploy.sh b/deploy.sh index 187c5816..11775b08 100755 --- a/deploy.sh +++ b/deploy.sh @@ -5,8 +5,8 @@ JQ="jq --raw-output --exit-status" ENV=$1 COUNTER_LIMIT=20 -ACCOUNT_ID=$(eval "echo \$${ENV}_AWS_ACCOUNT_ID") -AWS_REGION=$(eval "echo \$${ENV}_AWS_REGION") +#ACCOUNT_ID=$(eval "echo \$${ENV}_AWS_ACCOUNT_ID") +#AWS_REGION=$(eval "echo \$${ENV}_AWS_REGION") AWS_ECS_CONTAINER_NAME="tc-project-service" AWS_REPOSITORY=$(eval "echo \$${ENV}_AWS_REPOSITORY") AWS_ECS_CLUSTER=$(eval "echo \$${ENV}_AWS_ECS_CLUSTER") @@ -283,12 +283,12 @@ make_task_def(){ INVITE_EMAIL_SUBJECT=$(eval "echo \$${ENV}_INVITE_EMAIL_SUBJECT") INVITE_EMAIL_SECTION_TITLE=$(eval "echo \$${ENV}_INVITE_EMAIL_SECTION_TITLE") - task_def=$(printf "$task_template" $1 $ACCOUNT_ID $ACCOUNT_ID $AWS_ECS_CONTAINER_NAME $ACCOUNT_ID $AWS_REGION $AWS_REPOSITORY $CIRCLE_SHA1 $2 $3 $4 $NODE_ENV $ENABLE_FILE_UPLOAD $LOG_LEVEL $CAPTURE_LOGS $LOGENTRIES_TOKEN $API_VERSION $AWS_REGION $AUTH_DOMAIN $AUTH_SECRET $VALID_ISSUERS $DB_MASTER_URL $MEMBER_SERVICE_ENDPOINT $IDENTITY_SERVICE_ENDPOINT $BUS_API_URL $MESSAGE_SERVICE_URL $SYSTEM_USER_CLIENT_ID $SYSTEM_USER_CLIENT_SECRET $PROJECTS_ES_URL $PROJECTS_ES_INDEX_NAME $RABBITMQ_URL $DIRECT_PROJECT_SERVICE_ENDPOINT $FILE_SERVICE_ENDPOINT $CONNECT_PROJECTS_URL $CONNECT_URL $ACCOUNTS_APP_URL $SEGMENT_ANALYTICS_KEY "$AUTH0_URL" "$AUTH0_AUDIENCE" $AUTH0_CLIENT_ID "$AUTH0_CLIENT_SECRET" $TOKEN_CACHE_TIME "$KAFKA_CLIENT_CERT" "$KAFKA_CLIENT_CERT_KEY" $KAFKA_GROUP_ID $KAFKA_URL "$AUTH0_PROXY_SERVER_URL" "$EMAIL_INVITE_FROM_NAME" "$EMAIL_INVITE_FROM_EMAIL" "$INVITE_EMAIL_SUBJECT" "$INVITE_EMAIL_SECTION_TITLE" $PORT $PORT $AWS_ECS_CLUSTER $AWS_REGION $NODE_ENV) + task_def=$(printf "$task_template" $1 $AWS_ACCOUNT_ID $AWS_ACCOUNT_ID $AWS_ECS_CONTAINER_NAME $AWS_ACCOUNT_ID $AWS_REGION $AWS_REPOSITORY $CIRCLE_SHA1 $2 $3 $4 $NODE_ENV $ENABLE_FILE_UPLOAD $LOG_LEVEL $CAPTURE_LOGS $LOGENTRIES_TOKEN $API_VERSION $AWS_REGION $AUTH_DOMAIN $AUTH_SECRET $VALID_ISSUERS $DB_MASTER_URL $MEMBER_SERVICE_ENDPOINT $IDENTITY_SERVICE_ENDPOINT $BUS_API_URL $MESSAGE_SERVICE_URL $SYSTEM_USER_CLIENT_ID $SYSTEM_USER_CLIENT_SECRET $PROJECTS_ES_URL $PROJECTS_ES_INDEX_NAME $RABBITMQ_URL $DIRECT_PROJECT_SERVICE_ENDPOINT $FILE_SERVICE_ENDPOINT $CONNECT_PROJECTS_URL $CONNECT_URL $ACCOUNTS_APP_URL $SEGMENT_ANALYTICS_KEY "$AUTH0_URL" "$AUTH0_AUDIENCE" $AUTH0_CLIENT_ID "$AUTH0_CLIENT_SECRET" $TOKEN_CACHE_TIME "$KAFKA_CLIENT_CERT" "$KAFKA_CLIENT_CERT_KEY" $KAFKA_GROUP_ID $KAFKA_URL "$AUTH0_PROXY_SERVER_URL" "$EMAIL_INVITE_FROM_NAME" "$EMAIL_INVITE_FROM_EMAIL" "$INVITE_EMAIL_SUBJECT" "$INVITE_EMAIL_SECTION_TITLE" $PORT $PORT $AWS_ECS_CLUSTER $AWS_REGION $NODE_ENV) } push_ecr_image(){ eval $(aws ecr get-login --region $AWS_REGION --no-include-email) - docker push $ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$1:$CIRCLE_SHA1 + docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$1:$CIRCLE_SHA1 } register_definition() { @@ -319,11 +319,11 @@ check_service_status() { echo "$servicestatus" } -configure_aws_cli +#configure_aws_cli push_ecr_image $AWS_REPOSITORY deploy_cluster $AWS_ECS_SERVICE "npm" "run" "start" deploy_cluster $AWS_ECS_SERVICE_CONSUMERS "npm" "run" "startKafkaConsumers" check_service_status $AWS_ECS_SERVICE -check_service_status $AWS_ECS_SERVICE_CONSUMERS \ No newline at end of file +check_service_status $AWS_ECS_SERVICE_CONSUMERS From 234a8083d4ab0c2a9b014d0e5b3904cf37c248d4 Mon Sep 17 00:00:00 2001 From: Gunasekar-K Date: Fri, 5 Apr 2019 13:20:32 +0530 Subject: [PATCH 42/48] Update config.yml --- .circleci/config.yml | 91 +++++++++++++++++++++++++++----------------- 1 file changed, 56 insertions(+), 35 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a0c8e104..724b71ba 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,4 +1,45 @@ version: 2 +python_env: &python_env + docker: + - image: circleci/python:2.7-stretch-browsers + +install_awscli: &install_awscli + name: "Install awscli" + command: | + sudo pip install awscli --upgrade + +install_deploysuite: &install_deploysuite + name: Installation of install_deploysuite. + command: | + git clone --branch v1.3 https://github.com/topcoder-platform/tc-deploy-scripts ../buildscript + cp ./../buildscript/master_deploy.sh . + cp ./../buildscript/buildenv.sh . + cp ./../buildscript/awsconfiguration.sh . + +# Instructions of deployment +deploy_steps: &deploy_steps + - checkout + - attach_workspace: + at: ./workspace + - run: *install_awscli + - run: *install_deploysuite + - setup_remote_docker + - run: + name: "configuring aws environment" + command: | + ./awsconfiguration.sh $DEPLOY_ENV + - run: + name: "Building image for deploy" + command: | + source awsenvconf + ./build.sh $DEPLOY_ENV + - deploy: + name: "Deploying software" + command: | + source awsenvconf + ./deploy.sh $DEPLOY_ENV + + jobs: test: docker: @@ -30,54 +71,34 @@ jobs: root: . paths: - dist - deployDev: - docker: - - image: docker:17.06.1-ce-git - steps: - - checkout - - setup_remote_docker - - run: - name: Installation of build dependencies. - command: apk add --no-cache bash - - attach_workspace: - at: ./workspace - - run: - name: Installing AWS client - command: | - apk add --no-cache jq py-pip sudo - sudo pip install awscli --upgrade - - run: ./build.sh DEV - - run: ./deploy.sh DEV + + deployProd: - docker: - - image: docker:17.06.1-ce-git - steps: - - checkout - - setup_remote_docker - - run: - name: Installation of build dependencies. - command: apk add --no-cache bash - - attach_workspace: - at: ./workspace - - run: - name: Installing AWS client - command: | - apk add --no-cache jq py-pip sudo - sudo pip install awscli --upgrade - - run: ./build.sh PROD - - run: ./deploy.sh PROD + <<: *python_env + environment: + DEPLOY_ENV: "PROD" + steps: *deploy_steps + + deployDev: + <<: *python_env + environment: + DEPLOY_ENV: "DEV" + steps: *deploy_steps + workflows: version: 2 build: jobs: - test - deployDev: + context : org-global requires: - test filters: branches: only: ['dev', 'feature/attachmentPermissions'] - deployProd: + context : org-global requires: - test filters: From 1e0baa6ea3eb8d0adf95044d94c5016615b7fd1a Mon Sep 17 00:00:00 2001 From: Gunasekar-K Date: Fri, 5 Apr 2019 13:28:54 +0530 Subject: [PATCH 43/48] Update build.sh --- build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sh b/build.sh index 2cd75801..e4e1177f 100755 --- a/build.sh +++ b/build.sh @@ -6,7 +6,7 @@ JQ="jq --raw-output --exit-status" ENV=$1 #AWS_REGION=$(eval "echo \$${ENV}_AWS_REGION") #ACCOUNT_ID=$(eval "echo \$${ENV}_AWS_ACCOUNT_ID") -#AWS_REPOSITORY=$(eval "echo \$${ENV}_AWS_REPOSITORY") +AWS_REPOSITORY=$(eval "echo \$${ENV}_AWS_REPOSITORY") build() { docker build -t $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$AWS_REPOSITORY:$CIRCLE_SHA1 . From 38b63b06c80436b56a5be86ecd4d6db9d8776d7d Mon Sep 17 00:00:00 2001 From: nkumar-topcoder <33625707+nkumar-topcoder@users.noreply.github.com> Date: Wed, 10 Apr 2019 14:14:36 +0530 Subject: [PATCH 44/48] update to entrypoint [skip ci] --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index f02615be..130b5c44 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,4 +26,5 @@ RUN npm install EXPOSE 3000 -CMD ["npm", "start"] +ENTRYPOINT ["npm","run"] +#CMD ["npm", "start"] From a05e11716f731cce4281668efd6a4935641b6bb6 Mon Sep 17 00:00:00 2001 From: nkumar-topcoder <33625707+nkumar-topcoder@users.noreply.github.com> Date: Wed, 10 Apr 2019 14:20:49 +0530 Subject: [PATCH 45/48] STS update with S3 var --- .circleci/config.yml | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 724b71ba..8a152c00 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -24,21 +24,22 @@ deploy_steps: &deploy_steps - run: *install_awscli - run: *install_deploysuite - setup_remote_docker - - run: - name: "configuring aws environment" - command: | - ./awsconfiguration.sh $DEPLOY_ENV - - run: - name: "Building image for deploy" - command: | - source awsenvconf - ./build.sh $DEPLOY_ENV + - run: docker build -t tc-project-service:latest . - deploy: - name: "Deploying software" + name: "Running Masterscript - deploy tc-project-service " command: | + ./awsconfiguration.sh $DEPLOY_ENV source awsenvconf - ./deploy.sh $DEPLOY_ENV + ./buildenv.sh -e $DEPLOY_ENV -b ${VAR_ENV}-tc-project-service-deployvar + source buildenvvar + ./master_deploy.sh -d ECS -e $DEPLOY_ENV -t latest -s ${VAR_ENV}-global-appvar,${VAR_ENV}-tc-project-service-appvar -i tc-project-service -p FARGATE + echo "======= Running Masterscript - deploy tc-project-service-consumers ===========" + if [ -e ${VAR_ENV}-tc-project-service-appvar.json ]; then sudo rm -vf ${VAR_ENV}-tc-project-service-appvar.json; fi + + ./buildenv.sh -e $DEPLOY_ENV -b ${VAR_ENV}-tc-project-service-consumers-deployvar + source buildenvvar + ./master_deploy.sh -d ECS -e $DEPLOY_ENV -t latest -s ${VAR_ENV}-global-appvar,${VAR_ENV}-tc-project-service-appvar -i tc-project-service -p FARGATE jobs: test: @@ -71,18 +72,19 @@ jobs: root: . paths: - dist - - + deployProd: <<: *python_env environment: DEPLOY_ENV: "PROD" + VAR_ENV: "prod" steps: *deploy_steps deployDev: <<: *python_env environment: DEPLOY_ENV: "DEV" + VAR_ENV: "dev" steps: *deploy_steps workflows: @@ -96,7 +98,7 @@ workflows: - test filters: branches: - only: ['dev', 'feature/attachmentPermissions'] + only: ['dev', 'dev-sts'] - deployProd: context : org-global requires: From 35381b2bb4bfbacd2cb74eab33bc0c8a72bb0f8d Mon Sep 17 00:00:00 2001 From: nkumar-topcoder <33625707+nkumar-topcoder@users.noreply.github.com> Date: Thu, 11 Apr 2019 17:16:15 +0530 Subject: [PATCH 46/48] Update config.yml --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8a152c00..b429c3ba 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -11,7 +11,7 @@ install_awscli: &install_awscli install_deploysuite: &install_deploysuite name: Installation of install_deploysuite. command: | - git clone --branch v1.3 https://github.com/topcoder-platform/tc-deploy-scripts ../buildscript + git clone --branch master https://github.com/topcoder-platform/tc-deploy-scripts ../buildscript cp ./../buildscript/master_deploy.sh . cp ./../buildscript/buildenv.sh . cp ./../buildscript/awsconfiguration.sh . From bad537f943326119be9786c40856a2ac6410300d Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Fri, 19 Apr 2019 13:29:17 +0800 Subject: [PATCH 47/48] winning submission from challenge 30088714 - Topcoder Connect - Add add-ons flag --- migrations/20190418_productTemplates_isAddOn.sql | 12 ++++++++++++ src/models/productTemplate.js | 1 + src/routes/productTemplates/create.js | 1 + src/routes/productTemplates/create.spec.js | 1 + src/routes/productTemplates/list.spec.js | 4 +++- src/routes/productTemplates/update.js | 1 + src/routes/productTemplates/update.spec.js | 1 + swagger.yaml | 3 +++ 8 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 migrations/20190418_productTemplates_isAddOn.sql diff --git a/migrations/20190418_productTemplates_isAddOn.sql b/migrations/20190418_productTemplates_isAddOn.sql new file mode 100644 index 00000000..ee271239 --- /dev/null +++ b/migrations/20190418_productTemplates_isAddOn.sql @@ -0,0 +1,12 @@ +-- +-- UPDATE EXISTING TABLES: +-- product_templates: +-- added column `isAddOn` + +-- +-- product_templates + +-- Add new column +ALTER TABLE product_templates ADD COLUMN "isAddOn" boolean DEFAULT false; +-- Update new column +UPDATE product_templates SET "isAddOn"='true' WHERE "subCategory" != "category"; diff --git a/src/models/productTemplate.js b/src/models/productTemplate.js index f8fd7f17..9149ce04 100644 --- a/src/models/productTemplate.js +++ b/src/models/productTemplate.js @@ -18,6 +18,7 @@ module.exports = (sequelize, DataTypes) => { deletedAt: DataTypes.DATE, disabled: { type: DataTypes.BOOLEAN, defaultValue: false }, hidden: { type: DataTypes.BOOLEAN, defaultValue: false }, + isAddOn: { type: DataTypes.BOOLEAN, defaultValue: false }, createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, deletedBy: DataTypes.BIGINT, diff --git a/src/routes/productTemplates/create.js b/src/routes/productTemplates/create.js index 9907d085..aa9e4b98 100644 --- a/src/routes/productTemplates/create.js +++ b/src/routes/productTemplates/create.js @@ -26,6 +26,7 @@ const schema = { template: Joi.object().required(), disabled: Joi.boolean().optional(), hidden: Joi.boolean().optional(), + isAddOn: Joi.boolean().optional(), createdAt: Joi.any().strip(), updatedAt: Joi.any().strip(), deletedAt: Joi.any().strip(), diff --git a/src/routes/productTemplates/create.spec.js b/src/routes/productTemplates/create.spec.js index 0c283caf..fc367625 100644 --- a/src/routes/productTemplates/create.spec.js +++ b/src/routes/productTemplates/create.spec.js @@ -42,6 +42,7 @@ describe('CREATE product template', () => { aliases: ['product key 1', 'product_key_1'], disabled: true, hidden: true, + isAddOn: true, template: { template1: { name: 'template 1', diff --git a/src/routes/productTemplates/list.spec.js b/src/routes/productTemplates/list.spec.js index ae061c24..f975a3a9 100644 --- a/src/routes/productTemplates/list.spec.js +++ b/src/routes/productTemplates/list.spec.js @@ -15,7 +15,7 @@ const validateProductTemplates = (count, resJson, expectedTemplates) => { resJson.should.have.length(count); resJson.forEach((pt, idx) => { pt.should.have.all.keys('id', 'name', 'productKey', 'category', 'subCategory', 'icon', 'brief', 'details', - 'aliases', 'template', 'disabled', 'hidden', 'createdBy', 'createdAt', 'updatedBy', 'updatedAt'); + 'aliases', 'template', 'disabled', 'hidden', 'isAddOn', 'createdBy', 'createdAt', 'updatedBy', 'updatedAt'); pt.should.not.have.all.keys('deletedAt', 'deletedBy'); pt.name.should.be.eql(expectedTemplates[idx].name); pt.productKey.should.be.eql(expectedTemplates[idx].productKey); @@ -30,6 +30,7 @@ const validateProductTemplates = (count, resJson, expectedTemplates) => { pt.updatedBy.should.be.eql(expectedTemplates[idx].updatedBy); pt.disabled.should.be.eql(_.get(expectedTemplates[idx], 'disabled', false)); pt.hidden.should.be.eql(_.get(expectedTemplates[idx], 'hidden', false)); + pt.isAddOn.should.be.eql(_.get(expectedTemplates[idx], 'isAddOn', false)); }); }; @@ -52,6 +53,7 @@ describe('LIST product templates', () => { }, disabled: true, hidden: true, + isAddOn: true, template: { template1: { name: 'template 1', diff --git a/src/routes/productTemplates/update.js b/src/routes/productTemplates/update.js index 60d7f730..ad245b8e 100644 --- a/src/routes/productTemplates/update.js +++ b/src/routes/productTemplates/update.js @@ -29,6 +29,7 @@ const schema = { template: Joi.object(), disabled: Joi.boolean().optional(), hidden: Joi.boolean().optional(), + isAddOn: Joi.boolean().optional(), createdAt: Joi.any().strip(), updatedAt: Joi.any().strip(), deletedAt: Joi.any().strip(), diff --git a/src/routes/productTemplates/update.spec.js b/src/routes/productTemplates/update.spec.js index f0223d4b..a1508e2c 100644 --- a/src/routes/productTemplates/update.spec.js +++ b/src/routes/productTemplates/update.spec.js @@ -22,6 +22,7 @@ describe('UPDATE product template', () => { aliases: ['productTemplate-1', 'productTemplate_1'], disabled: true, hidden: true, + isAddOn: true, template: { template1: { name: 'template 1', diff --git a/swagger.yaml b/swagger.yaml index e8e33389..a7085701 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -3703,6 +3703,9 @@ definitions: template: type: object description: the product template template + isAddOn: + type: boolean + description: the flag that shows if the product template is an add on ProductTemplateBodyParam: title: Product template body param type: object From 7323369444bd3d6fb964d141604e2820d5d15fd8 Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Fri, 19 Apr 2019 15:48:54 +0800 Subject: [PATCH 48/48] Revert "fully removed system user token usage" This reverts commit 8b9c715bed63a617c858941ed09c3cbbf2466965. --- config/custom-environment-variables.json | 2 ++ config/default.json | 2 ++ src/events/projectMembers/index.js | 4 ++-- src/services/messageService.js | 4 ++++ src/util.js | 14 ++++++++++++++ 5 files changed, 24 insertions(+), 2 deletions(-) diff --git a/config/custom-environment-variables.json b/config/custom-environment-variables.json index 4260d83e..c807e407 100644 --- a/config/custom-environment-variables.json +++ b/config/custom-environment-variables.json @@ -22,6 +22,8 @@ "fileServiceEndpoint": "FILE_SERVICE_ENDPOINT", "identityServiceEndpoint": "IDENTITY_SERVICE_ENDPOINT", "memberServiceEndpoint": "MEMBER_SERVICE_ENDPOINT", + "systemUserClientId": "SYSTEM_USER_CLIENT_ID", + "systemUserClientSecret": "SYSTEM_USER_CLIENT_SECRET", "connectProjectsUrl": "CONNECT_PROJECTS_URL", "dbConfig": { "masterUrl": "DB_MASTER_URL", diff --git a/config/default.json b/config/default.json index 7dd5f6bc..95f5666d 100644 --- a/config/default.json +++ b/config/default.json @@ -25,6 +25,8 @@ "timelineIndexName": "timelines", "timelineDocType": "timelineV4" }, + "systemUserClientId": "", + "systemUserClientSecret": "", "connectProjectUrl":"", "dbConfig": { "masterUrl": "", diff --git a/src/events/projectMembers/index.js b/src/events/projectMembers/index.js index 5dfca77c..bab7e486 100644 --- a/src/events/projectMembers/index.js +++ b/src/events/projectMembers/index.js @@ -48,7 +48,7 @@ const projectMemberAddedHandler = Promise.coroutine(function* a(logger, msg, cha // add copilot/update manager permissions operation promise const directProjectId = yield models.Project.getDirectProjectId(projectId); if (directProjectId) { - const token = yield util.getM2MToken(); + const token = yield util.getSystemUserToken(logger); const req = { id: origRequestId, log: logger, @@ -119,7 +119,7 @@ const projectMemberRemovedHandler = Promise.coroutine(function* (logger, msg, ch if (_.indexOf([PROJECT_MEMBER_ROLE.COPILOT, PROJECT_MEMBER_ROLE.MANAGER], member.role) > -1) { const directProjectId = yield models.Project.getDirectProjectId(projectId); if (directProjectId) { - const token = yield util.getM2MToken(); + const token = yield util.getSystemUserToken(logger); const req = { id: origRequestId, log: logger, diff --git a/src/services/messageService.js b/src/services/messageService.js index 949c0134..4ea5f04b 100644 --- a/src/services/messageService.js +++ b/src/services/messageService.js @@ -65,8 +65,12 @@ async function getClient(logger) { function createTopic(topic, logger) { logger.debug(`createTopic for topic: ${JSON.stringify(topic)}`); return getClient(logger).then((msgClient) => { + // return util.getSystemUserToken(logger).then((adminToken) => { logger.debug('calling message service'); return msgClient.post('/topics/create', topic) + // const httpClient = util.getHttpClient({ id: `topic#create#${topic.referenceId}`, log: logger }); + // httpClient.defaults.headers.common.Authorization = `Bearer ${adminToken}`; + // return httpClient.post(`${config.get('messageApiUrl')}/topics/create`, topic) .then((resp) => { logger.debug('Topic created successfully'); logger.debug(`Topic created successfully [status]: ${resp.status}`); diff --git a/src/util.js b/src/util.js index b706f7f8..853e71c4 100644 --- a/src/util.js +++ b/src/util.js @@ -261,6 +261,20 @@ _.assignIn(util, { }); }, + getSystemUserToken: (logger, id = 'system') => { + const httpClient = util.getHttpClient({ id, log: logger }); + const url = `${config.get('identityServiceEndpoint')}authorizations`; + const formData = `clientId=${config.get('systemUserClientId')}&` + + `secret=${encodeURIComponent(config.get('systemUserClientSecret'))}`; + return httpClient.post(url, formData, + { + timeout: 4000, + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + }, + ) + .then(res => res.data.result.content.token); + }, + /** * Get machine to machine token. * @returns {Promise} promise which resolves to the m2m token