diff --git a/README.md b/README.md index 2a18f10c..e2e5a707 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ Run image: `docker run -p 3000:3000 -i -t -e DB_HOST=172.17.0.1 tc_projects_services` 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** to verify endpoints. +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`. diff --git a/postman.json b/postman.json index 11ebff26..4447684b 100644 --- a/postman.json +++ b/postman.json @@ -2345,6 +2345,238 @@ "response": [] } ] + }, + { + "name": "Project Templates", + "description": "", + "item": [ + { + "name": "Create project template", + "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 \"name\":\"new name\",\r\n \"key\":\"new key\",\r\n \"category\":\"new category\",\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": "{{api-url}}/v4/projectTemplates" + }, + "response": [] + }, + { + "name": "List project templates", + "request": { + "method": "GET", + "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 },\r\n \"phases\":{\r\n \"phase1\":\"phase 1\"\r\n }\r\n }\r\n}" + }, + "url": "{{api-url}}/v4/projectTemplates" + }, + "response": [] + }, + { + "name": "Get project template", + "request": { + "method": "GET", + "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 },\r\n \"phases\":{\r\n \"phase1\":\"phase 1\"\r\n }\r\n }\r\n}" + }, + "url": "{{api-url}}/v4/projectTemplates/1" + }, + "response": [] + }, + { + "name": "Update project template", + "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 \"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": "{{api-url}}/v4/projectTemplates/1" + }, + "response": [] + }, + { + "name": "Delete project template", + "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": "{{api-url}}/v4/projectTemplates/1" + }, + "response": [] + } + ] + }, + { + "name": "Product Templates", + "description": "", + "item": [ + { + "name": "Create product template", + "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 \"name\":\"new name\",\r\n \"productKey\":\"new productKey\",\r\n \"icon\":\"http://example.com/icon-new.ico\",\r\n \"brief\": \"new brief\",\r\n \"details\": \"new details\",\r\n \"aliases\":{\r\n \"alias1\":\"alias 1\"\r\n },\r\n \"template\":{\r\n \"template1\":\"template 1\"\r\n }\r\n }\r\n}" + }, + "url": "{{api-url}}/v4/productTemplates" + }, + "response": [] + }, + { + "name": "List product templates", + "request": { + "method": "GET", + "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 },\r\n \"phases\":{\r\n \"phase1\":\"phase 1\"\r\n }\r\n }\r\n}" + }, + "url": "{{api-url}}/v4/productTemplates" + }, + "response": [] + }, + { + "name": "Get product template", + "request": { + "method": "GET", + "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 },\r\n \"phases\":{\r\n \"phase1\":\"phase 1\"\r\n }\r\n }\r\n}" + }, + "url": "{{api-url}}/v4/productTemplates/1" + }, + "response": [] + }, + { + "name": "Update product template", + "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 \"name\":\"new name\",\r\n \"productKey\":\"new productKey\",\r\n \"icon\":\"http://example.com/icon-new.ico\",\r\n \"brief\": \"new brief\",\r\n \"details\": \"new details\",\r\n \"aliases\":{\r\n \"alias1\":\"scope 1\",\r\n \"alias2\": [\"a\"]\r\n },\r\n \"template\":{\r\n \"template1\":\"template 1\",\r\n \"template2\": {\r\n \t\"another\": \"another\"\r\n }\r\n }\r\n }\r\n}" + }, + "url": "{{api-url}}/v4/productTemplates/1" + }, + "response": [] + }, + { + "name": "Delete product template", + "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": "{{api-url}}/v4/productTemplates/1" + }, + "response": [] + } + ] } ] } \ No newline at end of file diff --git a/postman_environment.json b/postman_environment.json index 4f8f54be..12fab912 100644 --- a/postman_environment.json +++ b/postman_environment.json @@ -1,22 +1,23 @@ { - "id": "1d4b6c34-6da6-8651-3372-9c6d4d09cc8c", - "name": "project service", + "id": "e6b30b4b-1388-4622-8314-bc49ba1d752b", + "name": "tc-project-service", "values": [ { - "enabled": true, "key": "api-url", "value": "http://localhost:3000", - "type": "text" + "description": "", + "type": "text", + "enabled": true }, { - "enabled": true, "key": "jwt-token", - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJwc2hhaDEiLCJleHAiOjI0NjI0OTQ2MTgsInVzZXJJZCI6IjQwMTM1OTc4IiwiaWF0IjoxNDYyNDk0MDE4LCJlbWFpbCI6InBzaGFoMUB0ZXN0LmNvbSIsImp0aSI6ImY0ZTFhNTE0LTg5ODAtNDY0MC04ZWM1LWUzNmUzMWE3ZTg0OSJ9.XuNN7tpMOXvBG1QwWRQROj7NfuUbqhkjwn39Vy4tR5I", - "type": "text" + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", + "description": "", + "type": "text", + "enabled": true } ], - "timestamp": 1526351351170, "_postman_variable_scope": "environment", - "_postman_exported_at": "2018-05-15T14:19:14.630Z", - "_postman_exported_using": "Postman/5.5.2" + "_postman_exported_at": "2018-05-18T18:54:18.167Z", + "_postman_exported_using": "Postman/6.0.10" } \ No newline at end of file diff --git a/src/models/productTemplate.js b/src/models/productTemplate.js new file mode 100644 index 00000000..72d7bc30 --- /dev/null +++ b/src/models/productTemplate.js @@ -0,0 +1,32 @@ +/* eslint-disable valid-jsdoc */ + +/** + * The Product Template model + */ +module.exports = (sequelize, DataTypes) => { + const ProductTemplate = sequelize.define('ProductTemplate', { + id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, + name: { type: DataTypes.STRING(255), allowNull: false }, + productKey: { type: DataTypes.STRING(45), allowNull: false }, + icon: { type: DataTypes.STRING(255), allowNull: false }, + brief: { type: DataTypes.STRING(45), allowNull: false }, + details: { type: DataTypes.STRING(255), allowNull: false }, + aliases: { type: DataTypes.JSON, allowNull: false }, + template: { type: DataTypes.JSON, allowNull: false }, + deletedAt: DataTypes.DATE, + createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + deletedBy: DataTypes.BIGINT, + createdBy: { type: DataTypes.BIGINT, allowNull: false }, + updatedBy: { type: DataTypes.BIGINT, allowNull: false }, + }, { + tableName: 'product_templates', + paranoid: true, + timestamps: true, + updatedAt: 'updatedAt', + createdAt: 'createdAt', + deletedAt: 'deletedAt', + }); + + return ProductTemplate; +}; diff --git a/src/models/projectTemplate.js b/src/models/projectTemplate.js new file mode 100644 index 00000000..206fa9e0 --- /dev/null +++ b/src/models/projectTemplate.js @@ -0,0 +1,30 @@ +/* eslint-disable valid-jsdoc */ + +/** + * The Project Template model + */ +module.exports = (sequelize, DataTypes) => { + const ProjectTemplate = sequelize.define('ProjectTemplate', { + id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, + name: { type: DataTypes.STRING(255), allowNull: false }, + key: { type: DataTypes.STRING(45), allowNull: false }, + category: { type: DataTypes.STRING(45), allowNull: false }, + scope: { type: DataTypes.JSON, allowNull: false }, + phases: { type: DataTypes.JSON, allowNull: false }, + deletedAt: DataTypes.DATE, + createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + deletedBy: DataTypes.BIGINT, + createdBy: { type: DataTypes.BIGINT, allowNull: false }, + updatedBy: { type: DataTypes.BIGINT, allowNull: false }, + }, { + tableName: 'project_templates', + paranoid: true, + timestamps: true, + updatedAt: 'updatedAt', + createdAt: 'createdAt', + deletedAt: 'deletedAt', + }); + + return ProjectTemplate; +}; diff --git a/src/permissions/connectManagerOrAdmin.ops.js b/src/permissions/connectManagerOrAdmin.ops.js new file mode 100644 index 00000000..0a5fb15a --- /dev/null +++ b/src/permissions/connectManagerOrAdmin.ops.js @@ -0,0 +1,18 @@ +import util from '../util'; +import { MANAGER_ROLES } from '../constants'; + + +/** + * Only Connect Manager, Connect Admin, and administrator are allowed to perform the operations + * @param {Object} req the express request instance + * @return {Promise} returns a promise + */ +module.exports = req => new Promise((resolve, reject) => { + const hasAccess = util.hasRoles(req, MANAGER_ROLES); + + if (!hasAccess) { + return reject(new Error('You do not have permissions to perform this action')); + } + + return resolve(true); +}); diff --git a/src/permissions/index.js b/src/permissions/index.js index 3fc2a2a5..ea1adf72 100644 --- a/src/permissions/index.js +++ b/src/permissions/index.js @@ -6,6 +6,7 @@ const projectEdit = require('./project.edit'); const projectDelete = require('./project.delete'); const projectMemberDelete = require('./projectMember.delete'); const projectAdmin = require('./admin.ops'); +const connectManagerOrAdmin = require('./connectManagerOrAdmin.ops'); module.exports = () => { Authorizer.setDeniedStatusCode(403); @@ -23,6 +24,17 @@ module.exports = () => { Authorizer.setPolicy('project.downloadAttachment', projectView); Authorizer.setPolicy('project.updateMember', projectEdit); Authorizer.setPolicy('project.admin', projectAdmin); + + Authorizer.setPolicy('projectTemplate.create', connectManagerOrAdmin); + Authorizer.setPolicy('projectTemplate.edit', connectManagerOrAdmin); + Authorizer.setPolicy('projectTemplate.delete', connectManagerOrAdmin); + Authorizer.setPolicy('projectTemplate.view', true); + + Authorizer.setPolicy('productTemplate.create', connectManagerOrAdmin); + Authorizer.setPolicy('productTemplate.edit', connectManagerOrAdmin); + Authorizer.setPolicy('productTemplate.delete', connectManagerOrAdmin); + Authorizer.setPolicy('productTemplate.view', true); + Authorizer.setPolicy('project.addProjectPhase', projectEdit); Authorizer.setPolicy('project.updateProjectPhase', projectEdit); Authorizer.setPolicy('project.deleteProjectPhase', projectEdit); diff --git a/src/routes/index.js b/src/routes/index.js index 4c7291a5..63250efe 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -26,7 +26,9 @@ 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(?!\\/health).*`), jwtAuth()); +router.all( + RegExp(`\\/${apiVersion}\\/(projects|projectTemplates|productTemplates)(?!\\/health).*`), + jwtAuth()); // Register all the routes router.route('/v4/projects') @@ -51,18 +53,37 @@ router.route('/v4/projects/:projectId(\\d+)') .delete(require('./projects/delete')); router.route('/v4/projects/:projectId(\\d+)/members') - .post(require('./projectMembers/create')); + .post(require('./projectMembers/create')); router.route('/v4/projects/:projectId(\\d+)/members/:id(\\d+)') - .delete(require('./projectMembers/delete')) - .patch(require('./projectMembers/update')); + .delete(require('./projectMembers/delete')) + .patch(require('./projectMembers/update')); router.route('/v4/projects/:projectId(\\d+)/attachments') - .post(require('./attachments/create')); + .post(require('./attachments/create')); + router.route('/v4/projects/:projectId(\\d+)/attachments/:id(\\d+)') - .get(require('./attachments/download')) - .patch(require('./attachments/update')) - .delete(require('./attachments/delete')); + .get(require('./attachments/download')) + .patch(require('./attachments/update')) + .delete(require('./attachments/delete')); + +router.route('/v4/projectTemplates') + .post(require('./projectTemplates/create')) + .get(require('./projectTemplates/list')); + +router.route('/v4/projectTemplates/:templateId(\\d+)') + .get(require('./projectTemplates/get')) + .patch(require('./projectTemplates/update')) + .delete(require('./projectTemplates/delete')); + +router.route('/v4/productTemplates') + .post(require('./productTemplates/create')) + .get(require('./productTemplates/list')); + +router.route('/v4/productTemplates/:templateId(\\d+)') + .get(require('./productTemplates/get')) + .patch(require('./productTemplates/update')) + .delete(require('./productTemplates/delete')); router.route('/v4/projects/:projectId(\\d+)/phases') .get(require('./phases/list')) diff --git a/src/routes/productTemplates/create.js b/src/routes/productTemplates/create.js new file mode 100644 index 00000000..b00363e3 --- /dev/null +++ b/src/routes/productTemplates/create.js @@ -0,0 +1,51 @@ +/** + * API to add a product template + */ +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({ + id: Joi.any().strip(), + name: Joi.string().max(255).required(), + productKey: Joi.string().max(45).required(), + icon: Joi.string().max(255).required(), + brief: Joi.string().max(45).required(), + details: Joi.string().max(255).required(), + aliases: Joi.object().required(), + template: 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('productTemplate.create'), + (req, res, next) => { + const entity = _.assign(req.body.param, { + createdBy: req.authUser.userId, + updatedBy: req.authUser.userId, + }); + + return models.ProductTemplate.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/productTemplates/create.spec.js b/src/routes/productTemplates/create.spec.js new file mode 100644 index 00000000..8476a5ef --- /dev/null +++ b/src/routes/productTemplates/create.spec.js @@ -0,0 +1,157 @@ +/** + * Tests for create.js + */ +import chai from 'chai'; +import request from 'supertest'; + +import server from '../../app'; +import testUtil from '../../tests/util'; + +const should = chai.should(); + +describe('CREATE product template', () => { + describe('POST /productTemplates', () => { + const body = { + param: { + name: 'name 1', + productKey: 'productKey 1', + icon: 'http://example.com/icon1.ico', + brief: 'brief 1', + details: 'details 1', + aliases: { + alias1: { + subAlias1A: 1, + subAlias1B: 2, + }, + alias2: [1, 2, 3], + }, + template: { + template1: { + name: 'template 1', + details: { + anyDetails: 'any details 1', + }, + others: ['others 11', 'others 12'], + }, + template2: { + name: 'template 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/productTemplates') + .send(body) + .expect(403, done); + }); + + it('should return 403 for member', (done) => { + request(server) + .post('/v4/productTemplates') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send(body) + .expect(403, done); + }); + + it('should return 403 for copilot', (done) => { + request(server) + .post('/v4/productTemplates') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send(body) + .expect(403, done); + }); + + it('should return 422 if validations dont pass', (done) => { + const invalidBody = { + param: { + aliases: 'a', + template: 1, + }, + }; + + request(server) + .post('/v4/productTemplates') + .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/productTemplates') + .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.name.should.be.eql(body.param.name); + resJson.productKey.should.be.eql(body.param.productKey); + resJson.icon.should.be.eql(body.param.icon); + resJson.brief.should.be.eql(body.param.brief); + resJson.details.should.be.eql(body.param.details); + resJson.aliases.should.be.eql(body.param.aliases); + resJson.template.should.be.eql(body.param.template); + + 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 manager', (done) => { + request(server) + .post('/v4/productTemplates') + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + const resJson = res.body.result.content; + resJson.createdBy.should.be.eql(40051334); // manager + resJson.updatedBy.should.be.eql(40051334); // manager + done(); + }); + }); + + it('should return 201 for connect admin', (done) => { + request(server) + .post('/v4/productTemplates') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + const resJson = res.body.result.content; + resJson.createdBy.should.be.eql(40051336); // connect admin + resJson.updatedBy.should.be.eql(40051336); // connect admin + done(); + }); + }); + }); +}); diff --git a/src/routes/productTemplates/delete.js b/src/routes/productTemplates/delete.js new file mode 100644 index 00000000..81c65b6b --- /dev/null +++ b/src/routes/productTemplates/delete.js @@ -0,0 +1,55 @@ +/** + * API to delete a product template + */ +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: { + templateId: Joi.number().integer().positive().required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('productTemplate.delete'), + (req, res, next) => { + const where = { + deletedAt: { $eq: null }, + id: req.params.templateId, + }; + + return models.sequelize.transaction(tx => + // Update the deletedBy + models.ProductTemplate.update({ deletedBy: req.authUser.userId }, { + where, + returning: true, + raw: true, + transaction: tx, + }) + .then((updatedResults) => { + // Not found + if (updatedResults[0] === 0) { + const apiErr = new Error(`Product template not found for template id ${req.params.templateId}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + + // Soft delete + return models.ProductTemplate.destroy({ + where, + transaction: tx, + raw: true, + }); + }) + .then(() => { + res.status(204).end(); + }) + .catch(next), + ); + }, +]; diff --git a/src/routes/productTemplates/delete.spec.js b/src/routes/productTemplates/delete.spec.js new file mode 100644 index 00000000..058fea4c --- /dev/null +++ b/src/routes/productTemplates/delete.spec.js @@ -0,0 +1,129 @@ +/** + * Tests for delete.js + */ +import request from 'supertest'; + +import models from '../../models'; +import server from '../../app'; +import testUtil from '../../tests/util'; + + +describe('DELETE product template', () => { + let templateId; + + beforeEach(() => testUtil.clearDb() + .then(() => models.ProductTemplate.create({ + name: 'name 1', + productKey: 'productKey 1', + icon: 'http://example.com/icon1.ico', + brief: 'brief 1', + details: 'details 1', + aliases: { + alias1: { + subAlias1A: 1, + subAlias1B: 2, + }, + alias2: [1, 2, 3], + }, + template: { + template1: { + name: 'template 1', + details: { + anyDetails: 'any details 1', + }, + others: ['others 11', 'others 12'], + }, + template2: { + name: 'template 2', + details: { + anyDetails: 'any details 2', + }, + others: ['others 21', 'others 22'], + }, + }, + createdBy: 1, + updatedBy: 2, + })).then((template) => { + templateId = template.id; + return Promise.resolve(); + }), + ); + after(testUtil.clearDb); + + describe('DELETE /productTemplates/{templateId}', () => { + it('should return 403 if user is not authenticated', (done) => { + request(server) + .delete(`/v4/productTemplates/${templateId}`) + .expect(403, done); + }); + + it('should return 403 for member', (done) => { + request(server) + .delete(`/v4/productTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(403, done); + }); + + it('should return 403 for copilot', (done) => { + request(server) + .delete(`/v4/productTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(403, done); + }); + + it('should return 404 for non-existed template', (done) => { + request(server) + .delete('/v4/productTemplates/1234') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + + it('should return 404 for deleted template', (done) => { + models.ProductTemplate.destroy({ where: { id: templateId } }) + .then(() => { + request(server) + .delete(`/v4/productTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + }); + + it('should return 204, for admin, if template was successfully removed', (done) => { + request(server) + .delete(`/v4/productTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(204) + .end(done); + }); + + it('should return 204, for connect admin, if template was successfully removed', (done) => { + request(server) + .delete(`/v4/productTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(204) + .end(done); + }); + + it('should return 204, for connect manager, if template was successfully removed', (done) => { + request(server) + .delete(`/v4/productTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(204) + .end(done); + }); + }); +}); diff --git a/src/routes/productTemplates/get.js b/src/routes/productTemplates/get.js new file mode 100644 index 00000000..e660f979 --- /dev/null +++ b/src/routes/productTemplates/get.js @@ -0,0 +1,41 @@ +/** + * API to get a product template + */ +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: { + templateId: Joi.number().integer().positive().required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('productTemplate.view'), + (req, res, next) => models.ProductTemplate.findOne({ + where: { + deletedAt: { $eq: null }, + id: req.params.templateId, + }, + attributes: { exclude: ['deletedAt', 'deletedBy'] }, + raw: true, + }) + .then((productTemplate) => { + // Not found + if (!productTemplate) { + const apiErr = new Error(`Product template not found for product id ${req.params.templateId}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + + res.json(util.wrapResponse(req.id, productTemplate)); + return Promise.resolve(); + }) + .catch(next), +]; diff --git a/src/routes/productTemplates/get.spec.js b/src/routes/productTemplates/get.spec.js new file mode 100644 index 00000000..0fbdf37e --- /dev/null +++ b/src/routes/productTemplates/get.spec.js @@ -0,0 +1,153 @@ +/** + * 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 product template', () => { + const template = { + name: 'name 1', + productKey: 'productKey 1', + icon: 'http://example.com/icon1.ico', + brief: 'brief 1', + details: 'details 1', + aliases: { + alias1: { + subAlias1A: 1, + subAlias1B: 2, + }, + alias2: [1, 2, 3], + }, + template: { + template1: { + name: 'template 1', + details: { + anyDetails: 'any details 1', + }, + others: ['others 11', 'others 12'], + }, + template2: { + name: 'template 2', + details: { + anyDetails: 'any details 2', + }, + others: ['others 21', 'others 22'], + }, + }, + createdBy: 1, + updatedBy: 2, + }; + + let templateId; + + beforeEach(() => testUtil.clearDb() + .then(() => models.ProductTemplate.create(template)) + .then((createdTemplate) => { + templateId = createdTemplate.id; + return Promise.resolve(); + }), + ); + after(testUtil.clearDb); + + describe('GET /productTemplates/{templateId}', () => { + it('should return 403 if user is not authenticated', (done) => { + request(server) + .get(`/v4/productTemplates/${templateId}`) + .expect(403, done); + }); + + it('should return 404 for non-existed template', (done) => { + request(server) + .get('/v4/productTemplates/1234') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + + it('should return 404 for deleted template', (done) => { + models.ProductTemplate.destroy({ where: { id: templateId } }) + .then(() => { + request(server) + .get(`/v4/productTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + }); + + it('should return 200 for admin', (done) => { + request(server) + .get(`/v4/productTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end((err, res) => { + const resJson = res.body.result.content; + resJson.id.should.be.eql(templateId); + resJson.name.should.be.eql(template.name); + resJson.productKey.should.be.eql(template.productKey); + resJson.icon.should.be.eql(template.icon); + resJson.brief.should.be.eql(template.brief); + resJson.details.should.be.eql(template.details); + resJson.aliases.should.be.eql(template.aliases); + resJson.template.should.be.eql(template.template); + + resJson.createdBy.should.be.eql(template.createdBy); + should.exist(resJson.createdAt); + resJson.updatedBy.should.be.eql(template.updatedBy); + 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) + .get(`/v4/productTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for connect manager', (done) => { + request(server) + .get(`/v4/productTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for member', (done) => { + request(server) + .get(`/v4/productTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(200, done); + }); + + it('should return 200 for copilot', (done) => { + request(server) + .get(`/v4/productTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(200, done); + }); + }); +}); diff --git a/src/routes/productTemplates/list.js b/src/routes/productTemplates/list.js new file mode 100644 index 00000000..11a9e276 --- /dev/null +++ b/src/routes/productTemplates/list.js @@ -0,0 +1,23 @@ +/** + * API to list all product templates + */ +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../util'; +import models from '../../models'; + +const permissions = tcMiddleware.permissions; + +module.exports = [ + permissions('productTemplate.view'), + (req, res, next) => models.ProductTemplate.findAll({ + where: { + deletedAt: { $eq: null }, + }, + attributes: { exclude: ['deletedAt', 'deletedBy'] }, + raw: true, + }) + .then((productTemplates) => { + res.json(util.wrapResponse(req.id, productTemplates)); + }) + .catch(next), +]; diff --git a/src/routes/productTemplates/list.spec.js b/src/routes/productTemplates/list.spec.js new file mode 100644 index 00000000..e487d777 --- /dev/null +++ b/src/routes/productTemplates/list.spec.js @@ -0,0 +1,148 @@ +/** + * 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 product templates', () => { + const templates = [ + { + name: 'name 1', + productKey: 'productKey 1', + icon: 'http://example.com/icon1.ico', + brief: 'brief 1', + details: 'details 1', + aliases: { + alias1: { + subAlias1A: 1, + subAlias1B: 2, + }, + alias2: [1, 2, 3], + }, + template: { + template1: { + name: 'template 1', + details: { + anyDetails: 'any details 1', + }, + others: ['others 11', 'others 12'], + }, + template2: { + name: 'template 2', + details: { + anyDetails: 'any details 2', + }, + others: ['others 21', 'others 22'], + }, + }, + createdBy: 1, + updatedBy: 2, + }, + { + name: 'template 2', + productKey: 'productKey 2', + icon: 'http://example.com/icon2.ico', + brief: 'brief 2', + details: 'details 2', + aliases: {}, + template: {}, + createdBy: 3, + updatedBy: 4, + }, + ]; + + let templateId; + + beforeEach(() => testUtil.clearDb() + .then(() => models.ProductTemplate.create(templates[0])) + .then((createdTemplate) => { + templateId = createdTemplate.id; + return models.ProductTemplate.create(templates[1]); + }).then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('GET /productTemplates', () => { + it('should return 403 if user is not authenticated', (done) => { + request(server) + .get('/v4/productTemplates') + .expect(403, done); + }); + + it('should return 200 for admin', (done) => { + request(server) + .get('/v4/productTemplates') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end((err, res) => { + const template = templates[0]; + + const resJson = res.body.result.content; + resJson.should.have.length(2); + resJson[0].id.should.be.eql(templateId); + resJson[0].name.should.be.eql(template.name); + resJson[0].productKey.should.be.eql(template.productKey); + resJson[0].icon.should.be.eql(template.icon); + resJson[0].brief.should.be.eql(template.brief); + resJson[0].details.should.be.eql(template.details); + resJson[0].aliases.should.be.eql(template.aliases); + resJson[0].template.should.be.eql(template.template); + + resJson[0].createdBy.should.be.eql(template.createdBy); + should.exist(resJson[0].createdAt); + resJson[0].updatedBy.should.be.eql(template.updatedBy); + should.exist(resJson[0].updatedAt); + should.not.exist(resJson[0].deletedBy); + should.not.exist(resJson[0].deletedAt); + + done(); + }); + }); + + it('should return 200 for connect admin', (done) => { + request(server) + .get('/v4/productTemplates') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for connect manager', (done) => { + request(server) + .get('/v4/productTemplates') + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for member', (done) => { + request(server) + .get('/v4/productTemplates') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(200, done); + }); + + it('should return 200 for copilot', (done) => { + request(server) + .get('/v4/productTemplates') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(200, done); + }); + }); +}); diff --git a/src/routes/productTemplates/update.js b/src/routes/productTemplates/update.js new file mode 100644 index 00000000..c5ebf633 --- /dev/null +++ b/src/routes/productTemplates/update.js @@ -0,0 +1,72 @@ +/** + * API to update a product template + */ +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: { + templateId: Joi.number().integer().positive().required(), + }, + body: { + param: Joi.object().keys({ + id: Joi.any().strip(), + name: Joi.string().max(255).required(), + productKey: Joi.string().max(45).required(), + icon: Joi.string().max(255).required(), + brief: Joi.string().max(45).required(), + details: Joi.string().max(255).required(), + aliases: Joi.object().required(), + template: 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('productTemplate.edit'), + (req, res, next) => { + const entityToUpdate = _.assign(req.body.param, { + updatedBy: req.authUser.userId, + }); + + return models.ProductTemplate.findOne({ + where: { + deletedAt: { $eq: null }, + id: req.params.templateId, + }, + attributes: { exclude: ['deletedAt', 'deletedBy'] }, + }) + .then((productTemplate) => { + // Not found + if (!productTemplate) { + const apiErr = new Error(`Product template not found for template id ${req.params.templateId}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + + // Merge JSON fields + entityToUpdate.aliases = util.mergeJsonObjects(productTemplate.aliases, entityToUpdate.aliases); + entityToUpdate.template = util.mergeJsonObjects(productTemplate.template, entityToUpdate.template); + + return productTemplate.update(entityToUpdate); + }) + .then((productTemplate) => { + res.json(util.wrapResponse(req.id, productTemplate)); + return Promise.resolve(); + }) + .catch(next); + }, +]; diff --git a/src/routes/productTemplates/update.spec.js b/src/routes/productTemplates/update.spec.js new file mode 100644 index 00000000..0d5c5e0e --- /dev/null +++ b/src/routes/productTemplates/update.spec.js @@ -0,0 +1,244 @@ +/** + * 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('UPDATE product template', () => { + const template = { + name: 'name 1', + productKey: 'productKey 1', + icon: 'http://example.com/icon1.ico', + brief: 'brief 1', + details: 'details 1', + aliases: { + alias1: { + subAlias1A: 1, + subAlias1B: 2, + }, + alias2: [1, 2, 3], + }, + template: { + template1: { + name: 'template 1', + details: { + anyDetails: 'any details 1', + }, + others: ['others 11', 'others 12'], + }, + template2: { + name: 'template 2', + details: { + anyDetails: 'any details 2', + }, + others: ['others 21', 'others 22'], + }, + }, + createdBy: 1, + updatedBy: 2, + }; + + let templateId; + + beforeEach(() => testUtil.clearDb() + .then(() => models.ProductTemplate.create(template)) + .then((createdTemplate) => { + templateId = createdTemplate.id; + return Promise.resolve(); + }), + ); + after(testUtil.clearDb); + + describe('PATCH /productTemplates/{templateId}', () => { + const body = { + param: { + name: 'template 1 - update', + productKey: 'productKey 1 - update', + icon: 'http://example.com/icon1-update.ico', + brief: 'brief 1 - update', + details: 'details 1 - update', + aliases: { + alias1: { + subAlias1A: 11, + subAlias1C: 'new', + }, + alias2: [4], + alias3: 'new', + }, + template: { + template1: { + name: 'template 1 - update', + details: { + anyDetails: 'any details 1 - update', + newDetails: 'new', + }, + others: ['others new'], + }, + template3: { + name: 'template 3', + details: { + anyDetails: 'any details 3', + }, + others: ['others 31', 'others 32'], + }, + }, + }, + }; + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .patch(`/v4/productTemplates/${templateId}`) + .send(body) + .expect(403, done); + }); + + it('should return 403 for member', (done) => { + request(server) + .patch(`/v4/productTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send(body) + .expect(403, done); + }); + + it('should return 403 for copilot', (done) => { + request(server) + .patch(`/v4/productTemplates/${templateId}`) + .send(body) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(403, done); + }); + + it('should return 422 for invalid request', (done) => { + const invalidBody = { + param: { + aliases: 'a', + template: 1, + }, + }; + + request(server) + .patch(`/v4/productTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(invalidBody) + .expect(422, done); + }); + + it('should return 404 for non-existed template', (done) => { + request(server) + .patch('/v4/productTemplates/1234') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(body) + .expect(404, done); + }); + + it('should return 404 for deleted template', (done) => { + models.ProductTemplate.destroy({ where: { id: templateId } }) + .then(() => { + request(server) + .patch(`/v4/productTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(body) + .expect(404, done); + }); + }); + + it('should return 200 for admin', (done) => { + request(server) + .patch(`/v4/productTemplates/${templateId}`) + .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); + resJson.name.should.be.eql(body.param.name); + resJson.productKey.should.be.eql(body.param.productKey); + resJson.icon.should.be.eql(body.param.icon); + resJson.brief.should.be.eql(body.param.brief); + resJson.details.should.be.eql(body.param.details); + + resJson.aliases.should.be.eql({ + alias1: { + subAlias1A: 11, + subAlias1B: 2, + subAlias1C: 'new', + }, + alias2: [4], + alias3: 'new', + }); + resJson.template.should.be.eql({ + template1: { + name: 'template 1 - update', + details: { + anyDetails: 'any details 1 - update', + newDetails: 'new', + }, + others: ['others new'], + }, + template2: { + name: 'template 2', + details: { + anyDetails: 'any details 2', + }, + others: ['others 21', 'others 22'], + }, + template3: { + name: 'template 3', + details: { + anyDetails: 'any details 3', + }, + others: ['others 31', 'others 32'], + }, + }); + 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/productTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .send(body) + .expect(200) + .end(done); + }); + + it('should return 200 for connect manager', (done) => { + request(server) + .patch(`/v4/productTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send(body) + .expect(200) + .end(done); + }); + }); +}); diff --git a/src/routes/projectTemplates/create.js b/src/routes/projectTemplates/create.js new file mode 100644 index 00000000..4c19fc0f --- /dev/null +++ b/src/routes/projectTemplates/create.js @@ -0,0 +1,49 @@ +/** + * API to add a project template + */ +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({ + id: Joi.any().strip(), + name: Joi.string().max(255).required(), + key: Joi.string().max(45).required(), + category: Joi.string().max(45).required(), + scope: Joi.object().required(), + phases: Joi.object().required(), + createdAt: Joi.any().strip(), + updatedAt: Joi.any().strip(), + deletedAt: Joi.any().strip(), + createdBy: Joi.any().strip(), + updatedBy: Joi.any().strip(), + deletedBy: Joi.any().strip(), + }).required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('projectTemplate.create'), + (req, res, next) => { + 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 new file mode 100644 index 00000000..afb46113 --- /dev/null +++ b/src/routes/projectTemplates/create.spec.js @@ -0,0 +1,153 @@ +/** + * Tests for create.js + */ +import chai from 'chai'; +import request from 'supertest'; + +import server from '../../app'; +import testUtil from '../../tests/util'; + +const should = chai.should(); + +describe('CREATE project template', () => { + describe('POST /projectTemplates', () => { + const body = { + param: { + name: 'template 1', + key: 'key 1', + category: 'category 1', + 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'], + }, + }, + }, + }; + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .post('/v4/projectTemplates') + .send(body) + .expect(403, done); + }); + + it('should return 403 for member', (done) => { + request(server) + .post('/v4/projectTemplates') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send(body) + .expect(403, done); + }); + + it('should return 403 for copilot', (done) => { + request(server) + .post('/v4/projectTemplates') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send(body) + .expect(403, done); + }); + + it('should return 422 if validations dont pass', (done) => { + const invalidBody = { + param: { + scope: 'a', + phases: 1, + }, + }; + + request(server) + .post('/v4/projectTemplates') + .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/projectTemplates') + .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.name.should.be.eql(body.param.name); + resJson.key.should.be.eql(body.param.key); + resJson.category.should.be.eql(body.param.category); + resJson.scope.should.be.eql(body.param.scope); + resJson.phases.should.be.eql(body.param.phases); + + 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 manager', (done) => { + request(server) + .post('/v4/projectTemplates') + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + const resJson = res.body.result.content; + resJson.createdBy.should.be.eql(40051334); // manager + resJson.updatedBy.should.be.eql(40051334); // manager + done(); + }); + }); + + it('should return 201 for connect admin', (done) => { + request(server) + .post('/v4/projectTemplates') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + const resJson = res.body.result.content; + resJson.createdBy.should.be.eql(40051336); // connect admin + resJson.updatedBy.should.be.eql(40051336); // connect admin + done(); + }); + }); + }); +}); diff --git a/src/routes/projectTemplates/delete.js b/src/routes/projectTemplates/delete.js new file mode 100644 index 00000000..4db9a855 --- /dev/null +++ b/src/routes/projectTemplates/delete.js @@ -0,0 +1,55 @@ +/** + * API to delete a project template + */ +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: { + templateId: Joi.number().integer().positive().required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('projectTemplate.delete'), + (req, res, next) => { + const where = { + deletedAt: { $eq: null }, + id: req.params.templateId, + }; + + return models.sequelize.transaction(tx => + // Update the deletedBy + models.ProjectTemplate.update({ deletedBy: req.authUser.userId }, { + where, + returning: true, + raw: true, + transaction: tx, + }) + .then((updatedResults) => { + // Not found + if (updatedResults[0] === 0) { + const apiErr = new Error(`Project template not found for template id ${req.params.templateId}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + + // Soft delete + return models.ProjectTemplate.destroy({ + where, + transaction: tx, + raw: true, + }); + }) + .then(() => { + res.status(204).end(); + }) + .catch(next), + ); + }, +]; diff --git a/src/routes/projectTemplates/delete.spec.js b/src/routes/projectTemplates/delete.spec.js new file mode 100644 index 00000000..27973d7e --- /dev/null +++ b/src/routes/projectTemplates/delete.spec.js @@ -0,0 +1,127 @@ +/** + * Tests for delete.js + */ +import request from 'supertest'; + +import models from '../../models'; +import server from '../../app'; +import testUtil from '../../tests/util'; + + +describe('DELETE project template', () => { + let templateId; + + beforeEach(() => testUtil.clearDb() + .then(() => models.ProjectTemplate.create({ + name: 'template 1', + key: 'key 1', + category: 'category 1', + 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, + })).then((template) => { + templateId = template.id; + return Promise.resolve(); + }), + ); + after(testUtil.clearDb); + + describe('DELETE /projectTemplates/{templateId}', () => { + it('should return 403 if user is not authenticated', (done) => { + request(server) + .delete(`/v4/projectTemplates/${templateId}`) + .expect(403, done); + }); + + it('should return 403 for member', (done) => { + request(server) + .delete(`/v4/projectTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(403, done); + }); + + it('should return 403 for copilot', (done) => { + request(server) + .delete(`/v4/projectTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(403, done); + }); + + it('should return 404 for non-existed template', (done) => { + request(server) + .delete('/v4/projectTemplates/1234') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + + it('should return 404 for deleted template', (done) => { + models.ProjectTemplate.destroy({ where: { id: templateId } }) + .then(() => { + request(server) + .delete(`/v4/projectTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + }); + + it('should return 204, for admin, if template was successfully removed', (done) => { + request(server) + .delete(`/v4/projectTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(204) + .end(done); + }); + + it('should return 204, for connect admin, if template was successfully removed', (done) => { + request(server) + .delete(`/v4/projectTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(204) + .end(done); + }); + + it('should return 204, for connect manager, if template was successfully removed', (done) => { + request(server) + .delete(`/v4/projectTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(204) + .end(done); + }); + }); +}); diff --git a/src/routes/projectTemplates/get.js b/src/routes/projectTemplates/get.js new file mode 100644 index 00000000..e81c0939 --- /dev/null +++ b/src/routes/projectTemplates/get.js @@ -0,0 +1,41 @@ +/** + * API to get a project template + */ +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: { + templateId: Joi.number().integer().positive().required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('projectTemplate.view'), + (req, res, next) => models.ProjectTemplate.findOne({ + where: { + deletedAt: { $eq: null }, + id: req.params.templateId, + }, + attributes: { exclude: ['deletedAt', 'deletedBy'] }, + raw: true, + }) + .then((projectTemplate) => { + // Not found + if (!projectTemplate) { + const apiErr = new Error(`Project template not found for project id ${req.params.templateId}`); + apiErr.status = 404; + return Promise.reject(apiErr); + } + + res.json(util.wrapResponse(req.id, projectTemplate)); + return Promise.resolve(); + }) + .catch(next), +]; diff --git a/src/routes/projectTemplates/get.spec.js b/src/routes/projectTemplates/get.spec.js new file mode 100644 index 00000000..4c9b2ccf --- /dev/null +++ b/src/routes/projectTemplates/get.spec.js @@ -0,0 +1,148 @@ +/** + * 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 project template', () => { + const template = { + name: 'template 1', + key: 'key 1', + category: 'category 1', + 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.ProjectTemplate.create(template)) + .then((createdTemplate) => { + templateId = createdTemplate.id; + return Promise.resolve(); + }), + ); + after(testUtil.clearDb); + + describe('GET /projectTemplates/{templateId}', () => { + it('should return 403 if user is not authenticated', (done) => { + request(server) + .get(`/v4/projectTemplates/${templateId}`) + .expect(403, done); + }); + + it('should return 404 for non-existed template', (done) => { + request(server) + .get('/v4/projectTemplates/1234') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + + it('should return 404 for deleted template', (done) => { + models.ProjectTemplate.destroy({ where: { id: templateId } }) + .then(() => { + request(server) + .get(`/v4/projectTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(404, done); + }); + }); + + it('should return 200 for admin', (done) => { + request(server) + .get(`/v4/projectTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end((err, res) => { + const resJson = res.body.result.content; + resJson.id.should.be.eql(templateId); + resJson.name.should.be.eql(template.name); + resJson.key.should.be.eql(template.key); + resJson.category.should.be.eql(template.category); + resJson.scope.should.be.eql(template.scope); + resJson.phases.should.be.eql(template.phases); + resJson.createdBy.should.be.eql(template.createdBy); + should.exist(resJson.createdAt); + resJson.updatedBy.should.be.eql(template.updatedBy); + 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) + .get(`/v4/projectTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for connect manager', (done) => { + request(server) + .get(`/v4/projectTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for member', (done) => { + request(server) + .get(`/v4/projectTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(200, done); + }); + + it('should return 200 for copilot', (done) => { + request(server) + .get(`/v4/projectTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(200, done); + }); + }); +}); diff --git a/src/routes/projectTemplates/list.js b/src/routes/projectTemplates/list.js new file mode 100644 index 00000000..3e83f2e4 --- /dev/null +++ b/src/routes/projectTemplates/list.js @@ -0,0 +1,23 @@ +/** + * API to list all project templates + */ +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import util from '../../util'; +import models from '../../models'; + +const permissions = tcMiddleware.permissions; + +module.exports = [ + permissions('projectTemplate.view'), + (req, res, next) => models.ProjectTemplate.findAll({ + where: { + deletedAt: { $eq: null }, + }, + attributes: { exclude: ['deletedAt', 'deletedBy'] }, + raw: true, + }) + .then((projectTemplates) => { + res.json(util.wrapResponse(req.id, projectTemplates)); + }) + .catch(next), +]; diff --git a/src/routes/projectTemplates/list.spec.js b/src/routes/projectTemplates/list.spec.js new file mode 100644 index 00000000..b68fc28d --- /dev/null +++ b/src/routes/projectTemplates/list.spec.js @@ -0,0 +1,141 @@ +/** + * 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 project templates', () => { + const templates = [ + { + name: 'template 1', + key: 'key 1', + category: 'category 1', + 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, + }, + { + name: 'template 2', + key: 'key 2', + category: 'category 2', + scope: {}, + phases: {}, + createdBy: 1, + updatedBy: 2, + }, + ]; + + let templateId; + + beforeEach(() => testUtil.clearDb() + .then(() => models.ProjectTemplate.create(templates[0])) + .then((createdTemplate) => { + templateId = createdTemplate.id; + return models.ProjectTemplate.create(templates[1]); + }).then(() => Promise.resolve()), + ); + after(testUtil.clearDb); + + describe('GET /projectTemplates', () => { + it('should return 403 if user is not authenticated', (done) => { + request(server) + .get('/v4/projectTemplates') + .expect(403, done); + }); + + it('should return 200 for admin', (done) => { + request(server) + .get('/v4/projectTemplates') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end((err, res) => { + const template = templates[0]; + + const resJson = res.body.result.content; + resJson.should.have.length(2); + resJson[0].id.should.be.eql(templateId); + resJson[0].name.should.be.eql(template.name); + resJson[0].key.should.be.eql(template.key); + resJson[0].category.should.be.eql(template.category); + resJson[0].scope.should.be.eql(template.scope); + resJson[0].phases.should.be.eql(template.phases); + resJson[0].createdBy.should.be.eql(template.createdBy); + should.exist(resJson[0].createdAt); + resJson[0].updatedBy.should.be.eql(template.updatedBy); + should.exist(resJson[0].updatedAt); + should.not.exist(resJson[0].deletedBy); + should.not.exist(resJson[0].deletedAt); + + done(); + }); + }); + + it('should return 200 for connect admin', (done) => { + request(server) + .get('/v4/projectTemplates') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for connect manager', (done) => { + request(server) + .get('/v4/projectTemplates') + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for member', (done) => { + request(server) + .get('/v4/projectTemplates') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(200, done); + }); + + it('should return 200 for copilot', (done) => { + request(server) + .get('/v4/projectTemplates') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(200, done); + }); + }); +}); diff --git a/src/routes/projectTemplates/update.js b/src/routes/projectTemplates/update.js new file mode 100644 index 00000000..8b88c84a --- /dev/null +++ b/src/routes/projectTemplates/update.js @@ -0,0 +1,70 @@ +/** + * API to update a project template + */ +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: { + templateId: Joi.number().integer().positive().required(), + }, + body: { + param: Joi.object().keys({ + id: Joi.any().strip(), + name: Joi.string().max(255).required(), + key: Joi.string().max(45).required(), + category: Joi.string().max(45).required(), + scope: Joi.object().required(), + phases: Joi.object().required(), + createdAt: Joi.any().strip(), + updatedAt: Joi.any().strip(), + deletedAt: Joi.any().strip(), + createdBy: Joi.any().strip(), + updatedBy: Joi.any().strip(), + deletedBy: Joi.any().strip(), + }).required(), + }, +}; + +module.exports = [ + validate(schema), + permissions('projectTemplate.edit'), + (req, res, next) => { + const entityToUpdate = _.assign(req.body.param, { + updatedBy: req.authUser.userId, + }); + + 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); + entityToUpdate.phases = util.mergeJsonObjects(projectTemplate.phases, entityToUpdate.phases); + + return projectTemplate.update(entityToUpdate); + }) + .then((projectTemplate) => { + res.json(util.wrapResponse(req.id, projectTemplate)); + return Promise.resolve(); + }) + .catch(next); + }, +]; diff --git a/src/routes/projectTemplates/update.spec.js b/src/routes/projectTemplates/update.spec.js new file mode 100644 index 00000000..dd286a77 --- /dev/null +++ b/src/routes/projectTemplates/update.spec.js @@ -0,0 +1,237 @@ +/** + * 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('UPDATE project template', () => { + const template = { + name: 'template 1', + key: 'key 1', + category: 'category 1', + 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.ProjectTemplate.create(template)) + .then((createdTemplate) => { + templateId = createdTemplate.id; + return Promise.resolve(); + }), + ); + after(testUtil.clearDb); + + describe('PATCH /projectTemplates/{templateId}', () => { + const body = { + param: { + name: 'template 1 - update', + key: 'key 1 - update', + category: 'category 1 - update', + scope: { + scope1: { + subScope1A: 11, + subScope1C: 'new', + }, + scope2: [4], + scope3: 'new', + }, + phases: { + phase1: { + name: 'phase 1 - update', + details: { + anyDetails: 'any details 1 - update', + newDetails: 'new', + }, + others: ['others new'], + }, + phase3: { + name: 'phase 3', + details: { + anyDetails: 'any details 3', + }, + others: ['others 31', 'others 32'], + }, + }, + }, + }; + + it('should return 403 if user is not authenticated', (done) => { + request(server) + .patch(`/v4/projectTemplates/${templateId}`) + .send(body) + .expect(403, done); + }); + + it('should return 403 for member', (done) => { + request(server) + .patch(`/v4/projectTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send(body) + .expect(403, done); + }); + + it('should return 403 for copilot', (done) => { + request(server) + .patch(`/v4/projectTemplates/${templateId}`) + .send(body) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(403, done); + }); + + it('should return 422 for invalid request', (done) => { + const invalidBody = { + param: { + scope: 'a', + phases: 1, + }, + }; + + request(server) + .patch(`/v4/projectTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(invalidBody) + .expect(422, done); + }); + + it('should return 404 for non-existed template', (done) => { + request(server) + .patch('/v4/projectTemplates/1234') + .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) + .patch(`/v4/projectTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(body) + .expect(404, done); + }); + }); + + it('should return 200 for admin', (done) => { + request(server) + .patch(`/v4/projectTemplates/${templateId}`) + .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); + resJson.name.should.be.eql(body.param.name); + resJson.key.should.be.eql(body.param.key); + resJson.category.should.be.eql(body.param.category); + resJson.scope.should.be.eql({ + scope1: { + subScope1A: 11, + subScope1B: 2, + subScope1C: 'new', + }, + scope2: [4], + scope3: 'new', + }); + resJson.phases.should.be.eql({ + phase1: { + name: 'phase 1 - update', + details: { + anyDetails: 'any details 1 - update', + newDetails: 'new', + }, + others: ['others new'], + }, + phase2: { + name: 'phase 2', + details: { + anyDetails: 'any details 2', + }, + others: ['others 21', 'others 22'], + }, + phase3: { + name: 'phase 3', + details: { + anyDetails: 'any details 3', + }, + others: ['others 31', 'others 32'], + }, + }); + 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/projectTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .send(body) + .expect(200) + .end(done); + }); + + it('should return 200 for connect manager', (done) => { + request(server) + .patch(`/v4/projectTemplates/${templateId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send(body) + .expect(200) + .end(done); + }); + }); +}); diff --git a/src/tests/seed.js b/src/tests/seed.js index a1f53c84..bbd0aa08 100644 --- a/src/tests/seed.js +++ b/src/tests/seed.js @@ -1,108 +1,194 @@ import models from '../models'; models.sequelize.sync({ force: true }) - .then(() => - models.Project.bulkCreate([{ - type: 'generic', - directProjectId: 9999999, - billingAccountId: 1, - name: 'test1', - description: 'test project1', - status: 'active', - details: {}, - createdBy: 1, - updatedBy: 1, - }, { - type: 'visual_design', - directProjectId: 1, - billingAccountId: 2, - name: 'test2', - description: 'test project2', - status: 'draft', - details: {}, - createdBy: 1, - updatedBy: 1, - }, { - type: 'visual_design', - billingAccountId: 3, - name: 'test2', - description: 'completed project without copilot', - status: 'completed', - details: {}, - createdBy: 1, - updatedBy: 1, - }, { - type: 'generic', - billingAccountId: 4, - name: 'test2', - description: 'draft project without copilot', - status: 'draft', - details: {}, - createdBy: 1, - updatedBy: 1, - }, { - type: 'generic', - billingAccountId: 5, - name: 'test2', - description: 'active project without copilot', - status: 'active', - details: {}, - createdBy: 1, - updatedBy: 1, - }])) - .then(() => models.Project.findAll()) - .then((projects) => { - const project1 = projects[0]; - const project2 = projects[1]; - const operations = []; - operations.push(models.ProjectMember.bulkCreate([{ - userId: 40051331, - projectId: project1.id, - role: 'customer', - isPrimary: false, - createdBy: 1, - updatedBy: 1, - }, { - userId: 40051332, - projectId: project1.id, - role: 'copilot', - isPrimary: false, - createdBy: 1, - updatedBy: 1, - }, { - userId: 40051333, - projectId: project1.id, - role: 'manager', - isPrimary: true, - createdBy: 1, - updatedBy: 1, - }, { - userId: 40051332, - projectId: project2.id, - role: 'copilot', - isPrimary: false, - createdBy: 1, - updatedBy: 1, - }, { - userId: 40051331, - projectId: projects[2].id, - role: 'customer', - isPrimary: false, - createdBy: 1, - updatedBy: 1, - }])); - operations.push(models.ProjectAttachment.create({ - title: 'Spec', - projectId: project1.id, - description: 'specification', - filePath: 'projects/1/spec.pdf', - contentType: 'application/pdf', - createdBy: 1, - updatedBy: 1, - })); - return Promise.all(operations); - }) - .then(() => { - process.exit(0); - }) - .catch(() => process.exit(1)); + .then(() => + models.Project.bulkCreate([{ + type: 'generic', + directProjectId: 9999999, + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'active', + details: {}, + createdBy: 1, + updatedBy: 1, + }, { + type: 'visual_design', + directProjectId: 1, + billingAccountId: 2, + name: 'test2', + description: 'test project2', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + }, { + type: 'visual_design', + billingAccountId: 3, + name: 'test2', + description: 'completed project without copilot', + status: 'completed', + details: {}, + createdBy: 1, + updatedBy: 1, + }, { + type: 'generic', + billingAccountId: 4, + name: 'test2', + description: 'draft project without copilot', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + }, { + type: 'generic', + billingAccountId: 5, + name: 'test2', + description: 'active project without copilot', + status: 'active', + details: {}, + createdBy: 1, + updatedBy: 1, + }])) + .then(() => models.Project.findAll()) + .then((projects) => { + const project1 = projects[0]; + const project2 = projects[1]; + const operations = []; + operations.push(models.ProjectMember.bulkCreate([{ + userId: 40051331, + projectId: project1.id, + role: 'customer', + isPrimary: false, + createdBy: 1, + updatedBy: 1, + }, { + userId: 40051332, + projectId: project1.id, + role: 'copilot', + isPrimary: false, + createdBy: 1, + updatedBy: 1, + }, { + userId: 40051333, + projectId: project1.id, + role: 'manager', + isPrimary: true, + createdBy: 1, + updatedBy: 1, + }, { + userId: 40051332, + projectId: project2.id, + role: 'copilot', + isPrimary: false, + createdBy: 1, + updatedBy: 1, + }, { + userId: 40051331, + projectId: projects[2].id, + role: 'customer', + isPrimary: false, + createdBy: 1, + updatedBy: 1, + }])); + operations.push(models.ProjectAttachment.create({ + title: 'Spec', + projectId: project1.id, + description: 'specification', + filePath: 'projects/1/spec.pdf', + contentType: 'application/pdf', + createdBy: 1, + updatedBy: 1, + })); + return Promise.all(operations); + }) + .then(() => models.ProjectTemplate.bulkCreate([ + { + name: 'template 1', + key: 'key 1', + category: 'category 1', + 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, + }, + { + name: 'template 2', + key: 'key 2', + category: 'category 2', + scope: {}, + phases: {}, + createdBy: 1, + updatedBy: 2, + }, + ])) + .then(() => models.ProductTemplate.bulkCreate([ + { + name: 'name 1', + productKey: 'productKey 1', + icon: 'http://example.com/icon1.ico', + brief: 'brief 1', + details: 'details 1', + aliases: { + alias1: { + subAlias1A: 1, + subAlias1B: 2, + }, + alias2: [1, 2, 3], + }, + template: { + template1: { + name: 'template 1', + details: { + anyDetails: 'any details 1', + }, + others: ['others 11', 'others 12'], + }, + template2: { + name: 'template 2', + details: { + anyDetails: 'any details 2', + }, + others: ['others 21', 'others 22'], + }, + }, + createdBy: 1, + updatedBy: 2, + }, + { + name: 'template 2', + productKey: 'productKey 2', + icon: 'http://example.com/icon2.ico', + brief: 'brief 2', + details: 'details 2', + aliases: {}, + template: {}, + createdBy: 3, + updatedBy: 4, + }, + ])) + .then(() => { + process.exit(0); + }) + .catch(() => process.exit(1)); diff --git a/src/util.js b/src/util.js index 0afbdfb0..5a468d6e 100644 --- a/src/util.js +++ b/src/util.js @@ -227,30 +227,30 @@ _.assignIn(util, { getProjectAttachments: (req, projectId) => { let attachments = []; return models.ProjectAttachment.getActiveProjectAttachments(projectId) - .then((_attachments) => { - // if attachments were requested - if (attachments) { - attachments = _attachments; - } else { - return attachments; - } - // TODO consider using redis to cache attachments urls - const promises = []; - _.each(attachments, (a) => { - promises.push(util.getFileDownloadUrl(req, a.filePath)); - }); - return Promise.all(promises); - }) - .then((result) => { - // result is an array of 'tuples' => [[path, url], [path,url]] - // convert it to a map for easy lookup - const urls = _.fromPairs(result); - _.each(attachments, (at) => { - const a = at; - a.downloadUrl = urls[a.filePath]; - }); + .then((_attachments) => { + // if attachments were requested + if (attachments) { + attachments = _attachments; + } else { return attachments; + } + // TODO consider using redis to cache attachments urls + const promises = []; + _.each(attachments, (a) => { + promises.push(util.getFileDownloadUrl(req, a.filePath)); + }); + return Promise.all(promises); + }) + .then((result) => { + // result is an array of 'tuples' => [[path, url], [path,url]] + // convert it to a map for easy lookup + const urls = _.fromPairs(result); + _.each(attachments, (at) => { + const a = at; + a.downloadUrl = urls[a.filePath]; }); + return attachments; + }); }, getSystemUserToken: (logger, id = 'system') => { @@ -263,19 +263,19 @@ _.assignIn(util, { timeout: 4000, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, }, - ) + ) .then(res => res.data.result.content.token); }, - /** - * Fetches the topcoder user details using the given JWT token. - * - * @param {Number} userId id of the user to be fetched - * @param {String} jwtToken JWT token of the admin user or JWT token of the user to be fecthed - * @param {Object} logger logger to be used for logging purposes - * - * @return {Promise} promise which resolves to the user's information - */ + /** + * Fetches the topcoder user details using the given JWT token. + * + * @param {Number} userId id of the user to be fetched + * @param {String} jwtToken JWT token of the admin user or JWT token of the user to be fecthed + * @param {Object} logger logger to be used for logging purposes + * + * @return {Promise} promise which resolves to the user's information + */ getTopcoderUser: (userId, jwtToken, logger) => { const httpClient = util.getHttpClient({ id: `userService_${userId}`, log: logger }); httpClient.defaults.timeout = 3000; @@ -284,7 +284,7 @@ _.assignIn(util, { httpClient.defaults.headers.common.Authorization = `Bearer ${jwtToken}`; return httpClient.get(`${config.identityServiceEndpoint}users/${userId}`).then((response) => { if (response.data && response.data.result - && response.data.result.status === 200 && response.data.result.content) { + && response.data.result.status === 200 && response.data.result.content) { return response.data.result.content; } return null; @@ -356,6 +356,20 @@ _.assignIn(util, { return Promise.reject(err); } }), + + /** + * Merge two JSON objects. For array fields, the target will be replaced by source. + * @param {Object} targetObj the target object + * @param {Object} sourceObj the source object + * @returns {Object} the merged object + */ + // eslint-disable-next-line consistent-return + mergeJsonObjects: (targetObj, sourceObj) => _.mergeWith(targetObj, sourceObj, (target, source) => { + // Overwrite the array + if (_.isArray(source)) { + return source; + } + }), }); export default util; diff --git a/swagger.yaml b/swagger.yaml index f8178324..6df120e1 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -1,1484 +1,1978 @@ ---- -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 -schemes: -- "http" -produces: -- application/json -consumes: -- application/json - -securityDefinitions: - Bearer: - type: apiKey - name: Authorization - in: header - -paths: - /projects: - get: - tags: - - project - operationId: findProjects - security: - - Bearer: [] - description: Retreive projects that match the filter - responses: - '422': - description: Invalid input - schema: - $ref: "#/definitions/ErrorModel" - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" - '200': - description: A list of projects - schema: - $ref: "#/definitions/ProjectListResponse" - parameters: - - $ref: "#/parameters/offsetParam" - - $ref: "#/parameters/limitParam" - - name: filter - required: true - type: string - in: query - description: | - Url encoded list of Supported filters - - id - - status - - type - - memberOnly - - keyword - - name: sort - required: false - description: | - sort projects by status, name, type, createdAt, updatedAt. Default is createdAt asc - in: query - type: string - post: - operationId: addProject - security: - - Bearer: [] - description: Create a project - parameters: - - in: body - name: body - required: true - 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" - '422': - description: Invalid input - schema: - $ref: "#/definitions/ErrorModel" - - /projects/{projectId}: - get: - description: Retrieve project by id - security: - - Bearer: [] - responses: - '404': - description: Not found - schema: - $ref: "#/definitions/ErrorModel" - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" - '200': - description: a project - schema: - $ref: "#/definitions/ProjectResponse" - parameters: - - $ref: "#/parameters/projectIdParam" - - name: fields - required: false - type: string - in: query - 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 - - 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. - 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. Returns original and updated project object - schema: - $ref: "#/definitions/UpdateProjectResponse" - '422': - description: Invalid input - schema: - $ref: "#/definitions/ErrorModel" - default: - description: error payload - schema: - $ref: '#/definitions/ErrorModel' - parameters: - - $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 - schema: - $ref: "#/definitions/ProjectBodyParam" - - delete: - description: remove an existing project - security: - - Bearer: [] - parameters: - - $ref: "#/parameters/projectIdParam" - responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" - '404': - description: If project is not found - schema: - $ref: "#/definitions/ErrorModel" - '204': - description: Project successfully removed - - /projects/{projectId}/attachments: - post: - description: add a new project attachment - security: - - Bearer: [] - parameters: - - $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" - '422': - description: Invalid input - schema: - $ref: "#/definitions/ErrorModel" - - /projects/{projectId}/attachments/{id}: - patch: - description: Update an existing attachment - security: - - Bearer: [] - parameters: - - $ref: "#/parameters/projectIdParam" - - in: path - name: id - required: true - description: The id of attachment to update - type: integer - - in: body - name: body - required: true - description: Specify only those properties that needs to be updated - 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" - '404': - description: If project attachment is not found - schema: - $ref: "#/definitions/ErrorModel" - '422': - description: Invalid input - schema: - $ref: "#/definitions/ErrorModel" - delete: - description: remove an existing attachment - security: - - Bearer: [] - parameters: - - $ref: "#/parameters/projectIdParam" - - in: path - name: id - required: true - description: The id of attachment to delete - type: integer - responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" - '404': - description: If attachment is not found - schema: - $ref: "#/definitions/ErrorModel" - '204': - description: Attachment successfully removed - - /projects/{projectId}/members: - post: - description: add a new project member - security: - - Bearer: [] - parameters: - - $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" - '422': - description: Invalid input - schema: - $ref: "#/definitions/ErrorModel" - - /projects/{projectId}/members/{id}: - delete: - description: Delete a project member - security: - - Bearer: [] - parameters: - - $ref: "#/parameters/projectIdParam" - - in: path - name: id - required: true - type: integer - - responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" - '204': - description: Member successfully removed - 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: - parameters: - - $ref: "#/parameters/projectIdParam" - get: - tags: - - phase - operationId: findProjectPhases - security: - - Bearer: [] - description: Retreive all project phases. All users who can edit project can access this endpoint. - parameters: - - name: fields - required: false - type: string - in: query - description: | - Comma separated list of project phase fields to return. - - name: sort - required: false - description: | - sort project phases by startDate, endDate, status. 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" - post: - tags: - - phase - operationId: addProjectPhase - security: - - Bearer: [] - description: Create a project phase - parameters: - - in: body - name: body - required: true - schema: - $ref: '#/definitions/ProjectPhaseBodyParam' - responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" - '201': - description: Returns the newly created project phase - schema: - $ref: "#/definitions/ProjectPhaseResponse" - '422': - description: Invalid input - schema: - $ref: "#/definitions/ErrorModel" - - /projects/{projectId}/phases/{phaseId}: - parameters: - - $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. - security: - - Bearer: [] - responses: - '404': - description: Not found - schema: - $ref: "#/definitions/ErrorModel" - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" - '200': - description: a project phase - schema: - $ref: "#/definitions/ProjectPhaseResponse" - parameters: - - $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. - 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 phase. - schema: - $ref: "#/definitions/ProjectPhaseResponse" - '422': - description: Invalid input - schema: - $ref: "#/definitions/ErrorModel" - default: - description: error payload - schema: - $ref: '#/definitions/ErrorModel' - parameters: - - $ref: "#/parameters/phaseIdParam" - - name: body - in: body - required: true - schema: - $ref: "#/definitions/ProjectPhaseBodyParam" - - delete: - tags: - - phase - description: Remove an existing project phase. All users who can edit project can access this endpoint. - security: - - Bearer: [] - parameters: - - $ref: "#/parameters/phaseIdParam" - responses: - '403': - description: No permission or wrong token - schema: - $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: - parameters: - - $ref: "#/parameters/projectIdParam" - - $ref: "#/parameters/phaseIdParam" - get: - tags: - - phase product - operationId: findPhaseProducts - security: - - Bearer: [] - description: Retreive 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" - post: - tags: - - phase product - operationId: addPhaseProduct - security: - - Bearer: [] - description: Create a phase product - parameters: - - in: body - name: body - required: true - 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" - '422': - description: Invalid input - schema: - $ref: "#/definitions/ErrorModel" - - /projects/{projectId}/phases/{phaseId}/products/{productId}: - parameters: - - $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. - security: - - Bearer: [] - responses: - '404': - description: Not found - schema: - $ref: "#/definitions/ErrorModel" - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" - '200': - description: a phase product - schema: - $ref: "#/definitions/PhaseProductResponse" - parameters: - - $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. - responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" - '404': - description: Not found - schema: - $ref: "#/definitions/ErrorModel" - '200': - description: Successfully updated phase product. - schema: - $ref: "#/definitions/PhaseProductResponse" - '422': - description: Invalid input - schema: - $ref: "#/definitions/ErrorModel" - default: - description: error payload - schema: - $ref: '#/definitions/ErrorModel' - parameters: - - $ref: "#/parameters/phaseIdParam" - - name: body - in: body - required: true - schema: - $ref: "#/definitions/PhaseProductBodyParam" - - delete: - tags: - - phase product - description: Remove an existing phase product. All users who can edit project can access this endpoint. - security: - - Bearer: [] - parameters: - - $ref: "#/parameters/phaseIdParam" - responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" - '404': - description: If project is not found - schema: - $ref: "#/definitions/ErrorModel" - '204': - description: Project phase successfully removed - - -parameters: - projectIdParam: - name: projectId - in: path - description: project identifier - required: true - type: integer - format: int64 - phaseIdParam: - name: phaseId - in: path - description: project phase identifier - required: true - type: integer - format: int64 - minimum: 1 - productIdParam: - name: productId - in: path - description: project phase product identifier - required: true - type: integer - format: int64 - minimum: 1 - offsetParam: - name: offset - 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" - in: query - required: false - type: integer - format: int32 - -definitions: - ResponseMetadata: - title: Metadata object for a response - type: object - properties: - totalCount: - type: integer - format: int64 - description: Total count of the objects - - ErrorModel: - type: object - properties: - id: - type: string - description: unique id identifying the request - version: - type: string - result: - type: object - properties: - success: - type: boolean - status: - description: http status code - type: integer - format: int32 - debug: - type: object - content: - type: object - - ProjectBookMark: - title: Project bookmark - type: object - properties: - title: - type: string - address: - type: string - - ProjectBodyParam: - type: object - properties: - param: - $ref: "#/definitions/Project" - - NewProject: - type: object - required: - - name - - description - - type - properties: - name: - type: string - description: project name (required) - description: - type: string - description: Project description - billingAccountId: - type: number - format: long - description: the customer billing account id - estimatedPrice: - type: number - format: float - description: The estimated price of the project - terms: - type: array - items: - type: number - format: integer - external: - type: object - description: READ-ONLY, OPTIONAL. Refernce to external task/issue. - properties: - id: - type: string - description: Identifier for external reference - type: - type: string - description: external source type - enum: [ "github", "jira", "asana", "other"] - data: - type: string - description: "300 Char length text blob for customer provided data" - type: - type: string - description: project type - enum: ["generic", "visual_design", "visual_prototype", "app_dev"] - bookmarks: - type: array - items: - $ref: "#/definitions/ProjectBookMark" - challengeEligibility: - description: List of eligibility criteria (one entry per role) - type: array - items: - $ref: "#/definitions/ChallengeEligibility" - details: - $ref: "#/definitions/ProjectDetails" - utm: - description: READ-ONLY. Used for tracking - type: object - properties: - campaign: - type: string - medium: - type: string - source: - type: string - - - NewProjectBodyParam: - type: object - properties: - param: - $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 - - - Project: - type: object - properties: - id: - description: unique identifier - type: integer - format: int64 - directProjectId: - description: unique identifier in direct - type: integer - format: int64 - billingAccountId: - type: integer - format: int64 - description: The customer billing account id - utm: - description: READ-ONLY. Used for tracking - type: object - properties: - campaign: - type: string - medium: - type: string - source: - type: string - estimatedPrice: - type: number - format: float - description: The estimated price of the project - actualPrice: - type: number - format: float - description: The actual price of the project - terms: - type: array - items: - type: number - format: integer - name: - type: string - description: project name - description: - type: string - description: Project description - - external: - type: object - description: READ-ONLY, OPTIONAL. Refernce to external task/issue. - properties: - id: - type: string - description: Identifier for external reference - type: - type: string - description: external source type - enum: [ "github", "jira", "asana", "other"] - data: - type: string - description: "300 Char length text blob for customer provided data" - type: - type: string - description: project type - enum: ["app_dev", "generic", "visual_prototype", "visual_design"] - status: - type: string - description: current state of the task - enum: ["draft", "in_review", "reviewed", "active", "paused", "cancelled", "completed"] - cancelReason: - type: string - 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" - bookmarks: - type: array - items: - $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" - attachments: - description: | - READ-ONLY. List of project attachmens. - Use project attachment api to add/remove attachments - type: array - items: - $ref: "#/definitions/ProjectAttachment" - details: - $ref: "#/definitions/ProjectDetails" - - 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 - - ProjectDetails: - description: Project details - type: object - properties: - summary: - type: string - description: text summary of the project - TBD_usageDescription: - type: string - description: a description of how the app will be used - TBD_features: - type: object - properties: - id: - type: integer - title: - type: string - description: - type: string - isCustom: - type: boolean - - - NewProjectMember: - title: Project Member object - type: object - required: - - userId - - role - properties: - userId: - type: number - format: int64 - description: user identifier - isPrimary: - type: boolean - description: Flag to indicate this member is primary for specified role - role: - type: string - description: member role on specified project - enum: ["customer", "manager", "copilot"] - - NewProjectMemberBodyParam: - type: object - properties: - param: - $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"] - - UpdateProjectMemberBodyParam: - type: object - properties: - param: - $ref: "#/definitions/UpdateProjectMember" - - NewProjectAttachment: - title: Project attachment request - type: object - required: - - filePath - - s3Bucket - - title - - contentType - properties: - filePath: - type: string - description: path where file is stored - s3Bucket: - type: string - description: The s3 bucket of attachment - contentType: - type: string - description: Uploaded file content type - title: - type: string - description: Name of the attachment - description: - type: string - description: Optional description for the attached file. - category: - type: string - description: Category of attachment - size: - type: number - format: float - description: The size of attachment - - NewProjectAttachmentBodyParam: - type: object - properties: - param: - $ref: "#/definitions/NewProjectAttachment" - - NewProjectAttachmentResponse: - title: Project attachment object 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/ProjectAttachment" - - ProjectAttachment: - title: Project attachment - type: object - properties: - id: - type: number - description: unique id for the attachment - size: - type: number - format: float - description: The size of attachment - category: - type: string - description: The category of attachment - contentType: - type: string - description: Uploaded file content type - title: - type: string - description: Name of the attachment - description: - type: string - description: Optional description for the attached file. - downloadUrl: - type: string - description: download link for the attachment. - 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 - - ProjectMember: - title: Project Member object - type: object - properties: - id: - type: number - description: unique identifier for record - userId: - type: number - format: int64 - description: user identifier - isPrimary: - type: boolean - description: Flag to indicate this member is primary for specified role - projectId: - type: number - format: int64 - description: project identifier - role: - type: string - description: member role on specified project - enum: ["customer", "manager", "copilot"] - 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 - - - - NewProjectMemberResponse: - title: Project member object 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/ProjectMember" - - UpdateProjectMemberResponse: - title: Project member object 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/ProjectMember" - - - ProjectResponse: - title: Single project object - 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/Project" - - UpdateProjectResponse: - title: response with original and updated project object - 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: object - properties: - original: - $ref: "#/definitions/Project" - updated: - $ref: "#/definitions/Project" - - ProjectListResponse: - title: List response - type: object - properties: - id: - type: string - readOnly: true - description: unique id identifying the request - version: - type: string - result: - type: object - properties: - success: - type: boolean - status: - type: string - description: http status code - metadata: - $ref: "#/definitions/ResponseMetadata" - content: - type: array - items: - $ref: "#/definitions/Project" - - - - - ProjectPhaseRequest: - title: Project phase request object - type: object - required: - - name - - status - - startDate - - endDate - properties: - name: - type: string - description: the project phase name - status: - type: string - description: the project phase status - startDate: - type: string - format: date - description: the project phase start date - endDate: - type: string - format: date - description: the project phase end date - budget: - type: number - description: the project phase budget - progress: - type: number - description: the project phase progress - details: - type: object - description: the project phase details - - ProjectPhaseBodyParam: - title: Project phase body param - type: object - required: - - param - properties: - param: - $ref: "#/definitions/ProjectPhaseRequest" - - ProjectPhase: - title: Project phase object - allOf: - - type: object - required: - - id - - createdAt - - createdBy - - updatedAt - - updatedBy - properties: - id: - type: number - format: int64 - description: the id - createdAt: - type: string - description: Datetime (GMT) when object was created - readOnly: true - createdBy: - type: integer - format: int64 - description: READ-ONLY. User who created this object - readOnly: true - updatedAt: - type: string - description: READ-ONLY. Datetime (GMT) when object was updated - readOnly: true - updatedBy: - type: integer - format: int64 - description: READ-ONLY. User that last updated this object - readOnly: true - - $ref: "#/definitions/ProjectPhaseRequest" - - - ProjectPhaseResponse: - title: Single project phase response object - 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 - metadata: - $ref: "#/definitions/ResponseMetadata" - content: - $ref: "#/definitions/ProjectPhase" - - ProjectPhaseListResponse: - title: Project phase list response object - type: object - properties: - id: - type: string - readOnly: true - description: unique id identifying the request - version: - type: string - result: - type: object - properties: - success: - type: boolean - status: - type: string - description: http status code - metadata: - $ref: "#/definitions/ResponseMetadata" - content: - type: array - items: - $ref: "#/definitions/ProjectPhase" - - - PhaseProductRequest: - title: Phase product request object - type: object - properties: - name: - type: string - description: the phase product name - directProjectId: - type: number - description: the phase product direct project id - billingAccountId: - type: number - description: the phase product billing account Id - templateId: - type: number - description: the phase product template id - type: - type: string - description: the phase product type - estimatedPrice: - type: number - description: the phase product estimated price - actualPrice: - type: number - description: the phase product actual price - details: - type: object - description: the phase product details - - PhaseProductBodyParam: - title: Phase product body param - type: object - required: - - param - properties: - param: - $ref: "#/definitions/PhaseProductRequest" - - PhaseProduct: - title: Phase product object - allOf: - - type: object - required: - - id - - createdAt - - createdBy - - updatedAt - - updatedBy - properties: - id: - type: number - format: int64 - description: the id - createdAt: - type: string - description: Datetime (GMT) when object was created - readOnly: true - createdBy: - type: integer - format: int64 - description: READ-ONLY. User who created this object - readOnly: true - updatedAt: - type: string - description: READ-ONLY. Datetime (GMT) when object was updated - readOnly: true - updatedBy: - type: integer - format: int64 - description: READ-ONLY. User that last updated this object - readOnly: true - - $ref: "#/definitions/PhaseProductRequest" - - - PhaseProductResponse: - title: Single phase product response object - 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 - metadata: - $ref: "#/definitions/ResponseMetadata" - content: - $ref: "#/definitions/PhaseProduct" - - PhaseProductListResponse: - title: Phase product list response object - type: object - properties: - id: - type: string - readOnly: true - description: unique id identifying the request - version: - type: string - result: - type: object - properties: - success: - type: boolean - status: - type: string - description: http status code - metadata: - $ref: "#/definitions/ResponseMetadata" - content: - type: array - items: - $ref: "#/definitions/PhaseProduct" - \ No newline at end of file +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 +schemes: +- "http" +produces: +- application/json +consumes: +- application/json + +securityDefinitions: + Bearer: + type: apiKey + name: Authorization + in: header + +paths: + /projects: + get: + tags: + - project + operationId: findProjects + security: + - Bearer: [] + description: Retreive projects that match the filter + responses: + '422': + description: Invalid input + schema: + $ref: "#/definitions/ErrorModel" + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '200': + description: A list of projects + schema: + $ref: "#/definitions/ProjectListResponse" + parameters: + - $ref: "#/parameters/offsetParam" + - $ref: "#/parameters/limitParam" + - name: filter + required: true + type: string + in: query + description: | + Url encoded list of Supported filters + - id + - status + - type + - memberOnly + - keyword + - name: sort + required: false + description: | + sort projects by status, name, type, createdAt, updatedAt. Default is createdAt asc + in: query + type: string + post: + operationId: addProject + security: + - Bearer: [] + description: Create a project + parameters: + - in: body + name: body + required: true + 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" + '422': + description: Invalid input + schema: + $ref: "#/definitions/ErrorModel" + + /projects/{projectId}: + get: + description: Retrieve project by id + security: + - Bearer: [] + responses: + '404': + description: Not found + schema: + $ref: "#/definitions/ErrorModel" + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '200': + description: a project + schema: + $ref: "#/definitions/ProjectResponse" + parameters: + - $ref: "#/parameters/projectIdParam" + - name: fields + required: false + type: string + in: query + 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 + + 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. + 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. Returns original and updated project object + schema: + $ref: "#/definitions/UpdateProjectResponse" + '422': + description: Invalid input + schema: + $ref: "#/definitions/ErrorModel" + default: + description: error payload + schema: + $ref: '#/definitions/ErrorModel' + parameters: + - $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 + schema: + $ref: "#/definitions/ProjectBodyParam" + + delete: + description: remove an existing project + security: + - Bearer: [] + parameters: + - $ref: "#/parameters/projectIdParam" + responses: + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '404': + description: If project is not found + schema: + $ref: "#/definitions/ErrorModel" + '204': + description: Project successfully removed + + /projects/{projectId}/attachments: + post: + description: add a new project attachment + security: + - Bearer: [] + parameters: + - $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" + '422': + description: Invalid input + schema: + $ref: "#/definitions/ErrorModel" + + /projects/{projectId}/attachments/{id}: + patch: + description: Update an existing attachment + security: + - Bearer: [] + parameters: + - $ref: "#/parameters/projectIdParam" + - in: path + name: id + required: true + description: The id of attachment to update + type: integer + - in: body + name: body + required: true + description: Specify only those properties that needs to be updated + 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" + '404': + description: If project attachment is not found + schema: + $ref: "#/definitions/ErrorModel" + '422': + description: Invalid input + schema: + $ref: "#/definitions/ErrorModel" + delete: + description: remove an existing attachment + security: + - Bearer: [] + parameters: + - $ref: "#/parameters/projectIdParam" + - in: path + name: id + required: true + description: The id of attachment to delete + type: integer + responses: + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '404': + description: If attachment is not found + schema: + $ref: "#/definitions/ErrorModel" + '204': + description: Attachment successfully removed + + /projects/{projectId}/members: + post: + description: add a new project member + security: + - Bearer: [] + parameters: + - $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" + '422': + description: Invalid input + schema: + $ref: "#/definitions/ErrorModel" + + /projects/{projectId}/members/{id}: + delete: + description: Delete a project member + security: + - Bearer: [] + parameters: + - $ref: "#/parameters/projectIdParam" + - in: path + name: id + required: true + type: integer + + responses: + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '204': + description: Member successfully removed + 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: + parameters: + - $ref: "#/parameters/projectIdParam" + get: + tags: + - phase + operationId: findProjectPhases + security: + - Bearer: [] + description: Retreive all project phases. All users who can edit project can access this endpoint. + parameters: + - name: fields + required: false + type: string + in: query + description: | + Comma separated list of project phase fields to return. + - name: sort + required: false + description: | + sort project phases by startDate, endDate, status. 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" + post: + tags: + - phase + operationId: addProjectPhase + security: + - Bearer: [] + description: Create a project phase + parameters: + - in: body + name: body + required: true + schema: + $ref: '#/definitions/ProjectPhaseBodyParam' + responses: + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '201': + description: Returns the newly created project phase + schema: + $ref: "#/definitions/ProjectPhaseResponse" + '422': + description: Invalid input + schema: + $ref: "#/definitions/ErrorModel" + + /projects/{projectId}/phases/{phaseId}: + parameters: + - $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. + security: + - Bearer: [] + responses: + '404': + description: Not found + schema: + $ref: "#/definitions/ErrorModel" + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '200': + description: a project phase + schema: + $ref: "#/definitions/ProjectPhaseResponse" + parameters: + - $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. + 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 phase. + schema: + $ref: "#/definitions/ProjectPhaseResponse" + '422': + description: Invalid input + schema: + $ref: "#/definitions/ErrorModel" + default: + description: error payload + schema: + $ref: '#/definitions/ErrorModel' + parameters: + - $ref: "#/parameters/phaseIdParam" + - name: body + in: body + required: true + schema: + $ref: "#/definitions/ProjectPhaseBodyParam" + + delete: + tags: + - phase + description: Remove an existing project phase. All users who can edit project can access this endpoint. + security: + - Bearer: [] + parameters: + - $ref: "#/parameters/phaseIdParam" + responses: + '403': + description: No permission or wrong token + schema: + $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: + parameters: + - $ref: "#/parameters/projectIdParam" + - $ref: "#/parameters/phaseIdParam" + get: + tags: + - phase product + operationId: findPhaseProducts + security: + - Bearer: [] + description: Retreive 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" + post: + tags: + - phase product + operationId: addPhaseProduct + security: + - Bearer: [] + description: Create a phase product + parameters: + - in: body + name: body + required: true + 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" + '422': + description: Invalid input + schema: + $ref: "#/definitions/ErrorModel" + + /projects/{projectId}/phases/{phaseId}/products/{productId}: + parameters: + - $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. + security: + - Bearer: [] + responses: + '404': + description: Not found + schema: + $ref: "#/definitions/ErrorModel" + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '200': + description: a phase product + schema: + $ref: "#/definitions/PhaseProductResponse" + parameters: + - $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. + responses: + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '404': + description: Not found + schema: + $ref: "#/definitions/ErrorModel" + '200': + description: Successfully updated phase product. + schema: + $ref: "#/definitions/PhaseProductResponse" + '422': + description: Invalid input + schema: + $ref: "#/definitions/ErrorModel" + default: + description: error payload + schema: + $ref: '#/definitions/ErrorModel' + parameters: + - $ref: "#/parameters/phaseIdParam" + - name: body + in: body + required: true + schema: + $ref: "#/definitions/PhaseProductBodyParam" + + delete: + tags: + - phase product + description: Remove an existing phase product. All users who can edit project can access this endpoint. + security: + - Bearer: [] + parameters: + - $ref: "#/parameters/phaseIdParam" + responses: + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '404': + description: If project is not found + schema: + $ref: "#/definitions/ErrorModel" + '204': + description: Project phase successfully removed + + /projectTemplates: + get: + tags: + - projectTemplate + operationId: findProjectTemplates + security: + - Bearer: [] + description: Retreive 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" + post: + tags: + - projectTemplate + operationId: addProjectTemplate + security: + - Bearer: [] + description: Create a project template + parameters: + - in: body + name: body + required: true + 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" + '422': + description: Invalid input + schema: + $ref: "#/definitions/ErrorModel" + + /projectTemplates/{templateId}: + get: + tags: + - projectTemplate + description: Retrieve project template by id. All user roles can access this endpoint. + security: + - Bearer: [] + responses: + '404': + description: Not found + schema: + $ref: "#/definitions/ErrorModel" + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '200': + description: a project template + schema: + $ref: "#/definitions/ProjectTemplateResponse" + parameters: + - $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. + 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 template. + schema: + $ref: "#/definitions/ProjectTemplateResponse" + '422': + description: Invalid input + schema: + $ref: "#/definitions/ErrorModel" + default: + description: error payload + schema: + $ref: '#/definitions/ErrorModel' + parameters: + - $ref: "#/parameters/templateIdParam" + - name: body + in: body + required: true + schema: + $ref: "#/definitions/ProjectTemplateBodyParam" + + delete: + tags: + - projectTemplate + description: Remove an existing project template. Only connect manager, connect admin, and admin can access this endpoint. + security: + - Bearer: [] + parameters: + - $ref: "#/parameters/templateIdParam" + responses: + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '404': + description: If project is not found + schema: + $ref: "#/definitions/ErrorModel" + '204': + description: Project template successfully removed + + + /productTemplates: + get: + tags: + - productTemplate + operationId: findProductTemplates + security: + - Bearer: [] + description: Retreive 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" + post: + tags: + - productTemplate + operationId: addProductTemplate + security: + - Bearer: [] + description: Create a product template + parameters: + - in: body + name: body + required: true + 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" + '422': + description: Invalid input + schema: + $ref: "#/definitions/ErrorModel" + + /productTemplates/{templateId}: + get: + tags: + - productTemplate + description: Retrieve product template by id. All user roles can access this endpoint. + security: + - Bearer: [] + responses: + '404': + description: Not found + schema: + $ref: "#/definitions/ErrorModel" + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '200': + description: a product template + schema: + $ref: "#/definitions/ProductTemplateResponse" + parameters: + - $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. + responses: + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '404': + description: Not found + schema: + $ref: "#/definitions/ErrorModel" + '200': + description: Successfully updated product template. + schema: + $ref: "#/definitions/ProductTemplateResponse" + '422': + description: Invalid input + schema: + $ref: "#/definitions/ErrorModel" + default: + description: error payload + schema: + $ref: '#/definitions/ErrorModel' + parameters: + - $ref: "#/parameters/templateIdParam" + - name: body + in: body + required: true + schema: + $ref: "#/definitions/ProductTemplateBodyParam" + + delete: + tags: + - productTemplate + description: Remove an existing product template. Only connect manager, connect admin, and admin can access this endpoint. + security: + - Bearer: [] + parameters: + - $ref: "#/parameters/templateIdParam" + responses: + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '404': + description: If product is not found + schema: + $ref: "#/definitions/ErrorModel" + '204': + description: Product template successfully removed + + +parameters: + projectIdParam: + name: projectId + in: path + description: project identifier + required: true + type: integer + format: int64 + phaseIdParam: + name: phaseId + in: path + description: project phase identifier + required: true + type: integer + format: int64 + minimum: 1 + productIdParam: + name: productId + in: path + description: project phase product identifier + required: true + type: integer + format: int64 + minimum: 1 + templateIdParam: + name: templateId + in: path + description: template identifier + required: true + type: integer + format: int64 + minimum: 1 + offsetParam: + name: offset + 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" + in: query + required: false + type: integer + format: int32 + +definitions: + ResponseMetadata: + title: Metadata object for a response + type: object + properties: + totalCount: + type: integer + format: int64 + description: Total count of the objects + + ErrorModel: + type: object + properties: + id: + type: string + description: unique id identifying the request + version: + type: string + result: + type: object + properties: + success: + type: boolean + status: + description: http status code + type: integer + format: int32 + debug: + type: object + content: + type: object + + ProjectBookMark: + title: Project bookmark + type: object + properties: + title: + type: string + address: + type: string + + ProjectBodyParam: + type: object + properties: + param: + $ref: "#/definitions/Project" + + NewProject: + type: object + required: + - name + - description + - type + properties: + name: + type: string + description: project name (required) + description: + type: string + description: Project description + billingAccountId: + type: number + format: long + description: the customer billing account id + estimatedPrice: + type: number + format: float + description: The estimated price of the project + terms: + type: array + items: + type: number + format: integer + external: + type: object + description: READ-ONLY, OPTIONAL. Refernce to external task/issue. + properties: + id: + type: string + description: Identifier for external reference + type: + type: string + description: external source type + enum: [ "github", "jira", "asana", "other"] + data: + type: string + description: "300 Char length text blob for customer provided data" + type: + type: string + description: project type + enum: ["generic", "visual_design", "visual_prototype", "app_dev"] + bookmarks: + type: array + items: + $ref: "#/definitions/ProjectBookMark" + challengeEligibility: + description: List of eligibility criteria (one entry per role) + type: array + items: + $ref: "#/definitions/ChallengeEligibility" + details: + $ref: "#/definitions/ProjectDetails" + utm: + description: READ-ONLY. Used for tracking + type: object + properties: + campaign: + type: string + medium: + type: string + source: + type: string + + + NewProjectBodyParam: + type: object + properties: + param: + $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 + + + Project: + type: object + properties: + id: + description: unique identifier + type: integer + format: int64 + directProjectId: + description: unique identifier in direct + type: integer + format: int64 + billingAccountId: + type: integer + format: int64 + description: The customer billing account id + utm: + description: READ-ONLY. Used for tracking + type: object + properties: + campaign: + type: string + medium: + type: string + source: + type: string + estimatedPrice: + type: number + format: float + description: The estimated price of the project + actualPrice: + type: number + format: float + description: The actual price of the project + terms: + type: array + items: + type: number + format: integer + name: + type: string + description: project name + description: + type: string + description: Project description + + external: + type: object + description: READ-ONLY, OPTIONAL. Refernce to external task/issue. + properties: + id: + type: string + description: Identifier for external reference + type: + type: string + description: external source type + enum: [ "github", "jira", "asana", "other"] + data: + type: string + description: "300 Char length text blob for customer provided data" + type: + type: string + description: project type + enum: ["app_dev", "generic", "visual_prototype", "visual_design"] + status: + type: string + description: current state of the task + enum: ["draft", "in_review", "reviewed", "active", "paused", "cancelled", "completed"] + cancelReason: + type: string + 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" + bookmarks: + type: array + items: + $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" + attachments: + description: | + READ-ONLY. List of project attachmens. + Use project attachment api to add/remove attachments + type: array + items: + $ref: "#/definitions/ProjectAttachment" + details: + $ref: "#/definitions/ProjectDetails" + + 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 + + ProjectDetails: + description: Project details + type: object + properties: + summary: + type: string + description: text summary of the project + TBD_usageDescription: + type: string + description: a description of how the app will be used + TBD_features: + type: object + properties: + id: + type: integer + title: + type: string + description: + type: string + isCustom: + type: boolean + + + NewProjectMember: + title: Project Member object + type: object + required: + - userId + - role + properties: + userId: + type: number + format: int64 + description: user identifier + isPrimary: + type: boolean + description: Flag to indicate this member is primary for specified role + role: + type: string + description: member role on specified project + enum: ["customer", "manager", "copilot"] + + NewProjectMemberBodyParam: + type: object + properties: + param: + $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"] + + UpdateProjectMemberBodyParam: + type: object + properties: + param: + $ref: "#/definitions/UpdateProjectMember" + + NewProjectAttachment: + title: Project attachment request + type: object + required: + - filePath + - s3Bucket + - title + - contentType + properties: + filePath: + type: string + description: path where file is stored + s3Bucket: + type: string + description: The s3 bucket of attachment + contentType: + type: string + description: Uploaded file content type + title: + type: string + description: Name of the attachment + description: + type: string + description: Optional description for the attached file. + category: + type: string + description: Category of attachment + size: + type: number + format: float + description: The size of attachment + + NewProjectAttachmentBodyParam: + type: object + properties: + param: + $ref: "#/definitions/NewProjectAttachment" + + NewProjectAttachmentResponse: + title: Project attachment object 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/ProjectAttachment" + + ProjectAttachment: + title: Project attachment + type: object + properties: + id: + type: number + description: unique id for the attachment + size: + type: number + format: float + description: The size of attachment + category: + type: string + description: The category of attachment + contentType: + type: string + description: Uploaded file content type + title: + type: string + description: Name of the attachment + description: + type: string + description: Optional description for the attached file. + downloadUrl: + type: string + description: download link for the attachment. + 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 + + ProjectMember: + title: Project Member object + type: object + properties: + id: + type: number + description: unique identifier for record + userId: + type: number + format: int64 + description: user identifier + isPrimary: + type: boolean + description: Flag to indicate this member is primary for specified role + projectId: + type: number + format: int64 + description: project identifier + role: + type: string + description: member role on specified project + enum: ["customer", "manager", "copilot"] + 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 + + + + NewProjectMemberResponse: + title: Project member object 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/ProjectMember" + + UpdateProjectMemberResponse: + title: Project member object 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/ProjectMember" + + + ProjectResponse: + title: Single project object + 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/Project" + + UpdateProjectResponse: + title: response with original and updated project object + 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: object + properties: + original: + $ref: "#/definitions/Project" + updated: + $ref: "#/definitions/Project" + + ProjectListResponse: + title: List response + type: object + properties: + id: + type: string + readOnly: true + description: unique id identifying the request + version: + type: string + result: + type: object + properties: + success: + type: boolean + status: + type: string + description: http status code + metadata: + $ref: "#/definitions/ResponseMetadata" + content: + type: array + items: + $ref: "#/definitions/Project" + + ProjectTemplateRequest: + title: Project template request object + type: object + required: + - name + - key + - category + - scope + - phases + properties: + name: + type: string + description: the project template name + key: + type: string + description: the project template key + category: + type: string + description: the project template category + scope: + type: object + description: the project template scope + phases: + type: object + description: the project template phases + + ProjectTemplateBodyParam: + title: Project template body param + type: object + required: + - param + properties: + param: + $ref: "#/definitions/ProjectTemplateRequest" + + ProjectTemplate: + title: Project template object + allOf: + - type: object + required: + - id + - createdAt + - createdBy + - updatedAt + - updatedBy + properties: + id: + type: number + format: int64 + description: the id + createdAt: + type: string + description: Datetime (GMT) when object was created + readOnly: true + createdBy: + type: integer + format: int64 + description: READ-ONLY. User who created this object + readOnly: true + updatedAt: + type: string + description: READ-ONLY. Datetime (GMT) when object was updated + readOnly: true + updatedBy: + type: integer + format: int64 + description: READ-ONLY. User that last updated this object + readOnly: true + - $ref: "#/definitions/ProjectTemplateRequest" + + + ProjectTemplateResponse: + title: Single project template response object + 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 + metadata: + $ref: "#/definitions/ResponseMetadata" + content: + $ref: "#/definitions/ProjectTemplate" + + ProjectTemplateListResponse: + title: Project template list response object + type: object + properties: + id: + type: string + readOnly: true + description: unique id identifying the request + version: + type: string + result: + type: object + properties: + success: + type: boolean + status: + type: string + description: http status code + metadata: + $ref: "#/definitions/ResponseMetadata" + content: + type: array + items: + $ref: "#/definitions/ProjectTemplate" + + ProductTemplateRequest: + title: Product template request object + type: object + required: + - name + - key + - category + - scope + - phases + properties: + name: + type: string + description: the product template name + productKey: + type: string + description: the product template key + icon: + type: string + description: the product template icon + brief: + type: string + description: the product template brief + details: + type: string + description: the product template details + aliases: + type: object + description: the product template aliases + template: + type: object + description: the product template template + + ProductTemplateBodyParam: + title: Product template body param + type: object + required: + - param + properties: + param: + $ref: "#/definitions/ProductTemplateRequest" + + ProductTemplate: + title: Product template object + allOf: + - type: object + required: + - id + - createdAt + - createdBy + - updatedAt + - updatedBy + properties: + id: + type: number + format: int64 + description: the id + createdAt: + type: string + description: Datetime (GMT) when object was created + readOnly: true + createdBy: + type: integer + format: int64 + description: READ-ONLY. User who created this object + readOnly: true + updatedAt: + type: string + description: READ-ONLY. Datetime (GMT) when object was updated + readOnly: true + updatedBy: + type: integer + format: int64 + description: READ-ONLY. User that last updated this object + readOnly: true + - $ref: "#/definitions/ProductTemplateRequest" + + + ProductTemplateResponse: + title: Single product template response object + 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 + metadata: + $ref: "#/definitions/ResponseMetadata" + content: + $ref: "#/definitions/ProductTemplate" + + ProductTemplateListResponse: + title: Product template list response object + type: object + properties: + id: + type: string + readOnly: true + description: unique id identifying the request + version: + type: string + result: + type: object + properties: + success: + type: boolean + status: + type: string + description: http status code + metadata: + $ref: "#/definitions/ResponseMetadata" + content: + type: array + items: + $ref: "#/definitions/ProductTemplate" + + ProjectPhaseRequest: + title: Project phase request object + type: object + required: + - name + - status + - startDate + - endDate + properties: + name: + type: string + description: the project phase name + status: + type: string + description: the project phase status + startDate: + type: string + format: date + description: the project phase start date + endDate: + type: string + format: date + description: the project phase end date + budget: + type: number + description: the project phase budget + progress: + type: number + description: the project phase progress + details: + type: object + description: the project phase details + + ProjectPhaseBodyParam: + title: Project phase body param + type: object + required: + - param + properties: + param: + $ref: "#/definitions/ProjectPhaseRequest" + + ProjectPhase: + title: Project phase object + allOf: + - type: object + required: + - id + - createdAt + - createdBy + - updatedAt + - updatedBy + properties: + id: + type: number + format: int64 + description: the id + createdAt: + type: string + description: Datetime (GMT) when object was created + readOnly: true + createdBy: + type: integer + format: int64 + description: READ-ONLY. User who created this object + readOnly: true + updatedAt: + type: string + description: READ-ONLY. Datetime (GMT) when object was updated + readOnly: true + updatedBy: + type: integer + format: int64 + description: READ-ONLY. User that last updated this object + readOnly: true + - $ref: "#/definitions/ProjectPhaseRequest" + + + ProjectPhaseResponse: + title: Single project phase response object + 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 + metadata: + $ref: "#/definitions/ResponseMetadata" + content: + $ref: "#/definitions/ProjectPhase" + + ProjectPhaseListResponse: + title: Project phase list response object + type: object + properties: + id: + type: string + readOnly: true + description: unique id identifying the request + version: + type: string + result: + type: object + properties: + success: + type: boolean + status: + type: string + description: http status code + metadata: + $ref: "#/definitions/ResponseMetadata" + content: + type: array + items: + $ref: "#/definitions/ProjectPhase" + + + PhaseProductRequest: + title: Phase product request object + type: object + properties: + name: + type: string + description: the phase product name + directProjectId: + type: number + description: the phase product direct project id + billingAccountId: + type: number + description: the phase product billing account Id + templateId: + type: number + description: the phase product template id + type: + type: string + description: the phase product type + estimatedPrice: + type: number + description: the phase product estimated price + actualPrice: + type: number + description: the phase product actual price + details: + type: object + description: the phase product details + + PhaseProductBodyParam: + title: Phase product body param + type: object + required: + - param + properties: + param: + $ref: "#/definitions/PhaseProductRequest" + + PhaseProduct: + title: Phase product object + allOf: + - type: object + required: + - id + - createdAt + - createdBy + - updatedAt + - updatedBy + properties: + id: + type: number + format: int64 + description: the id + createdAt: + type: string + description: Datetime (GMT) when object was created + readOnly: true + createdBy: + type: integer + format: int64 + description: READ-ONLY. User who created this object + readOnly: true + updatedAt: + type: string + description: READ-ONLY. Datetime (GMT) when object was updated + readOnly: true + updatedBy: + type: integer + format: int64 + description: READ-ONLY. User that last updated this object + readOnly: true + - $ref: "#/definitions/PhaseProductRequest" + + + PhaseProductResponse: + title: Single phase product response object + 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 + metadata: + $ref: "#/definitions/ResponseMetadata" + content: + $ref: "#/definitions/PhaseProduct" + + PhaseProductListResponse: + title: Phase product list response object + type: object + properties: + id: + type: string + readOnly: true + description: unique id identifying the request + version: + type: string + result: + type: object + properties: + success: + type: boolean + status: + type: string + description: http status code + metadata: + $ref: "#/definitions/ResponseMetadata" + content: + type: array + items: + $ref: "#/definitions/PhaseProduct" \ No newline at end of file