diff --git a/.circleci/config.yml b/.circleci/config.yml
index ed25f599..4864453b 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -32,6 +32,7 @@ deploy_steps: &deploy_steps
./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
diff --git a/.eslintignore b/.eslintignore
index 5eb52385..87d8ad2d 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,5 +1,7 @@
config/local.js
-config/sample.local.js
+config/mock.local.js
+config/m2m.local.js
+local/seed/
node_modules
dist
.ebextensions
diff --git a/README.md b/README.md
index 2b4a6450..e2790ac4 100644
--- a/README.md
+++ b/README.md
@@ -4,89 +4,123 @@ 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
+### 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` - two instances: for app and for unit tests
+ - `elasticsearch`
+ - `rabbitmq`
+ - `mock-services` - mocks some Topcoder API
+
+ *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`, `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`:
+
+ ```bash
+ cp config/mock.local.js config/local.js
+ # or
+ cp config/m2m.local.js config/local.js
+ ```
+
+ `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
+ ```
+ 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`, `AUTH0_PROXY_SERVER_URL`) provided to access Topcoder DEV environment services.
+
+* 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
+
+ **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
+ ```
+ 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 & projects
+```bash
+CONNECT_USER_TOKEN= npm run demo-data
```
+This command will create sample metadata entries in the DB (duplicate what is currently in development environment).
-#### 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`
+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.
-#### Elasticsearch
-Docker compose includes elasticsearch instance as well. It will open ports 9200 & 9300 (kibana)
+### Run Connect App with Project Service locally
-#### Sync indices and mappings
+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
-There is a helper script to sync the indices and mappings with the elasticsearch.
+ ```js
+ PROJECTS_API_URL: 'http://localhost:8001'
+ ```
+2. Bypass token validation in Project Service.
-Run `npm run sync:es` from the root of project to execute the script.
+ 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.
-> NOTE: This will first clear all the indices and than recreate them. So use with caution.
+ *NOTE: this change only let us bypass validation during local development process*.
-**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
-```
-
-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"
+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.
```
@@ -95,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:
@@ -104,4 +141,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/build.sh b/build.sh
index 6e97ae07..e4e1177f 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_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
diff --git a/config/default.json b/config/default.json
index 4da2970e..95f5666d 100644
--- a/config/default.json
+++ b/config/default.json
@@ -58,6 +58,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/m2m.local.js b/config/m2m.local.js
new file mode 100644
index 00000000..8fb26130
--- /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/v3/",
+ 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/config/test.json b/config/test.json
index 23ca972e..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": "",
@@ -14,7 +15,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/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
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:
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) ?
diff --git a/local/seed/index.js b/local/seed/index.js
new file mode 100644
index 00000000..a98fcc1c
--- /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().then(() => process.exit());
diff --git a/local/seed/projects.json b/local/seed/projects.json
new file mode 100644
index 00000000..9fb184af
--- /dev/null
+++ b/local/seed/projects.json
@@ -0,0 +1,264 @@
+[
+ {
+ "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"
+ }
+ },
+ {
+ "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": [40152855],
+ "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": [40152855],
+ "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/seedMetadata.js b/local/seed/seedMetadata.js
new file mode 100644
index 00000000..eca3aa20
--- /dev/null
+++ b/local/seed/seedMetadata.js
@@ -0,0 +1,98 @@
+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.')
+ process.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';
+
+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
+ })
+ .then(async function (response) {
+ let data = response.data;
+
+ console.log('Creating metadata objects locally...');
+
+ var headers = {
+ 'Content-Type': 'application/json',
+ 'Authorization': 'Bearer ' + token
+ }
+
+ let promises = _(data.result.content.projectTypes).map(pt=>{
+ 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)
+ })
+ });
+
+ await Promise.all(promises);
+
+ promises = _(data.result.content.productCategories).map(pt=>{
+ 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)
+ })
+ });
+
+ 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)
+ })
+ });
+
+ 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 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..e1d50417
--- /dev/null
+++ b/local/seed/seedProjects.js
@@ -0,0 +1,137 @@
+import util from '../../src/tests/util';
+
+const axios = require('axios');
+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.
+ */
+module.exports = (targetUrl, token) => {
+ let projectPromises;
+
+ const projectsUrl = `${targetUrl}projects`;
+ const adminHeaders = {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${token}`,
+ };
+
+ const connectAdminHeaders = {
+ '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: 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, 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 = []
+ responses.forEach(response => {
+ const userId = _.get(response, 'data.result.content.success[0].userId')
+ acceptInvitationPromises.push(updateProjectMemberInvite(projectId, {
+ param: {
+ userId,
+ status: 'accepted'
+ }
+ }, 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)
+ }
+ }
+
+ return {
+ projectId,
+ status,
+ cancelReason,
+ };
+ });
+ });
+
+ return Promise.all(projectPromises)
+ .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,
+ },
+ );
+}
+
+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/migrations/20190316_extract_scope_from_project_templates.sql b/migrations/20190316_extract_scope_from_project_templates.sql
new file mode 100644
index 00000000..63feb984
--- /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,
+ "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 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,
+ "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 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/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/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/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",
diff --git a/postman.json b/postman.json
index 8c74a585..a157e443 100644
--- a/postman.json
+++ b/postman.json
@@ -1,6 +1,6 @@
{
"info": {
- "_postman_id": "4fc2b7cf-404a-4fd7-b6d2-4828a3994859",
+ "_postman_id": "db83f8a1-5b3f-4276-a371-aa3c3497542d",
"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\"config\": {\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\"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",
+ "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\"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",
+ "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\"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",
+ "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\"config\": {\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\"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",
+ "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\"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",
+ "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\"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",
+ "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/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/models/form.js b/src/models/form.js
new file mode 100644
index 00000000..1c41b53c
--- /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 },
+ 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: 'form',
+ paranoid: true,
+ timestamps: true,
+ updatedAt: 'updatedAt',
+ createdAt: 'createdAt',
+ deletedAt: 'deletedAt',
+ indexes: [
+ {
+ unique: true,
+ fields: ['key', 'version', 'revision'],
+ },
+ ],
+ });
+
+ const classMethods = versionModelClassMethods(Form, 'config');
+ 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..8231a686
--- /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 },
+ 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: 'plan_config',
+ paranoid: true,
+ timestamps: true,
+ updatedAt: 'updatedAt',
+ createdAt: 'createdAt',
+ deletedAt: 'deletedAt',
+ indexes: [
+ {
+ unique: true,
+ fields: ['key', 'version', 'revision'],
+ },
+ ],
+ });
+
+ const classMethods = versionModelClassMethods(PlanConfig, 'config');
+ 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..a954344d
--- /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/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/models/project.js b/src/models/project.js
index 5339b5b4..a59dc0c4 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', {
@@ -61,28 +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 does not yet have a co-pilot assigned
- * @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],
- ['"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],
- ],
- },
- attributes: ['id'],
- raw: true,
- })
- .then(res => _.map(res, 'id'));
- },
/**
* Get direct project id
* @param id the id of project
@@ -115,47 +93,79 @@ 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 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 status IN ('${statuses}') `;
+ query += 'AND projects.status IN (:status) ';
+ replacements.status = statusFilter.$in;
} else if (_.isString(statusFilter)) {
- query += `AND status ='${statusFilter}'`;
+ query += 'AND projects.status = :status';
+ replacements.status = statusFilter;
}
}
if (_.has(parameters.filters, 'type')) {
- query += `AND type = '${parameters.filters.type}' `;
+ query += 'AND projects.type = :type ';
+ replacements.type = parameters.filters.type;
}
if (_.has(parameters.filters, 'keyword')) {
- query += `AND "projectFullText" ~ lower('${parameters.filters.keyword}')`;
+ query += 'AND projects."projectFullText" ~ lower(:keyword)';
+ replacements.keyword = parameters.filters.keyword;
}
- const attributesStr = `"${parameters.attributes.join('","')}"`;
+ let joinQuery = '';
+ if (_.has(parameters.filters, 'userId') || _.has(parameters.filters, 'email')) {
+ 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}"`);
+ 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
+ ${joinQuery}
+ WHERE ${query}`,
{ type: sequelize.QueryTypes.SELECT,
+ replacements,
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;
+ }
+
+ replacements.limit = parameters.limit;
+ replacements.offset = parameters.offset;
// 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
+ ${joinQuery}
+ WHERE ${query} ORDER BY ` +
+ ` projects.${orderStr} LIMIT :limit OFFSET :offset`,
{ type: sequelize.QueryTypes.SELECT,
+ replacements,
logging: (str) => { log.debug(str); },
raw: true,
})
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..13d79d70
--- /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/permissions/project.view.js b/src/permissions/project.view.js
index 61e4ebed..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,48 +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 the project doesn't have any copilots then
- // user can access the project
- 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 {
- // 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));
}
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/form/revision/create.js b/src/routes/form/revision/create.js
new file mode 100644
index 00000000..c1f570a7
--- /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({
+ 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('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,
+ config: req.body.param.config,
+ });
+ 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..d23deaf7
--- /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',
+ 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.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: {
+ config: {
+ '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 config', (done) => {
+ const invalidBody = {
+ param: _.assign({}, body.param, {
+ config: 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.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/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..6bcd95c6
--- /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',
+ 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.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..9e2b218f
--- /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',
+ 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.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.config.should.be.eql(form.config);
+ 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..b2be931d
--- /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',
+ 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.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].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);
+ 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..2a16d113
--- /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({
+ 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('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,
+ config: req.body.param.config,
+ });
+ 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..68a27ba5
--- /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',
+ 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.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: {
+ config: {
+ '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 config', (done) => {
+ const invalidBody = {
+ param: _.assign({}, body.param, {
+ config: 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.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/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..0f3fd95e
--- /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',
+ 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.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..e68c6ac3
--- /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..64abd727
--- /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',
+ 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.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.config.should.be.eql(form.config);
+ 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..60b3f2ec
--- /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',
+ 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.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.config.should.be.eql(form.config);
+ 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..019a0a72
--- /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',
+ 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.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].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);
+ 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..ebb7294d
--- /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({
+ 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('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 revision = form.revision + 1;
+ const entity = {
+ version: req.params.version,
+ revision,
+ createdBy: req.authUser.userId,
+ updatedBy: req.authUser.userId,
+ key: req.params.key,
+ config: req.body.param.config,
+ };
+ 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..4f66e975
--- /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',
+ 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.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: {
+ config: {
+ '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 config', (done) => {
+ const invalidBody = {
+ param: _.assign({}, body.param, {
+ config: 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.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/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 110c5ca3..045384bf 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+)')
@@ -42,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'));
@@ -58,13 +68,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')
@@ -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 86798d21..6e037309 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',
+ 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 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',
+ config: {
+ hello: 'world',
+ },
+ version: 1,
+ revision: 1,
+ createdBy: 1,
+ updatedBy: 1,
+ },
+ {
+ key: 'key1',
+ config: {
+ hello: 'world',
+ },
+ version: 2,
+ revision: 1,
+ createdBy: 1,
+ updatedBy: 1,
+ },
+];
describe('GET all metadata', () => {
beforeEach(() => testUtil.clearDb()
@@ -90,26 +159,18 @@ 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);
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 +190,46 @@ 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);
+ 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();
+ });
});
it('should return 200 for connect manager', (done) => {
diff --git a/src/routes/milestones/update.spec.js b/src/routes/milestones/update.spec.js
index ad591287..9a694e73 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/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..060a1a79 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) => {
@@ -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);
});
});
diff --git a/src/routes/planConfig/revision/create.js b/src/routes/planConfig/revision/create.js
new file mode 100644
index 00000000..00bbd164
--- /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({
+ 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('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,
+ config: req.body.param.config,
+ });
+ 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..66a8121c
--- /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',
+ 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.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: {
+ config: {
+ '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 config', (done) => {
+ const invalidBody = {
+ param: _.assign({}, body.param, {
+ config: 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.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/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..80802235
--- /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',
+ 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.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..874aefd8
--- /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',
+ 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.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.config.should.be.eql(planConfig.config);
+ 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..0781018c
--- /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',
+ 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.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].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);
+ 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..50d4c09c
--- /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({
+ 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('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,
+ config: req.body.param.config,
+ });
+ 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..80de2b60
--- /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',
+ 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.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: {
+ config: {
+ '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 config', (done) => {
+ const invalidBody = {
+ param: _.assign({}, body.param, {
+ config: 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.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/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..c147fd8f
--- /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',
+ 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.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..4b336122
--- /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..f77d5cb3
--- /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',
+ 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.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.config.should.be.eql(planConfig.config);
+ 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..ffca3809
--- /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',
+ 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.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.config.should.be.eql(planConfig.config);
+ 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..851a9577
--- /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',
+ 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.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].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);
+ 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..44591e6c
--- /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({
+ 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('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 revision = planConfig.revision + 1;
+ const entity = {
+ version: req.params.version,
+ revision,
+ createdBy: req.authUser.userId,
+ updatedBy: req.authUser.userId,
+ key: req.params.key,
+ config: req.body.param.config,
+ };
+ 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..537b8c57
--- /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',
+ 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.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: {
+ config: {
+ '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 config', (done) => {
+ const invalidBody = {
+ param: _.assign({}, body.param, {
+ config: 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.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/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..da624c3b
--- /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..6b7a7e44
--- /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 revision = priceConfig.revision + 1;
+ const entity = {
+ version: req.params.version,
+ revision,
+ 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/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/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/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..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',
@@ -113,16 +115,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/productTemplates/update.js b/src/routes/productTemplates/update.js
index 39456144..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(),
@@ -64,7 +65,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);
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/src/routes/projectMemberInvites/create.spec.js b/src/routes/projectMemberInvites/create.spec.js
index 74f6be9c..6965d2c4 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();
+ });
});
}));
});
@@ -500,25 +509,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 +523,75 @@ describe('Project Member Invite create', () => {
},
})
.expect('Content-Type', /json/)
- .expect(403)
+ .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('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 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({
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/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
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({
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/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/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..71a34aca
--- /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,
+ config: ['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,
+ config: ['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/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) => {
diff --git a/src/routes/projects/list-db.js b/src/routes/projects/list-db.js
index 3e10736f..187eb6b8 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,35 +131,12 @@ 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 have
- //
- 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;
- // 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));
+
+ // regular users can only see projects they are members of (or invited, handled bellow)
+ 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-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..52bf3b25 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');
@@ -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))) {
@@ -436,37 +505,12 @@ 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 have
- //
- 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) => {
- 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));
},
];
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();
}
});
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 5231249b..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: {
@@ -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;
@@ -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 });
diff --git a/swagger.yaml b/swagger.yaml
index fcd02b60..a7085701 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,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
@@ -2742,8 +3713,7 @@ definitions:
- param
properties:
param:
- $ref: "#/definitions/ProductTemplateRequest"
-
+ $ref: '#/definitions/ProductTemplateRequest'
ProductTemplate:
title: Product template object
allOf:
@@ -2781,8 +3751,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 +3770,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 +3789,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 +3811,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 +3852,6 @@ definitions:
type: number
format: integer
description: the project phase order
-
ProjectPhaseBodyParam:
title: Project phase body param
type: object
@@ -2894,8 +3859,7 @@ definitions:
- param
properties:
param:
- $ref: "#/definitions/ProjectPhaseRequest"
-
+ $ref: '#/definitions/ProjectPhaseRequest'
ProjectPhase:
title: Project phase object
allOf:
@@ -2929,9 +3893,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 +3912,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 +3934,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 +3967,6 @@ definitions:
details:
type: object
description: the phase product details
-
PhaseProductBodyParam:
title: Phase product body param
type: object
@@ -3016,8 +3974,7 @@ definitions:
- param
properties:
param:
- $ref: "#/definitions/PhaseProductRequest"
-
+ $ref: '#/definitions/PhaseProductRequest'
PhaseProduct:
title: Phase product object
allOf:
@@ -3051,9 +4008,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 +4027,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 +4049,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 +4063,6 @@ definitions:
displayName:
type: string
description: the product category display name
-
ProductCategoryBodyParam:
title: Product category body param
type: object
@@ -3120,8 +4070,7 @@ definitions:
- param
properties:
param:
- $ref: "#/definitions/ProductCategoryRequest"
-
+ $ref: '#/definitions/ProductCategoryRequest'
ProductCategoryCreateRequest:
title: Product category creation request object
type: object
@@ -3133,8 +4082,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 +4090,7 @@ definitions:
- param
properties:
param:
- $ref: "#/definitions/ProductCategoryCreateRequest"
-
+ $ref: '#/definitions/ProductCategoryCreateRequest'
ProductCategory:
title: Product category object
allOf:
@@ -3175,9 +4122,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 +4141,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 +4163,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 +4177,6 @@ definitions:
displayName:
type: string
description: the project type display name
-
ProjectTypeBodyParam:
title: Project type body param
type: object
@@ -3243,8 +4184,7 @@ definitions:
- param
properties:
param:
- $ref: "#/definitions/ProjectTypeRequest"
-
+ $ref: '#/definitions/ProjectTypeRequest'
ProjectTypeCreateRequest:
title: Project type creation request object
type: object
@@ -3256,8 +4196,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 +4204,7 @@ definitions:
- param
properties:
param:
- $ref: "#/definitions/ProjectTypeCreateRequest"
-
+ $ref: '#/definitions/ProjectTypeCreateRequest'
ProjectType:
title: Project type object
allOf:
@@ -3298,8 +4236,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 +4250,6 @@ definitions:
configName:
type: string
description: the organization config name
-
OrgConfigCreateRequest:
title: Organization config creation request object
type: object
@@ -3323,8 +4259,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 +4267,7 @@ definitions:
- param
properties:
param:
- $ref: "#/definitions/OrgConfigCreateRequest"
-
+ $ref: '#/definitions/OrgConfigCreateRequest'
OrgConfig:
title: Organization config object
allOf:
@@ -3378,8 +4312,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 +4331,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 +4353,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 +4376,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 +4398,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 +4435,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 +4445,7 @@ definitions:
- param
properties:
param:
- $ref: "#/definitions/TimelineRequest"
-
+ $ref: '#/definitions/TimelineRequest'
Timeline:
title: Timeline object
allOf:
@@ -3551,8 +4479,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 +4498,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 +4520,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 +4587,6 @@ definitions:
blockedText:
type: string
description: the milestone blocked text
-
MilestonePatchRequest:
title: Milestone request object
type: object
@@ -3716,7 +4640,6 @@ definitions:
blockedText:
type: string
description: the milestone blocked text
-
MilestonePostBodyParam:
title: Milestone body param
type: object
@@ -3724,8 +4647,7 @@ definitions:
- param
properties:
param:
- $ref: "#/definitions/MilestonePostRequest"
-
+ $ref: '#/definitions/MilestonePostRequest'
MilestonePatchBodyParam:
title: Milestone body param
type: object
@@ -3733,8 +4655,7 @@ definitions:
- param
properties:
param:
- $ref: "#/definitions/MilestonePatchRequest"
-
+ $ref: '#/definitions/MilestonePatchRequest'
Milestone:
title: Milestone object
allOf:
@@ -3768,8 +4689,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 +4708,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 +4730,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 +4767,7 @@ definitions:
reference:
type: string
enum:
- - productTemplate
+ - productTemplate
description: the milestone template reference
refereneceId:
type: number
@@ -3860,7 +4777,6 @@ definitions:
metadata:
type: object
description: the milestone template metadata
-
MilestoneTemplateBodyParam:
title: Milestone template body param
type: object
@@ -3868,8 +4784,7 @@ definitions:
- param
properties:
param:
- $ref: "#/definitions/MilestoneTemplateRequest"
-
+ $ref: '#/definitions/MilestoneTemplateRequest'
MilestoneCloneTemplateRequest:
title: Milestone clone template request object
type: object
@@ -3882,7 +4797,7 @@ definitions:
sourceReference:
type: string
enum:
- - productTemplate
+ - productTemplate
description: the source reference to clone the milestone templates from
sourceReferenceId:
type: number
@@ -3892,14 +4807,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 +4847,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 +4866,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 +4888,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 +4912,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 +4957,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 +4987,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 +5013,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 +5031,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 +5061,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
+ config:
+ description: config 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:
+ config:
+ 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
+ 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
+ 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:
+ config:
+ description: config json
+ type: object