diff --git a/README.md b/README.md index c487aab6..e2e5a707 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Authentication is handled via Authorization (Bearer) token header field. Token i ``` eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJwc2hhaDEiLCJleHAiOjI0NjI0OTQ2MTgsInVzZXJJZCI6IjQwMTM1OTc4IiwiaWF0IjoxNDYyNDk0MDE4LCJlbWFpbCI6InBzaGFoMUB0ZXN0LmNvbSIsImp0aSI6ImY0ZTFhNTE0LTg5ODAtNDY0MC04ZWM1LWUzNmUzMWE3ZTg0OSJ9.XuNN7tpMOXvBG1QwWRQROj7NfuUbqhkjwn39Vy4tR5I ``` -It's been signed with the secret 'secret'. This secret should match your entry in config/local.js. You can generate your own token using https://jwt.io +It's been signed with the secret 'secret'. This secret should match your entry in config/local.json. You can generate your own token using https://jwt.io ### Local Deployment Build image: diff --git a/config/custom-environment-variables.json b/config/custom-environment-variables.json index 3e0b0287..6b5731a7 100644 --- a/config/custom-environment-variables.json +++ b/config/custom-environment-variables.json @@ -9,9 +9,7 @@ "host": "PROJECTS_ES_URL", "apiVersion": "2.3", "indexName": "PROJECTS_ES_INDEX_NAME", - "docType": "projectV4", - "timelineIndexName": "TIMELINES_ES_INDEX_NAME", - "timelineDocType": "TIMELINES_ES_DOC_TYPE" + "docType": "projectV4" }, "rabbitmqURL": "RABBITMQ_URL", "pubsubQueueName": "PUBSUB_QUEUE_NAME", diff --git a/config/default.json b/config/default.json index 556a5a10..053f0c35 100644 --- a/config/default.json +++ b/config/default.json @@ -20,9 +20,7 @@ "host": "", "apiVersion": "2.3", "indexName": "projects", - "docType": "projectV4", - "timelineIndexName": "timelines", - "timelineDocType": "timelineV4" + "docType": "projectV4" }, "systemUserClientId": "", "systemUserClientSecret": "", diff --git a/config/test.json b/config/test.json index 8668be6e..26d22a7a 100644 --- a/config/test.json +++ b/config/test.json @@ -7,9 +7,7 @@ "host": "http://localhost:9200", "apiVersion": "2.3", "indexName": "projects_test", - "docType": "projectV4", - "timelineIndexName": "timelines_test", - "timelineDocType": "timelineV4" + "docType": "projectV4" }, "rabbitmqUrl": "amqp://localhost:5672", "dbConfig": { diff --git a/migrations/elasticsearch_sync.js b/migrations/elasticsearch_sync.js index eac45e5f..321f86cb 100644 --- a/migrations/elasticsearch_sync.js +++ b/migrations/elasticsearch_sync.js @@ -16,7 +16,6 @@ import util from '../src/util'; const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName'); const ES_PROJECT_TYPE = config.get('elasticsearchConfig.docType'); -const ES_TIMELINE_INDEX = config.get('elasticsearchConfig.timelineIndexName'); // create new elasticsearch client // the client modifies the config object, so always passed the cloned object @@ -324,14 +323,10 @@ esClient.indices.delete({ ignore: [404], }) .then(() => esClient.indices.create(getRequestBody(ES_PROJECT_INDEX))) -// Re-create timeline index -.then(() => esClient.indices.delete({ index: ES_TIMELINE_INDEX, ignore: [404] })) -.then(() => esClient.indices.create({ index: ES_TIMELINE_INDEX })) .then(() => { console.log('elasticsearch indices synced successfully'); process.exit(); -}) -.catch((err) => { +}).catch((err) => { console.error('elasticsearch indices sync failed', err); process.exit(); }); diff --git a/migrations/seedElasticsearchIndex.js b/migrations/seedElasticsearchIndex.js index cf353f8f..4a10ec48 100644 --- a/migrations/seedElasticsearchIndex.js +++ b/migrations/seedElasticsearchIndex.js @@ -6,7 +6,6 @@ import config from 'config'; import Promise from 'bluebird'; import models from '../src/models'; import RabbitMQService from '../src/services/rabbitmq'; -import { TIMELINE_REFERENCES } from '../src/constants'; const logger = bunyan.createLogger({ name: 'init-es', level: config.get('logLevel') }); @@ -24,19 +23,6 @@ function getProjectIds() { return []; } -/** - * Retrieve timeline ids from cli if provided - * @return {Array} list of timelineIds - */ -function getTimelineIds() { - let timelineIdArg = _.find(process.argv, a => a.indexOf('timelineIds') > -1); - if (timelineIdArg) { - timelineIdArg = timelineIdArg.split('='); - return timelineIdArg[1].split(',').map(i => parseInt(i, 10)); - } - return []; -} - Promise.coroutine(function* wrapped() { try { const rabbit = new RabbitMQService(logger); @@ -72,48 +58,12 @@ Promise.coroutine(function* wrapped() { logger.info(`Retrieved #${members.length} members`); members = _.groupBy(members, 'projectId'); - // Get timelines - const timelineIds = getTimelineIds(); - const timelineWhereClause = (timelineIds.length > 0) ? { id: { $in: timelineIds } } : {}; - let timelines = yield models.Timeline.findAll({ - where: timelineWhereClause, - include: [{ model: models.Milestone, as: 'milestones' }], - }); - logger.info(`Retrieved #${projects.length} timelines`); - - // Convert to raw json and remove unnecessary fields - timelines = _.map(timelines, (timeline) => { - const entity = _.omit(timeline.toJSON(), ['deletedBy', 'deletedAt']); - entity.milestones = _.map(entity.milestones, milestone => _.omit(milestone, ['deletedBy', 'deletedAt'])); - return entity; - }); - - // Get projectId for each timeline - yield Promise.all( - _.map(timelines, (timeline) => { - if (timeline.reference === TIMELINE_REFERENCES.PROJECT) { - timeline.projectId = timeline.referenceId; - return Promise.resolve(timeline); - } - - return models.ProjectPhase.findById(timeline.referenceId) - .then((phase) => { - timeline.projectId = phase.projectId; - return Promise.resolve(timeline); - }); - }), - ); - const promises = []; _.forEach(projects, (p) => { p.members = members[p.id]; logger.debug(`Processing Project #${p.id}`); promises.push(rabbit.publish('project.initial', p, {})); }); - _.forEach(timelines, (t) => { - logger.debug(`Processing Timeline #${t.id}`); - promises.push(rabbit.publish('timeline.initial', t, {})); - }); Promise.all(promises) .then(() => { logger.info(`Published ${promises.length} msgs`); diff --git a/postman.json b/postman.json index 048cea72..d9cb23a4 100644 --- a/postman.json +++ b/postman.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "440ee43d-66ca-4c9b-858d-22db97ea4cea", + "_postman_id": "1791b330-5331-4768-a265-f1cb5e6b4492", "name": "tc-project-service", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -2840,7 +2840,7 @@ }, { "name": "issue86 (create project with templateId)", - "description": null, + "description": "", "item": [ { "name": "Create project with templateId", @@ -2928,18 +2928,8 @@ "mode": "raw", "raw": "{\n\t\"param\": {\n\t\t\"targetVersion\": \"v3\",\n\t\t\"defaultProductTemplateId\": 3\n\t}\n}" }, - "url": { - "raw": "{{api-url}}/v4/projects/6/upgrade", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "projects", - "6", - "upgrade" - ] - } + "url": "{{api-url}}/v4/projects/6/upgrade", + "description": "" }, "response": [] }, @@ -2961,18 +2951,8 @@ "mode": "raw", "raw": "{\n\t\"param\": {\n\t\t\"targetVersion\": \"v3\",\n\t\t\"defaultProductTemplateId\": 3\n\t}\n}" }, - "url": { - "raw": "{{api-url}}/v4/projects/7/upgrade", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "projects", - "7", - "upgrade" - ] - } + "url": "{{api-url}}/v4/projects/7/upgrade", + "description": "" }, "response": [] }, @@ -2994,18 +2974,8 @@ "mode": "raw", "raw": "{\n\t\"param\": {\n\t\t\"targetVersion\": \"v3\",\n\t\t\"defaultProductTemplateId\": 3,\n\t\t\"phaseName\": \"Custom phase name\"\n\t}\n}" }, - "url": { - "raw": "{{api-url}}/v4/projects/6/upgrade", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "projects", - "6", - "upgrade" - ] - } + "url": "{{api-url}}/v4/projects/6/upgrade", + "description": "" }, "response": [] }, @@ -3027,1156 +2997,12 @@ "mode": "raw", "raw": "{\n\t\"param\": {\n\t\t\"targetVersion\": \"v3\",\n\t\t\"defaultProductTemplateId\": 3,\n\t\t\"phaseName\": \"Custom phase name\"\n\t}\n}" }, - "url": { - "raw": "{{api-url}}/v4/projects/7/upgrade", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "projects", - "7", - "upgrade" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Timeline", - "description": null, - "item": [ - { - "name": "Create timeline", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{jwt-token-connectAdmin-40051336}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"name\":\"new name\",\r\n \"description\":\"new description\",\r\n \"startDate\":\"2018-05-29T00:00:00.000Z\",\r\n \"endDate\": \"2018-05-30T00:00:00.000Z\",\r\n \"reference\": \"project\",\r\n \"referenceId\": 1\r\n }\r\n}" - }, - "url": { - "raw": "{{api-url}}/v4/timelines", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "timelines" - ] - } - }, - "response": [] - }, - { - "name": "Create timeline with invalid data", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{jwt-token-connectAdmin-40051336}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"startDate\":\"2018-05-29T00:00:00.000Z\",\r\n \"endDate\": \"2018-05-28T00:00:00.000Z\",\r\n \"reference\": \"invalid\",\r\n \"referenceId\": 0\r\n }\r\n}" - }, - "url": { - "raw": "{{api-url}}/v4/timelines", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "timelines" - ] - } - }, - "response": [] - }, - { - "name": "List timelines", - "request": { - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{jwt-token-copilot-40051332}}" - } - ], - "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": { - "raw": "{{api-url}}/v4/timelines", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "timelines" - ] - } - }, - "response": [] - }, - { - "name": "List timelines (filter by reference)", - "request": { - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{jwt-token-copilot-40051332}}" - } - ], - "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": { - "raw": "{{api-url}}/v4/timelines?filter=reference%3Dproject", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "timelines" - ], - "query": [ - { - "key": "filter", - "value": "reference%3Dproject" - } - ] - } - }, - "response": [] - }, - { - "name": "List timelines (filter by referenceId)", - "request": { - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{jwt-token-copilot-40051332}}" - } - ], - "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": { - "raw": "{{api-url}}/v4/timelines?filter=referenceId%3D1", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "timelines" - ], - "query": [ - { - "key": "filter", - "value": "referenceId%3D1" - } - ] - } - }, - "response": [] - }, - { - "name": "List timelines (filter by reference and referenceId)", - "request": { - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{jwt-token-copilot-40051332}}" - } - ], - "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": { - "raw": "{{api-url}}/v4/timelines?filter=reference%3Dphase%26referenceId%3D1", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "timelines" - ], - "query": [ - { - "key": "filter", - "value": "reference%3Dphase%26referenceId%3D1" - } - ] - } - }, - "response": [] - }, - { - "name": "Get timeline", - "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": { - "raw": "{{api-url}}/v4/timelines/1", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "timelines", - "1" - ] - } - }, - "response": [] - }, - { - "name": "Update timeline", - "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\": \"timeline 1-updated\",\r\n \"description\": \"description-updated\",\r\n \"startDate\": \"2018-05-01T00:00:00.000Z\",\r\n \"endDate\": null,\r\n \"reference\": \"project\",\r\n \"referenceId\": 1\r\n }\r\n}" - }, - "url": { - "raw": "{{api-url}}/v4/timelines/1", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "timelines", - "1" - ] - } - }, - "response": [] - }, - { - "name": "Update timeline (startDate)", - "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\": \"timeline 1-updated\",\r\n \"description\": \"description-updated\",\r\n \"startDate\": \"2018-05-05T00:00:00.000Z\",\r\n \"endDate\": null,\r\n \"reference\": \"project\",\r\n \"referenceId\": 1\r\n }\r\n}" - }, - "url": { - "raw": "{{api-url}}/v4/timelines/1", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "timelines", - "1" - ] - } - }, - "response": [] - }, - { - "name": "Update timeline (endDate)", - "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\": \"timeline 1-updated\",\r\n \"description\": \"description-updated\",\r\n \"startDate\": \"2018-05-04T00:00:00.000Z\",\r\n \"endDate\": \"2018-05-05T00:00:00.000Z\",\r\n \"reference\": \"project\",\r\n \"referenceId\": 1\r\n }\r\n}" - }, - "url": { - "raw": "{{api-url}}/v4/timelines/1", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "timelines", - "1" - ] - } - }, - "response": [] - }, - { - "name": "Delete timeline", - "request": { - "method": "DELETE", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{jwt-token}}" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{api-url}}/v4/timelines/4", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "timelines", - "4" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Milestone", - "description": null, - "item": [ - { - "name": "Create milestone", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{jwt-token-member-40051331}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"name\": \"milestone 3\",\r\n \"description\": \"description 3\",\r\n \"duration\": 4,\r\n \"startDate\": \"2018-05-05T00:00:00.000Z\",\r\n \"endDate\": \"2018-05-07T00:00:00.000Z\",\r\n \"completionDate\": \"2018-05-08T00:00:00.000Z\",\r\n \"status\": \"open\",\r\n \"type\": \"type3\",\r\n \"details\": {\r\n \"detail1\": {\r\n \"subDetail1C\": 3\r\n },\r\n \"detail2\": [\r\n 2,\r\n 3,\r\n 4\r\n ]\r\n },\r\n \"order\": 1,\r\n \"plannedText\": \"plannedText 3\",\r\n \"activeText\": \"activeText 3\",\r\n \"completedText\": \"completedText 3\",\r\n \"blockedText\": \"blockedText 3\"\r\n }\r\n}" - }, - "url": { - "raw": "{{api-url}}/v4/timelines/1/milestones", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "timelines", - "1", - "milestones" - ] - } - }, - "response": [] - }, - { - "name": "Create milestone with invalid data", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{jwt-token-member-40051331}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"startDate\": \"2018-05-05T00:00:00.000Z\",\r\n \"endDate\": \"2018-05-04T00:00:00.000Z\",\r\n \"completionDate\": \"2018-05-04T00:00:00.000Z\"\r\n }\r\n}" - }, - "url": { - "raw": "{{api-url}}/v4/timelines/1/milestones", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "timelines", - "1", - "milestones" - ] - } - }, - "response": [] - }, - { - "name": "List milestones", - "request": { - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{jwt-token-copilot-40051332}}" - } - ], - "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": { - "raw": "{{api-url}}/v4/timelines/1/milestones", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "timelines", - "1", - "milestones" - ] - } - }, - "response": [] - }, - { - "name": "List milestones (sort)", - "request": { - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{jwt-token-copilot-40051332}}" - } - ], - "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": { - "raw": "{{api-url}}/v4/timelines/1/milestones?sort=order desc", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "timelines", - "1", - "milestones" - ], - "query": [ - { - "key": "sort", - "value": "order desc" - } - ] - } - }, - "response": [] - }, - { - "name": "Get milestone", - "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": { - "raw": "{{api-url}}/v4/timelines/1/milestones/1", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "timelines", - "1", - "milestones", - "1" - ] - } - }, - "response": [] - }, - { - "name": "Update milestone", - "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\": \"milestone 1-updated\",\r\n \"description\": \"description-updated\",\r\n \"duration\": 3,\r\n \"startDate\": \"2018-05-04T00:00:00.000Z\",\r\n \"endDate\": \"2018-05-06T00:00:00.000Z\",\r\n \"completionDate\": \"2018-05-07T00:00:00.000Z\",\r\n \"status\": \"closed\",\r\n \"type\": \"type2\",\r\n \"details\": {\r\n \"detail1\": {\r\n \"subDetail1C\": 3\r\n },\r\n \"detail2\": [\r\n 4\r\n ]\r\n },\r\n \"order\": 1,\r\n \"plannedText\": \"plannedText 1-updated\",\r\n \"activeText\": \"activeText 1-updated\",\r\n \"completedText\": \"completedText 1-updated\",\r\n \"blockedText\": \"blockedText 1-updated\"\r\n }\r\n}" - }, - "url": { - "raw": "{{api-url}}/v4/timelines/1/milestones/1", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "timelines", - "1", - "milestones", - "1" - ] - } - }, - "response": [] - }, - { - "name": "Update milestone (order 1 => 2)", - "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\": \"milestone 1-updated\",\r\n \"description\": \"description-updated\",\r\n \"duration\": 3,\r\n \"startDate\": \"2018-05-04T00:00:00.000Z\",\r\n \"endDate\": \"2018-05-06T00:00:00.000Z\",\r\n \"completionDate\": \"2018-05-07T00:00:00.000Z\",\r\n \"status\": \"closed\",\r\n \"type\": \"type2\",\r\n \"details\": {\r\n \"detail1\": {\r\n \"subDetail1C\": 3\r\n },\r\n \"detail2\": [\r\n 4\r\n ]\r\n },\r\n \"order\": 2,\r\n \"plannedText\": \"plannedText 1-updated\",\r\n \"activeText\": \"activeText 1-updated\",\r\n \"completedText\": \"completedText 1-updated\",\r\n \"blockedText\": \"blockedText 1-updated\"\r\n }\r\n}" - }, - "url": { - "raw": "{{api-url}}/v4/timelines/1/milestones/1", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "timelines", - "1", - "milestones", - "1" - ] - } - }, - "response": [] - }, - { - "name": "Update milestone (order 2 => 1)", - "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\": \"milestone 1-updated\",\r\n \"description\": \"description-updated\",\r\n \"duration\": 3,\r\n \"startDate\": \"2018-05-04T00:00:00.000Z\",\r\n \"endDate\": \"2018-05-06T00:00:00.000Z\",\r\n \"completionDate\": \"2018-05-07T00:00:00.000Z\",\r\n \"status\": \"closed\",\r\n \"type\": \"type2\",\r\n \"details\": {\r\n \"detail1\": {\r\n \"subDetail1C\": 3\r\n },\r\n \"detail2\": [\r\n 4\r\n ]\r\n },\r\n \"order\": 1,\r\n \"plannedText\": \"plannedText 1-updated\",\r\n \"activeText\": \"activeText 1-updated\",\r\n \"completedText\": \"completedText 1-updated\",\r\n \"blockedText\": \"blockedText 1-updated\"\r\n }\r\n}" - }, - "url": { - "raw": "{{api-url}}/v4/timelines/1/milestones/1", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "timelines", - "1", - "milestones", - "1" - ] - } - }, - "response": [] - }, - { - "name": "Update milestone (order 1 => 3)", - "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\": \"milestone 1-updated\",\r\n \"description\": \"description-updated\",\r\n \"duration\": 3,\r\n \"startDate\": \"2018-05-04T00:00:00.000Z\",\r\n \"endDate\": \"2018-05-06T00:00:00.000Z\",\r\n \"completionDate\": \"2018-05-07T00:00:00.000Z\",\r\n \"status\": \"closed\",\r\n \"type\": \"type2\",\r\n \"details\": {\r\n \"detail1\": {\r\n \"subDetail1C\": 3\r\n },\r\n \"detail2\": [\r\n 4\r\n ]\r\n },\r\n \"order\": 3,\r\n \"plannedText\": \"plannedText 1-updated\",\r\n \"activeText\": \"activeText 1-updated\",\r\n \"completedText\": \"completedText 1-updated\",\r\n \"blockedText\": \"blockedText 1-updated\"\r\n }\r\n}" - }, - "url": { - "raw": "{{api-url}}/v4/timelines/1/milestones/1", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "timelines", - "1", - "milestones", - "1" - ] - } - }, - "response": [] - }, - { - "name": "Update milestone (order 3 => 1)", - "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\": \"milestone 1-updated\",\r\n \"description\": \"description-updated\",\r\n \"duration\": 3,\r\n \"startDate\": \"2018-05-04T00:00:00.000Z\",\r\n \"endDate\": \"2018-05-06T00:00:00.000Z\",\r\n \"completionDate\": \"2018-05-07T00:00:00.000Z\",\r\n \"status\": \"closed\",\r\n \"type\": \"type2\",\r\n \"details\": {\r\n \"detail1\": {\r\n \"subDetail1C\": 3\r\n },\r\n \"detail2\": [\r\n 4\r\n ]\r\n },\r\n \"order\": 1,\r\n \"plannedText\": \"plannedText 1-updated\",\r\n \"activeText\": \"activeText 1-updated\",\r\n \"completedText\": \"completedText 1-updated\",\r\n \"blockedText\": \"blockedText 1-updated\"\r\n }\r\n}" - }, - "url": { - "raw": "{{api-url}}/v4/timelines/1/milestones/1", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "timelines", - "1", - "milestones", - "1" - ] - } - }, - "response": [] - }, - { - "name": "Delete milestone", - "request": { - "method": "DELETE", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{jwt-token}}" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{api-url}}/v4/timelines/1/milestones/2", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "timelines", - "1", - "milestones", - "2" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Milestone Template", - "description": null, - "item": [ - { - "name": "Create milestone template", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{jwt-token-admin-40051333}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"param\":{\r\n \"name\": \"milestoneTemplate 3\",\r\n \"description\": \"description 3\",\r\n \"duration\": 33,\r\n \"type\": \"type3\",\r\n \"order\": 1\r\n }\r\n}" - }, - "url": { - "raw": "{{api-url}}/v4/productTemplates/1/milestones", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "productTemplates", - "1", - "milestones" - ] - } - }, - "response": [] - }, - { - "name": "Create milestone template with invalid data", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{jwt-token-admin-40051333}}" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"param\":{\r\n\r\n }\r\n}" - }, - "url": { - "raw": "{{api-url}}/v4/productTemplates/1/milestones", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "productTemplates", - "1", - "milestones" - ] - } - }, - "response": [] - }, - { - "name": "List milestone templates", - "request": { - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{jwt-token-copilot-40051332}}" - } - ], - "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": { - "raw": "{{api-url}}/v4/productTemplates/1/milestones", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "productTemplates", - "1", - "milestones" - ] - } - }, - "response": [] - }, - { - "name": "List milestone templates (sort)", - "request": { - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{jwt-token-copilot-40051332}}" - } - ], - "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": { - "raw": "{{api-url}}/v4/productTemplates/1/milestones?sort=order desc", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "productTemplates", - "1", - "milestones" - ], - "query": [ - { - "key": "sort", - "value": "order desc" - } - ] - } - }, - "response": [] - }, - { - "name": "Get milestone 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": { - "raw": "{{api-url}}/v4/productTemplates/1/milestones/1", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "productTemplates", - "1", - "milestones", - "1" - ] - } - }, - "response": [] - }, - { - "name": "Update milestone", - "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\t\"name\": \"milestoneTemplate 1-updated\",\r\n\t\"description\": \"description 1-updated\",\r\n\t\"duration\": 34,\r\n\t\"type\": \"type1-updated\",\r\n\t\"order\": 1\r\n }\r\n}" - }, - "url": { - "raw": "{{api-url}}/v4/productTemplates/1/milestones/1", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "productTemplates", - "1", - "milestones", - "1" - ] - } - }, - "response": [] - }, - { - "name": "Update milestone (order 1 => 2)", - "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\": \"milestoneTemplate 1-updated\",\r\n \"description\": \"description 1-updated\",\r\n \"duration\": 34,\r\n \"type\": \"type1-updated\",\r\n \"order\": 2\r\n }\r\n}" - }, - "url": { - "raw": "{{api-url}}/v4/productTemplates/1/milestones/1", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "productTemplates", - "1", - "milestones", - "1" - ] - } - }, - "response": [] - }, - { - "name": "Update milestone (order 2 => 1)", - "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\t\"name\": \"milestoneTemplate 1-updated\",\r\n\t\"description\": \"description 1-updated\",\r\n\t\"duration\": 34,\r\n\t\"type\": \"type1-updated\",\r\n\t\"order\": 1\r\n }\r\n}" - }, - "url": { - "raw": "{{api-url}}/v4/productTemplates/1/milestones/1", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "productTemplates", - "1", - "milestones", - "1" - ] - } - }, - "response": [] - }, - { - "name": "Update milestone (order 1 => 3)", - "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\": \"milestoneTemplate 1-updated\",\r\n \"description\": \"description 1-updated\",\r\n \"duration\": 34,\r\n \"type\": \"type1-updated\",\r\n \"order\": 3\r\n }\r\n}" - }, - "url": { - "raw": "{{api-url}}/v4/productTemplates/1/milestones/1", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "productTemplates", - "1", - "milestones", - "1" - ] - } - }, - "response": [] - }, - { - "name": "Update milestone (order 3 => 1)", - "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\": \"milestoneTemplate 1-updated\",\r\n \"description\": \"description 1-updated\",\r\n \"duration\": 34,\r\n \"type\": \"type1-updated\",\r\n \"order\": 1\r\n }\r\n}" - }, - "url": { - "raw": "{{api-url}}/v4/productTemplates/1/milestones/1", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "productTemplates", - "1", - "milestones", - "1" - ] - } - }, - "response": [] - }, - { - "name": "Delete milestone", - "request": { - "method": "DELETE", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Authorization", - "value": "Bearer {{jwt-token}}" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{api-url}}/v4/productTemplates/1/milestones/2", - "host": [ - "{{api-url}}" - ], - "path": [ - "v4", - "productTemplates", - "1", - "milestones", - "2" - ] - } + "url": "{{api-url}}/v4/projects/7/upgrade", + "description": "" }, "response": [] } ] } ] -} \ No newline at end of file +} diff --git a/postman_environment.json b/postman_environment.json index 84968c61..12fab912 100644 --- a/postman_environment.json +++ b/postman_environment.json @@ -15,48 +15,6 @@ "description": "", "type": "text", "enabled": true - }, - { - "key": "jwt-token-admin-40051333", - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", - "description": "", - "type": "text", - "enabled": true - }, - { - "key": "jwt-token-member-40051331", - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzEiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.pDtRzcGQjgCBD6aLsW-1OFhzmrv5mXhb8YLDWbGAnKo", - "description": "", - "type": "text", - "enabled": true - }, - { - "key": "jwt-token-copilot-40051332", - "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBDb3BpbG90Il0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjo0MDA1MTMzMiwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImlhdCI6MTQ3MDYyMDA0NH0.DnX17gBaVF2JTuRai-C2BDSdEjij9da_s4eYcMIjP0c", - "description": "", - "type": "text", - "enabled": true - }, - { - "key": "jwt-token-manager-40051334", - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzQiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.J5VtOEQVph5jfe2Ji-NH7txEDcx_5gthhFeD-MzX9ck", - "description": "", - "type": "text", - "enabled": true - }, - { - "key": "jwt-token-member2-40051335", - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJtZW1iZXIyIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzNSIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.Mh4bw3wm-cn5Kcf96gLFVlD0kySOqqk4xN3qnreAKL4", - "description": "", - "type": "text", - "enabled": true - }, - { - "key": "jwt-token-connectAdmin-40051336", - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJDb25uZWN0IEFkbWluIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJjb25uZWN0X2FkbWluMSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzYiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoiY29ubmVjdF9hZG1pbjFAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.nSGfXMl02NZ90ZKLiEKPg75iAjU92mfteaY6xgqkM30", - "description": "", - "type": "text", - "enabled": true } ], "_postman_variable_scope": "environment", diff --git a/src/constants.js b/src/constants.js index a99469f5..4e24c806 100644 --- a/src/constants.js +++ b/src/constants.js @@ -49,14 +49,6 @@ export const EVENT = { PROJECT_PHASE_PRODUCT_ADDED: 'project.phase.product.added', PROJECT_PHASE_PRODUCT_UPDATED: 'project.phase.product.updated', PROJECT_PHASE_PRODUCT_REMOVED: 'project.phase.product.removed', - - TIMELINE_ADDED: 'timeline.added', - TIMELINE_UPDATED: 'timeline.updated', - TIMELINE_REMOVED: 'timeline.removed', - - MILESTONE_ADDED: 'milestone.added', - MILESTONE_UPDATED: 'milestone.updated', - MILESTONE_REMOVED: 'milestone.removed', }, }; @@ -96,8 +88,3 @@ export const REGEX = { export const TOKEN_SCOPES = { CONNECT_PROJECT_ADMIN: 'all:connect_project', }; - -export const TIMELINE_REFERENCES = { - PROJECT: 'project', - PHASE: 'phase', -}; diff --git a/src/events/index.js b/src/events/index.js index fac17d8d..cf6decf8 100644 --- a/src/events/index.js +++ b/src/events/index.js @@ -9,8 +9,6 @@ import { projectPhaseAddedHandler, projectPhaseRemovedHandler, projectPhaseUpdatedHandler } from './projectPhases'; import { phaseProductAddedHandler, phaseProductRemovedHandler, phaseProductUpdatedHandler } from './phaseProducts'; -import { timelineAddedHandler, timelineUpdatedHandler, timelineRemovedHandler } from './timelines'; -import { milestoneAddedHandler, milestoneUpdatedHandler, milestoneRemovedHandler } from './milestones'; export default { 'project.initial': projectCreatedHandler, @@ -32,13 +30,4 @@ export default { [EVENT.ROUTING_KEY.PROJECT_PHASE_PRODUCT_ADDED]: phaseProductAddedHandler, [EVENT.ROUTING_KEY.PROJECT_PHASE_PRODUCT_REMOVED]: phaseProductRemovedHandler, [EVENT.ROUTING_KEY.PROJECT_PHASE_PRODUCT_UPDATED]: phaseProductUpdatedHandler, - - // Timeline and milestone - 'timeline.initial': timelineAddedHandler, - [EVENT.ROUTING_KEY.TIMELINE_ADDED]: timelineAddedHandler, - [EVENT.ROUTING_KEY.TIMELINE_REMOVED]: timelineRemovedHandler, - [EVENT.ROUTING_KEY.TIMELINE_UPDATED]: timelineUpdatedHandler, - [EVENT.ROUTING_KEY.MILESTONE_ADDED]: milestoneAddedHandler, - [EVENT.ROUTING_KEY.MILESTONE_REMOVED]: milestoneRemovedHandler, - [EVENT.ROUTING_KEY.MILESTONE_UPDATED]: milestoneUpdatedHandler, }; diff --git a/src/events/milestones/index.js b/src/events/milestones/index.js deleted file mode 100644 index 3ebd578a..00000000 --- a/src/events/milestones/index.js +++ /dev/null @@ -1,150 +0,0 @@ -/** - * Event handlers for milestone create, update and delete. - */ -import config from 'config'; -import _ from 'lodash'; -import Promise from 'bluebird'; -import util from '../../util'; - -const ES_TIMELINE_INDEX = config.get('elasticsearchConfig.timelineIndexName'); -const ES_TIMELINE_TYPE = config.get('elasticsearchConfig.timelineDocType'); - -const eClient = util.getElasticSearchClient(); - -/** - * Handler for milestone creation event - * @param {Object} logger logger to log along with trace id - * @param {Object} msg event payload - * @param {Object} channel channel to ack, nack - */ -const milestoneAddedHandler = Promise.coroutine(function* (logger, msg, channel) { // eslint-disable-line func-names - const data = JSON.parse(msg.content.toString()); - try { - const doc = yield eClient.get({ index: ES_TIMELINE_INDEX, type: ES_TIMELINE_TYPE, id: data.timelineId }); - const milestones = _.isArray(doc._source.milestones) ? doc._source.milestones : []; // eslint-disable-line no-underscore-dangle - - // Increase the order of the other milestones in the same timeline, - // which have `order` >= this milestone order - _.each(milestones, (milestone) => { - if (milestone.order >= data.order) { - milestone.order += 1; // eslint-disable-line no-param-reassign - } - }); - - milestones.push(data); - const merged = _.assign(doc._source, { milestones }); // eslint-disable-line no-underscore-dangle - yield eClient.update({ - index: ES_TIMELINE_INDEX, - type: ES_TIMELINE_TYPE, - id: data.timelineId, - body: { doc: merged }, - }); - logger.debug('milestone added to timeline document successfully'); - channel.ack(msg); - } catch (error) { - logger.error(`Error processing event (milestoneId: ${data.id})`, error); - // if the message has been redelivered dont attempt to reprocess it - channel.nack(msg, false, !msg.fields.redelivered); - } -}); - -/** - * Handler for milestone updated event - * @param {Object} logger logger to log along with trace id - * @param {Object} msg event payload - * @param {Object} channel channel to ack, nack - * @returns {undefined} - */ -const milestoneUpdatedHandler = Promise.coroutine(function* (logger, msg, channel) { // eslint-disable-line func-names - const data = JSON.parse(msg.content.toString()); - try { - const doc = yield eClient.get({ index: ES_TIMELINE_INDEX, type: ES_TIMELINE_TYPE, id: data.original.timelineId }); - const milestones = _.map(doc._source.milestones, (single) => { // eslint-disable-line no-underscore-dangle - if (single.id === data.original.id) { - return _.assign(single, data.updated); - } - return single; - }); - - if (data.original.order !== data.updated.order) { - const milestoneWithSameOrder = - _.find(milestones, milestone => milestone.id !== data.updated.id && milestone.order === data.updated.order); - if (milestoneWithSameOrder) { - // Increase the order from M to K: if there is an item with order K, - // orders from M+1 to K should be made M to K-1 - if (data.original.order < data.updated.order) { - _.each(milestones, (single) => { - if (single.id !== data.updated.id - && (data.original.order + 1) <= single.order - && single.order <= data.updated.order) { - single.order -= 1; // eslint-disable-line no-param-reassign - } - }); - } else { - // Decrease the order from M to K: if there is an item with order K, - // orders from K to M-1 should be made K+1 to M - _.each(milestones, (single) => { - if (single.id !== data.updated.id - && data.updated.order <= single.order - && single.order <= (data.original.order - 1)) { - single.order += 1; // eslint-disable-line no-param-reassign - } - }); - } - } - } - - const merged = _.assign(doc._source, { milestones }); // eslint-disable-line no-underscore-dangle - yield eClient.update({ - index: ES_TIMELINE_INDEX, - type: ES_TIMELINE_TYPE, - id: data.original.timelineId, - body: { - doc: merged, - }, - }); - logger.debug('elasticsearch index updated, milestone updated successfully'); - channel.ack(msg); - } catch (error) { - logger.error(`Error processing event (milestoneId: ${data.original.id})`, error); - // if the message has been redelivered dont attempt to reprocess it - channel.nack(msg, false, !msg.fields.redelivered); - } -}); - -/** - * Handler for milestone deleted event - * @param {Object} logger logger to log along with trace id - * @param {Object} msg event payload - * @param {Object} channel channel to ack, nack - * @returns {undefined} - */ -const milestoneRemovedHandler = Promise.coroutine(function* (logger, msg, channel) { // eslint-disable-line func-names - const data = JSON.parse(msg.content.toString()); - try { - const doc = yield eClient.get({ index: ES_TIMELINE_INDEX, type: ES_TIMELINE_TYPE, id: data.timelineId }); - const milestones = _.filter(doc._source.milestones, single => single.id !== data.id); // eslint-disable-line no-underscore-dangle - const merged = _.assign(doc._source, { milestones }); // eslint-disable-line no-underscore-dangle - yield eClient.update({ - index: ES_TIMELINE_INDEX, - type: ES_TIMELINE_TYPE, - id: data.timelineId, - body: { - doc: merged, - }, - }); - logger.debug('milestone removed from timeline document successfully'); - channel.ack(msg); - } catch (error) { - logger.error(`Error processing event (milestoneId: ${data.id})`, error); - // if the message has been redelivered dont attempt to reprocess it - channel.nack(msg, false, !msg.fields.redelivered); - } -}); - - -module.exports = { - milestoneAddedHandler, - milestoneRemovedHandler, - milestoneUpdatedHandler, -}; diff --git a/src/events/timelines/index.js b/src/events/timelines/index.js deleted file mode 100644 index 0de36410..00000000 --- a/src/events/timelines/index.js +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Event handlers for timeline create, update and delete - */ -import _ from 'lodash'; -import Promise from 'bluebird'; -import config from 'config'; -import util from '../../util'; - -const ES_TIMELINE_INDEX = config.get('elasticsearchConfig.timelineIndexName'); -const ES_TIMELINE_TYPE = config.get('elasticsearchConfig.timelineDocType'); -const eClient = util.getElasticSearchClient(); - -/** - * Handler for timeline creation event - * @param {Object} logger logger to log along with trace id - * @param {Object} msg event payload - * @param {Object} channel channel to ack, nack - */ -const timelineAddedHandler = Promise.coroutine(function* (logger, msg, channel) { // eslint-disable-line func-names - const data = JSON.parse(msg.content.toString()); - try { - // add the record to the index - const result = yield eClient.index({ - index: ES_TIMELINE_INDEX, - type: ES_TIMELINE_TYPE, - id: data.id, - body: data, - }); - logger.debug(`timeline indexed successfully (timelineId: ${data.id})`, result); - channel.ack(msg); - } catch (error) { - logger.error(`Error processing event (timelineId: ${data.id})`, error); - channel.nack(msg, false, !msg.fields.redelivered); - } -}); - -/** - * Handler for timeline updated event - * @param {Object} logger logger to log along with trace id - * @param {Object} msg event payload - * @param {Object} channel channel to ack, nack - */ -const timelineUpdatedHandler = Promise.coroutine(function* (logger, msg, channel) { // eslint-disable-line func-names - const data = JSON.parse(msg.content.toString()); - try { - // first get the existing document and than merge the updated changes and save the new document - const doc = yield eClient.get({ index: ES_TIMELINE_INDEX, type: ES_TIMELINE_TYPE, id: data.original.id }); - const merged = _.merge(doc._source, data.updated); // eslint-disable-line no-underscore-dangle - merged.milestones = data.updated.milestones; - // update the merged document - yield eClient.update({ - index: ES_TIMELINE_INDEX, - type: ES_TIMELINE_TYPE, - id: data.original.id, - body: { - doc: merged, - }, - }); - logger.debug(`timeline updated successfully in elasticsearh index, (timelineId: ${data.original.id})`); - channel.ack(msg); - } catch (error) { - logger.error(`failed to get timeline document, (timelineId: ${data.original.id})`, error); - channel.nack(msg, false, !msg.fields.redelivered); - } -}); - -/** - * Handler for timeline deleted event - * @param {Object} logger logger to log along with trace id - * @param {Object} msg event payload - * @param {Object} channel channel to ack, nack - */ -const timelineRemovedHandler = Promise.coroutine(function* (logger, msg, channel) { // eslint-disable-line func-names - const data = JSON.parse(msg.content.toString()); - try { - yield eClient.delete({ index: ES_TIMELINE_INDEX, type: ES_TIMELINE_TYPE, id: data.id }); - logger.debug(`timeline deleted successfully from elasticsearh index (timelineId: ${data.id})`); - channel.ack(msg); - } catch (error) { - logger.error(`failed to delete timeline document (timelineId: ${data.id})`, error); - channel.nack(msg, false, !msg.fields.redelivered); - } -}); - - -module.exports = { - timelineAddedHandler, - timelineUpdatedHandler, - timelineRemovedHandler, -}; diff --git a/src/models/milestone.js b/src/models/milestone.js deleted file mode 100644 index cb3e0306..00000000 --- a/src/models/milestone.js +++ /dev/null @@ -1,39 +0,0 @@ -/* eslint-disable valid-jsdoc */ - -/** - * The Milestone model - */ -module.exports = (sequelize, DataTypes) => { - const Milestone = sequelize.define('Milestone', { - id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, - name: { type: DataTypes.STRING(255), allowNull: false }, - description: DataTypes.STRING(255), - duration: { type: DataTypes.INTEGER, allowNull: false }, - startDate: { type: DataTypes.DATE, allowNull: false }, - endDate: DataTypes.DATE, - completionDate: DataTypes.DATE, - status: { type: DataTypes.STRING(45), allowNull: false }, - type: { type: DataTypes.STRING(45), allowNull: false }, - details: DataTypes.JSON, - order: { type: DataTypes.INTEGER, allowNull: false }, - plannedText: { type: DataTypes.STRING(512), allowNull: false }, - activeText: { type: DataTypes.STRING(512), allowNull: false }, - completedText: { type: DataTypes.STRING(512), allowNull: false }, - blockedText: { type: DataTypes.STRING(512), 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: 'milestones', - paranoid: true, - timestamps: true, - updatedAt: 'updatedAt', - createdAt: 'createdAt', - deletedAt: 'deletedAt', - }); - - return Milestone; -}; diff --git a/src/models/productMilestoneTemplate.js b/src/models/productMilestoneTemplate.js deleted file mode 100644 index acd40c11..00000000 --- a/src/models/productMilestoneTemplate.js +++ /dev/null @@ -1,30 +0,0 @@ -/* eslint-disable valid-jsdoc */ - -/** - * The Product Milestone Template model - */ -module.exports = (sequelize, DataTypes) => { - const ProductMilestoneTemplate = sequelize.define('ProductMilestoneTemplate', { - id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, - name: { type: DataTypes.STRING(255), allowNull: false }, - description: DataTypes.STRING(255), - duration: { type: DataTypes.INTEGER, allowNull: false }, - type: { type: DataTypes.STRING(45), allowNull: false }, - order: { type: DataTypes.INTEGER, 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_milestone_templates', - paranoid: true, - timestamps: true, - updatedAt: 'updatedAt', - createdAt: 'createdAt', - deletedAt: 'deletedAt', - }); - - return ProductMilestoneTemplate; -}; diff --git a/src/models/productTemplate.js b/src/models/productTemplate.js index 2671d98e..72d7bc30 100644 --- a/src/models/productTemplate.js +++ b/src/models/productTemplate.js @@ -26,15 +26,6 @@ module.exports = (sequelize, DataTypes) => { updatedAt: 'updatedAt', createdAt: 'createdAt', deletedAt: 'deletedAt', - classMethods: { - associate: (models) => { - ProductTemplate.hasMany(models.ProductMilestoneTemplate, { - as: 'milestones', - foreignKey: 'productTemplateId', - onDelete: 'cascade', - }); - }, - }, }); return ProductTemplate; diff --git a/src/models/timeline.js b/src/models/timeline.js deleted file mode 100644 index 5b9d6247..00000000 --- a/src/models/timeline.js +++ /dev/null @@ -1,36 +0,0 @@ -/* eslint-disable valid-jsdoc */ - -/** - * The Timeline model - */ -module.exports = (sequelize, DataTypes) => { - const Timeline = sequelize.define('Timeline', { - id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, - name: { type: DataTypes.STRING(255), allowNull: false }, - description: DataTypes.STRING(255), - startDate: { type: DataTypes.DATE, allowNull: false }, - endDate: DataTypes.DATE, - reference: { type: DataTypes.STRING(45), allowNull: false }, - referenceId: { type: DataTypes.BIGINT, 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: 'timelines', - paranoid: true, - timestamps: true, - updatedAt: 'updatedAt', - createdAt: 'createdAt', - deletedAt: 'deletedAt', - classMethods: { - associate: (models) => { - Timeline.hasMany(models.Milestone, { as: 'milestones', foreignKey: 'timelineId', onDelete: 'cascade' }); - }, - }, - }); - - return Timeline; -}; diff --git a/src/permissions/index.js b/src/permissions/index.js index f0d3af2a..6ea7a418 100644 --- a/src/permissions/index.js +++ b/src/permissions/index.js @@ -35,11 +35,6 @@ module.exports = () => { Authorizer.setPolicy('productTemplate.delete', connectManagerOrAdmin); Authorizer.setPolicy('productTemplate.view', true); - Authorizer.setPolicy('milestoneTemplate.create', connectManagerOrAdmin); - Authorizer.setPolicy('milestoneTemplate.edit', connectManagerOrAdmin); - Authorizer.setPolicy('milestoneTemplate.delete', connectManagerOrAdmin); - Authorizer.setPolicy('milestoneTemplate.view', true); - Authorizer.setPolicy('project.addProjectPhase', projectEdit); Authorizer.setPolicy('project.updateProjectPhase', projectEdit); Authorizer.setPolicy('project.deleteProjectPhase', projectEdit); @@ -51,14 +46,4 @@ module.exports = () => { Authorizer.setPolicy('projectType.edit', projectAdmin); Authorizer.setPolicy('projectType.delete', projectAdmin); Authorizer.setPolicy('projectType.view', true); // anyone can view project types - - Authorizer.setPolicy('timeline.create', projectEdit); - Authorizer.setPolicy('timeline.edit', projectEdit); - Authorizer.setPolicy('timeline.delete', projectEdit); - Authorizer.setPolicy('timeline.view', projectView); - - Authorizer.setPolicy('milestone.create', projectEdit); - Authorizer.setPolicy('milestone.edit', projectEdit); - Authorizer.setPolicy('milestone.delete', projectEdit); - Authorizer.setPolicy('milestone.view', projectView); }; diff --git a/src/routes/index.js b/src/routes/index.js index d9b46645..289f47c6 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -27,7 +27,7 @@ router.get(`/${apiVersion}/projects/health`, (req, res) => { const jwtAuth = require('tc-core-library-js').middleware.jwtAuthenticator; router.all( - RegExp(`\\/${apiVersion}\\/(projects|projectTemplates|productTemplates|projectTypes|timelines)(?!\\/health).*`), + RegExp(`\\/${apiVersion}\\/(projects|projectTemplates|productTemplates|projectTypes)(?!\\/health).*`), jwtAuth()); // Register all the routes @@ -88,15 +88,6 @@ router.route('/v4/productTemplates/:templateId(\\d+)') .patch(require('./productTemplates/update')) .delete(require('./productTemplates/delete')); -router.route('/v4/productTemplates/:productTemplateId(\\d+)/milestones') - .post(require('./milestoneTemplates/create')) - .get(require('./milestoneTemplates/list')); - -router.route('/v4/productTemplates/:productTemplateId(\\d+)/milestones/:milestoneTemplateId(\\d+)') - .get(require('./milestoneTemplates/get')) - .patch(require('./milestoneTemplates/update')) - .delete(require('./milestoneTemplates/delete')); - router.route('/v4/projects/:projectId(\\d+)/phases') .get(require('./phases/list')) .post(require('./phases/create')); @@ -124,24 +115,6 @@ router.route('/v4/projectTypes/:key') .patch(require('./projectTypes/update')) .delete(require('./projectTypes/delete')); -router.route('/v4/timelines') - .post(require('./timelines/create')) - .get(require('./timelines/list')); - -router.route('/v4/timelines/:timelineId(\\d+)') - .get(require('./timelines/get')) - .patch(require('./timelines/update')) - .delete(require('./timelines/delete')); - -router.route('/v4/timelines/:timelineId(\\d+)/milestones') - .post(require('./milestones/create')) - .get(require('./milestones/list')); - -router.route('/v4/timelines/:timelineId(\\d+)/milestones/:milestoneId(\\d+)') - .get(require('./milestones/get')) - .patch(require('./milestones/update')) - .delete(require('./milestones/delete')); - // register error handler router.use((err, req, res, next) => { // eslint-disable-line no-unused-vars // DO NOT REMOVE next arg.. even though eslint diff --git a/src/routes/milestoneTemplates/create.js b/src/routes/milestoneTemplates/create.js deleted file mode 100644 index 55ea5c12..00000000 --- a/src/routes/milestoneTemplates/create.js +++ /dev/null @@ -1,85 +0,0 @@ -/** - * API to add a milestone template - */ -import validate from 'express-validation'; -import _ from 'lodash'; -import Joi from 'joi'; -import Sequelize from 'sequelize'; -import { middleware as tcMiddleware } from 'tc-core-library-js'; -import util from '../../util'; -import models from '../../models'; - -const permissions = tcMiddleware.permissions; - -const schema = { - params: { - productTemplateId: Joi.number().integer().positive().required(), - }, - body: { - param: Joi.object().keys({ - id: Joi.any().strip(), - name: Joi.string().max(255).required(), - description: Joi.string().max(255), - duration: Joi.number().integer().required(), - type: Joi.string().max(45).required(), - order: Joi.number().integer().required(), - productTemplateId: Joi.any().strip(), - 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('milestoneTemplate.create'), - (req, res, next) => { - const entity = _.assign(req.body.param, { - createdBy: req.authUser.userId, - updatedBy: req.authUser.userId, - productTemplateId: req.params.productTemplateId, - }); - let result; - - return models.sequelize.transaction(tx => - // Find the product template - models.ProductTemplate.findById(req.params.productTemplateId, { transaction: tx }) - .then((productTemplate) => { - // Not found - if (!productTemplate) { - const apiErr = new Error( - `Product template not found for product template id ${req.params.productTemplateId}`); - apiErr.status = 404; - return Promise.reject(apiErr); - } - - // Create the milestone template - return models.ProductMilestoneTemplate.create(entity, { transaction: tx }); - }) - .then((createdEntity) => { - // Omit deletedAt and deletedBy - result = _.omit(createdEntity.toJSON(), 'deletedAt', 'deletedBy'); - - // Increase the order of the other milestone templates in the same product template, - // which have `order` >= this milestone template order - return models.ProductMilestoneTemplate.update({ order: Sequelize.literal('"order" + 1') }, { - where: { - productTemplateId: req.params.productTemplateId, - id: { $ne: result.id }, - order: { $gte: result.order }, - }, - transaction: tx, - }); - }) - .then(() => { - // Write to response - res.status(201).json(util.wrapResponse(req.id, result, 1, 201)); - }) - .catch(next), - ); - }, -]; diff --git a/src/routes/milestoneTemplates/create.spec.js b/src/routes/milestoneTemplates/create.spec.js deleted file mode 100644 index 6fe6c128..00000000 --- a/src/routes/milestoneTemplates/create.spec.js +++ /dev/null @@ -1,282 +0,0 @@ -/** - * Tests for create.js - */ -import chai from 'chai'; -import request from 'supertest'; -import _ from 'lodash'; -import server from '../../app'; -import testUtil from '../../tests/util'; -import models from '../../models'; - -const should = chai.should(); - -const productTemplates = [ - { - 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, - deletedAt: new Date(), - }, -]; -const milestoneTemplates = [ - { - name: 'milestoneTemplate 1', - duration: 3, - type: 'type1', - order: 1, - productTemplateId: 1, - createdBy: 1, - updatedBy: 2, - }, - { - name: 'milestoneTemplate 2', - duration: 4, - type: 'type2', - order: 2, - productTemplateId: 1, - createdBy: 2, - updatedBy: 3, - }, -]; - -describe('CREATE milestone template', () => { - beforeEach(() => testUtil.clearDb() - .then(() => models.ProductTemplate.bulkCreate(productTemplates)) - .then(() => models.ProductMilestoneTemplate.bulkCreate(milestoneTemplates)), - ); - after(testUtil.clearDb); - - describe('POST /productTemplates/{productTemplateId}/milestones', () => { - const body = { - param: { - name: 'milestoneTemplate 3', - description: 'description 3', - duration: 33, - type: 'type3', - order: 1, - }, - }; - - it('should return 403 if user is not authenticated', (done) => { - request(server) - .post('/v4/productTemplates/1/milestones') - .send(body) - .expect(403, done); - }); - - it('should return 403 for member', (done) => { - request(server) - .post('/v4/productTemplates/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.member}`, - }) - .send(body) - .expect(403, done); - }); - - it('should return 403 for copilot', (done) => { - request(server) - .post('/v4/productTemplates/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .send(body) - .expect(403, done); - }); - - it('should return 404 for non-existed product template', (done) => { - request(server) - .post('/v4/productTemplates/1000/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(body) - .expect(404, done); - }); - - it('should return 422 if missing name', (done) => { - const invalidBody = { - param: { - name: undefined, - }, - }; - - request(server) - .post('/v4/productTemplates/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if missing duration', (done) => { - const invalidBody = { - param: { - duration: undefined, - }, - }; - - request(server) - .post('/v4/productTemplates/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if missing type', (done) => { - const invalidBody = { - param: { - type: undefined, - }, - }; - - request(server) - .post('/v4/productTemplates/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if missing order', (done) => { - const invalidBody = { - param: { - order: undefined, - }, - }; - - request(server) - .post('/v4/productTemplates/1/milestones') - .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/1/milestones') - .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.description.should.be.eql(body.param.description); - resJson.duration.should.be.eql(body.param.duration); - resJson.type.should.be.eql(body.param.type); - resJson.order.should.be.eql(body.param.order); - - 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); - - // Verify 'order' of the other milestones - models.ProductMilestoneTemplate.findAll({ - where: { - productTemplateId: 1, - }, - }) - .then((milestones) => { - _.each(milestones, (milestone) => { - if (milestone.id === 1) { - milestone.order.should.be.eql(1 + 1); - } else if (milestone.id === 2) { - milestone.order.should.be.eql(2 + 1); - } - }); - - done(); - }); - }); - }); - - it('should return 201 for connect manager', (done) => { - request(server) - .post('/v4/productTemplates/1/milestones') - .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/1/milestones') - .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/milestoneTemplates/delete.js b/src/routes/milestoneTemplates/delete.js deleted file mode 100644 index bacb3e36..00000000 --- a/src/routes/milestoneTemplates/delete.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * API to delete a milestone 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: { - productTemplateId: Joi.number().integer().positive().required(), - milestoneTemplateId: Joi.number().integer().positive().required(), - }, -}; - -module.exports = [ - validate(schema), - permissions('milestoneTemplate.delete'), - (req, res, next) => { - const where = { - id: req.params.milestoneTemplateId, - deletedAt: { $eq: null }, - productTemplateId: req.params.productTemplateId, - }; - - return models.sequelize.transaction(tx => - // Update the deletedBy - models.ProductMilestoneTemplate.update({ deletedBy: req.authUser.userId }, { - where, - returning: true, - raw: true, - transaction: tx, - }) - .then((updatedResults) => { - // Not found - if (updatedResults[0] === 0) { - const apiErr = new Error( - `Milestone template not found for milestone template id ${req.params.milestoneTemplateId}`); - apiErr.status = 404; - return Promise.reject(apiErr); - } - - // Soft delete - return models.ProductMilestoneTemplate.destroy({ - where, - transaction: tx, - }); - }) - .then(() => { - res.status(204).end(); - }) - .catch(next), - ); - }, -]; diff --git a/src/routes/milestoneTemplates/delete.spec.js b/src/routes/milestoneTemplates/delete.spec.js deleted file mode 100644 index 02fd111c..00000000 --- a/src/routes/milestoneTemplates/delete.spec.js +++ /dev/null @@ -1,187 +0,0 @@ -/** - * Tests for delete.js - */ -import request from 'supertest'; - -import models from '../../models'; -import server from '../../app'; -import testUtil from '../../tests/util'; - -const productTemplates = [ - { - 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, - deletedAt: new Date(), - }, -]; -const milestoneTemplates = [ - { - id: 1, - name: 'milestoneTemplate 1', - duration: 3, - type: 'type1', - order: 1, - productTemplateId: 1, - createdBy: 1, - updatedBy: 2, - }, - { - id: 2, - name: 'milestoneTemplate 2', - duration: 4, - type: 'type2', - order: 2, - productTemplateId: 1, - createdBy: 2, - updatedBy: 3, - deletedAt: new Date(), - }, -]; - -describe('DELETE milestone template', () => { - beforeEach(() => testUtil.clearDb() - .then(() => models.ProductTemplate.bulkCreate(productTemplates)) - .then(() => models.ProductMilestoneTemplate.bulkCreate(milestoneTemplates)), - ); - after(testUtil.clearDb); - - describe('DELETE /productTemplates/{productTemplateId}/milestones/{milestoneTemplateId}', () => { - it('should return 403 if user is not authenticated', (done) => { - request(server) - .delete('/v4/productTemplates/1/milestones/1') - .expect(403, done); - }); - - it('should return 403 for member', (done) => { - request(server) - .delete('/v4/productTemplates/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.member}`, - }) - .expect(403, done); - }); - - it('should return 403 for copilot', (done) => { - request(server) - .delete('/v4/productTemplates/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .expect(403, done); - }); - - it('should return 404 for non-existed product template', (done) => { - request(server) - .delete('/v4/productTemplates/1234/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(404, done); - }); - - it('should return 404 for non-existed milestone template', (done) => { - request(server) - .delete('/v4/productTemplates/1/milestones/444') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(404, done); - }); - - it('should return 404 for deleted milestone template', (done) => { - request(server) - .delete('/v4/productTemplates/1/milestones/2') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(404, done); - }); - - it('should return 422 for invalid productTemplateId param', (done) => { - request(server) - .delete('/v4/productTemplates/0/milestones/2') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(422, done); - }); - - it('should return 422 for invalid milestoneTemplateId param', (done) => { - request(server) - .delete('/v4/productTemplates/1/milestones/0') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(422, done); - }); - - it('should return 204, for admin, if template was successfully removed', (done) => { - request(server) - .delete('/v4/productTemplates/1/milestones/1') - .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/1/milestones/1') - .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/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, - }) - .expect(204) - .end(done); - }); - }); -}); diff --git a/src/routes/milestoneTemplates/get.js b/src/routes/milestoneTemplates/get.js deleted file mode 100644 index 1c1fa3f0..00000000 --- a/src/routes/milestoneTemplates/get.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * API to get a milestone 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: { - productTemplateId: Joi.number().integer().positive().required(), - milestoneTemplateId: Joi.number().integer().positive().required(), - }, -}; - -module.exports = [ - validate(schema), - permissions('milestoneTemplate.view'), - (req, res, next) => models.ProductMilestoneTemplate.findOne({ - where: { - id: req.params.milestoneTemplateId, - productTemplateId: req.params.productTemplateId, - }, - attributes: { exclude: ['deletedAt', 'deletedBy'] }, - raw: true, - }) - .then((milestoneTemplate) => { - // Not found - if (!milestoneTemplate) { - const apiErr = new Error( - `Milestone template not found for milestone template id ${req.params.milestoneTemplateId}`); - apiErr.status = 404; - return Promise.reject(apiErr); - } - - res.json(util.wrapResponse(req.id, milestoneTemplate)); - return Promise.resolve(); - }) - .catch(next), -]; diff --git a/src/routes/milestoneTemplates/get.spec.js b/src/routes/milestoneTemplates/get.spec.js deleted file mode 100644 index c2b144e3..00000000 --- a/src/routes/milestoneTemplates/get.spec.js +++ /dev/null @@ -1,189 +0,0 @@ -/** - * 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(); - -const productTemplates = [ - { - 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, - deletedAt: new Date(), - }, -]; -const milestoneTemplates = [ - { - id: 1, - name: 'milestoneTemplate 1', - duration: 3, - type: 'type1', - order: 1, - productTemplateId: 1, - createdBy: 1, - updatedBy: 2, - }, - { - id: 2, - name: 'milestoneTemplate 2', - duration: 4, - type: 'type2', - order: 2, - productTemplateId: 1, - createdBy: 2, - updatedBy: 3, - deletedAt: new Date(), - }, -]; - -describe('GET milestone template', () => { - beforeEach(() => testUtil.clearDb() - .then(() => models.ProductTemplate.bulkCreate(productTemplates)) - .then(() => models.ProductMilestoneTemplate.bulkCreate(milestoneTemplates)), - ); - after(testUtil.clearDb); - - describe('GET /productTemplates/{productTemplateId}/milestones/{milestoneTemplateId}', () => { - it('should return 403 if user is not authenticated', (done) => { - request(server) - .get('/v4/productTemplates/1/milestones/1') - .expect(403, done); - }); - - it('should return 404 for non-existed product template', (done) => { - request(server) - .get('/v4/productTemplates/1234/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(404, done); - }); - - it('should return 404 for non-existed milestone template', (done) => { - request(server) - .get('/v4/productTemplates/1/milestones/1111') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(404, done); - }); - - it('should return 404 for deleted milestone template', (done) => { - request(server) - .get('/v4/productTemplates/1/milestones/2') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(404, done); - }); - - it('should return 200 for admin', (done) => { - request(server) - .get('/v4/productTemplates/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(200) - .end((err, res) => { - const resJson = res.body.result.content; - resJson.id.should.be.eql(milestoneTemplates[0].id); - resJson.name.should.be.eql(milestoneTemplates[0].name); - resJson.duration.should.be.eql(milestoneTemplates[0].duration); - resJson.type.should.be.eql(milestoneTemplates[0].type); - resJson.order.should.be.eql(milestoneTemplates[0].order); - resJson.productTemplateId.should.be.eql(milestoneTemplates[0].productTemplateId); - - resJson.createdBy.should.be.eql(milestoneTemplates[0].createdBy); - should.exist(resJson.createdAt); - resJson.updatedBy.should.be.eql(milestoneTemplates[0].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/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, - }) - .expect(200) - .end(done); - }); - - it('should return 200 for connect manager', (done) => { - request(server) - .get('/v4/productTemplates/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, - }) - .expect(200) - .end(done); - }); - - it('should return 200 for member', (done) => { - request(server) - .get('/v4/productTemplates/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.member}`, - }) - .expect(200, done); - }); - - it('should return 200 for copilot', (done) => { - request(server) - .get('/v4/productTemplates/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .expect(200, done); - }); - }); -}); diff --git a/src/routes/milestoneTemplates/list.js b/src/routes/milestoneTemplates/list.js deleted file mode 100644 index 40b6ae19..00000000 --- a/src/routes/milestoneTemplates/list.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * API to list all milestone templates - */ -import validate from 'express-validation'; -import Joi from 'joi'; -import _ from 'lodash'; -import { middleware as tcMiddleware } from 'tc-core-library-js'; -import util from '../../util'; -import models from '../../models'; - -const permissions = tcMiddleware.permissions; - -const schema = { - params: { - productTemplateId: Joi.number().integer().positive().required(), - }, -}; - -module.exports = [ - validate(schema), - permissions('milestoneTemplate.view'), - (req, res, next) => { - // Parse the sort query - let sort = req.query.sort ? decodeURIComponent(req.query.sort) : 'order'; - if (sort && sort.indexOf(' ') === -1) { - sort += ' asc'; - } - const sortableProps = [ - 'order asc', 'order desc', - ]; - if (sort && _.indexOf(sortableProps, sort) < 0) { - const apiErr = new Error('Invalid sort criteria'); - apiErr.status = 422; - return next(apiErr); - } - const sortColumnAndOrder = sort.split(' '); - - // Get all milestone templates - return models.ProductMilestoneTemplate.findAll({ - where: { - productTemplateId: req.params.productTemplateId, - }, - order: [sortColumnAndOrder], - attributes: { exclude: ['deletedAt', 'deletedBy'] }, - raw: true, - }) - .then((milestoneTemplates) => { - res.json(util.wrapResponse(req.id, milestoneTemplates)); - }) - .catch(next); - }, -]; diff --git a/src/routes/milestoneTemplates/list.spec.js b/src/routes/milestoneTemplates/list.spec.js deleted file mode 100644 index 87fb3228..00000000 --- a/src/routes/milestoneTemplates/list.spec.js +++ /dev/null @@ -1,217 +0,0 @@ -/** - * 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(); - -const productTemplates = [ - { - 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, - deletedAt: new Date(), - }, -]; -const milestoneTemplates = [ - { - id: 1, - name: 'milestoneTemplate 1', - duration: 3, - type: 'type1', - order: 1, - productTemplateId: 1, - createdBy: 1, - updatedBy: 2, - }, - { - id: 2, - name: 'milestoneTemplate 2', - duration: 4, - type: 'type2', - order: 2, - productTemplateId: 1, - createdBy: 2, - updatedBy: 3, - }, - { - id: 3, - name: 'milestoneTemplate 3', - duration: 5, - type: 'type3', - order: 3, - productTemplateId: 1, - createdBy: 2, - updatedBy: 3, - deletedAt: new Date(), - }, -]; - -describe('LIST milestone template', () => { - beforeEach(() => testUtil.clearDb() - .then(() => models.ProductTemplate.bulkCreate(productTemplates)) - .then(() => models.ProductMilestoneTemplate.bulkCreate(milestoneTemplates)), - ); - after(testUtil.clearDb); - - describe('GET /productTemplates/{productTemplateId}/milestones', () => { - it('should return 403 if user is not authenticated', (done) => { - request(server) - .get('/v4/productTemplates/1/milestones') - .expect(403, done); - }); - - it('should return 422 for invalid productTemplateId param', (done) => { - request(server) - .get('/v4/productTemplates/0/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(422, done); - }); - - it('should return 422 for invalid sort column', (done) => { - request(server) - .get('/v4/productTemplates/1/milestones?sort=id') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(422, done); - }); - - it('should return 422 for invalid sort order', (done) => { - request(server) - .get('/v4/productTemplates/1/milestones?sort=order%20invalid') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(422, done); - }); - - it('should return 200 for admin', (done) => { - request(server) - .get('/v4/productTemplates/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(200) - .end((err, res) => { - const resJson = res.body.result.content; - resJson.should.have.length(2); - resJson[0].id.should.be.eql(milestoneTemplates[0].id); - resJson[0].name.should.be.eql(milestoneTemplates[0].name); - resJson[0].duration.should.be.eql(milestoneTemplates[0].duration); - resJson[0].type.should.be.eql(milestoneTemplates[0].type); - resJson[0].order.should.be.eql(milestoneTemplates[0].order); - resJson[0].productTemplateId.should.be.eql(milestoneTemplates[0].productTemplateId); - - resJson[0].createdBy.should.be.eql(milestoneTemplates[0].createdBy); - should.exist(resJson[0].createdAt); - resJson[0].updatedBy.should.be.eql(milestoneTemplates[0].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/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, - }) - .expect(200) - .end(done); - }); - - it('should return 200 for connect manager', (done) => { - request(server) - .get('/v4/productTemplates/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, - }) - .expect(200) - .end(done); - }); - - it('should return 200 for member', (done) => { - request(server) - .get('/v4/productTemplates/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.member}`, - }) - .expect(200, done); - }); - - it('should return 200 for copilot', (done) => { - request(server) - .get('/v4/productTemplates/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .expect(200, done); - }); - - it('should return 200 with sort desc', (done) => { - request(server) - .get('/v4/productTemplates/1/milestones?sort=order%20desc') - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .expect(200) - .end((err, res) => { - const resJson = res.body.result.content; - resJson.should.have.length(2); - resJson[0].id.should.be.eql(2); - resJson[1].id.should.be.eql(1); - - done(); - }); - }); - }); -}); diff --git a/src/routes/milestoneTemplates/update.js b/src/routes/milestoneTemplates/update.js deleted file mode 100644 index 65da9e9f..00000000 --- a/src/routes/milestoneTemplates/update.js +++ /dev/null @@ -1,121 +0,0 @@ -/** - * API to update a milestone template - */ -import validate from 'express-validation'; -import _ from 'lodash'; -import Joi from 'joi'; -import Sequelize from 'sequelize'; -import { middleware as tcMiddleware } from 'tc-core-library-js'; -import util from '../../util'; -import models from '../../models'; - -const permissions = tcMiddleware.permissions; - -const schema = { - params: { - productTemplateId: Joi.number().integer().positive().required(), - milestoneTemplateId: Joi.number().integer().positive().required(), - }, - body: { - param: Joi.object().keys({ - id: Joi.any().strip(), - name: Joi.string().max(255).required(), - description: Joi.string().max(255), - duration: Joi.number().integer().required(), - type: Joi.string().max(45).required(), - order: Joi.number().integer().required(), - productTemplateId: Joi.any().strip(), - 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('milestoneTemplate.edit'), - (req, res, next) => { - const entityToUpdate = _.assign(req.body.param, { - updatedBy: req.authUser.userId, - }); - - let original; - let updated; - - return models.sequelize.transaction(() => - // Get the milestone template - models.ProductMilestoneTemplate.findOne({ - where: { - id: req.params.milestoneTemplateId, - productTemplateId: req.params.productTemplateId, - }, - attributes: { exclude: ['deletedAt', 'deletedBy'] }, - }) - .then((milestoneTemplate) => { - // Not found - if (!milestoneTemplate) { - const apiErr = new Error(`Milestone template not found for template id ${req.params.milestoneTemplateId}`); - apiErr.status = 404; - return Promise.reject(apiErr); - } - - original = _.omit(milestoneTemplate.toJSON(), ['deletedAt', 'deletedBy']); - - // Update - return milestoneTemplate.update(entityToUpdate); - }) - .then((milestoneTemplate) => { - updated = _.omit(milestoneTemplate.toJSON(), ['deletedAt', 'deletedBy']); - - // Update order of the other milestones only if the order was changed - if (original.order === updated.order) { - return Promise.resolve(); - } - - return models.ProductMilestoneTemplate.count({ - where: { - productTemplateId: updated.productTemplateId, - id: { $ne: updated.id }, - order: updated.order, - }, - }) - .then((count) => { - if (count === 0) { - return Promise.resolve(); - } - - // Increase the order from M to K: if there is an item with order K, - // orders from M+1 to K should be made M to K-1 - if (original.order < updated.order) { - return models.ProductMilestoneTemplate.update({ order: Sequelize.literal('"order" - 1') }, { - where: { - productTemplateId: updated.productTemplateId, - id: { $ne: updated.id }, - order: { $between: [original.order + 1, updated.order] }, - }, - }); - } - - // Decrease the order from M to K: if there is an item with order K, - // orders from K to M-1 should be made K+1 to M - return models.ProductMilestoneTemplate.update({ order: Sequelize.literal('"order" + 1') }, { - where: { - productTemplateId: updated.productTemplateId, - id: { $ne: updated.id }, - order: { $between: [updated.order, original.order - 1] }, - }, - }); - }); - }) - .then(() => { - res.json(util.wrapResponse(req.id, updated)); - return Promise.resolve(); - }) - .catch(next), - ); - }, -]; diff --git a/src/routes/milestoneTemplates/update.spec.js b/src/routes/milestoneTemplates/update.spec.js deleted file mode 100644 index 297f6ea9..00000000 --- a/src/routes/milestoneTemplates/update.spec.js +++ /dev/null @@ -1,428 +0,0 @@ -/** - * Tests for get.js - */ -import chai from 'chai'; -import request from 'supertest'; -import _ from 'lodash'; -import models from '../../models'; -import server from '../../app'; -import testUtil from '../../tests/util'; - -const should = chai.should(); - -const productTemplates = [ - { - 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, - deletedAt: new Date(), - }, -]; -const milestoneTemplates = [ - { - id: 1, - name: 'milestoneTemplate 1', - duration: 3, - type: 'type1', - order: 1, - productTemplateId: 1, - createdBy: 1, - updatedBy: 2, - }, - { - id: 2, - name: 'milestoneTemplate 2', - duration: 4, - type: 'type2', - order: 2, - productTemplateId: 1, - createdBy: 2, - updatedBy: 3, - }, - { - id: 3, - name: 'milestoneTemplate 3', - duration: 5, - type: 'type3', - order: 3, - productTemplateId: 1, - createdBy: 2, - updatedBy: 3, - }, - { - id: 4, - name: 'milestoneTemplate 4', - duration: 5, - type: 'type4', - order: 4, - productTemplateId: 1, - createdBy: 2, - updatedBy: 3, - deletedAt: new Date(), - }, -]; - -describe('UPDATE milestone template', () => { - beforeEach(() => testUtil.clearDb() - .then(() => models.ProductTemplate.bulkCreate(productTemplates)) - .then(() => models.ProductMilestoneTemplate.bulkCreate(milestoneTemplates)), - ); - after(testUtil.clearDb); - - describe('PATCH /productTemplates/{productTemplateId}/milestones/{milestoneTemplateId}', () => { - const body = { - param: { - name: 'milestoneTemplate 1-updated', - description: 'description-updated', - duration: 6, - type: 'type1-updated', - order: 5, - }, - }; - - it('should return 403 if user is not authenticated', (done) => { - request(server) - .patch('/v4/productTemplates/1/milestones/1') - .send(body) - .expect(403, done); - }); - - it('should return 403 for member', (done) => { - request(server) - .patch('/v4/productTemplates/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.member}`, - }) - .send(body) - .expect(403, done); - }); - - it('should return 403 for copilot', (done) => { - request(server) - .patch('/v4/productTemplates/1/milestones/1') - .send(body) - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .expect(403, done); - }); - - it('should return 422 for missing name', (done) => { - const invalidBody = { - param: { - name: undefined, - }, - }; - - request(server) - .patch('/v4/productTemplates/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect(422, done); - }); - - it('should return 422 for missing type', (done) => { - const invalidBody = { - param: { - type: undefined, - }, - }; - - request(server) - .patch('/v4/productTemplates/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect(422, done); - }); - - it('should return 422 for missing duration', (done) => { - const invalidBody = { - param: { - duration: undefined, - }, - }; - - request(server) - .patch('/v4/productTemplates/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect(422, done); - }); - - it('should return 422 for missing order', (done) => { - const invalidBody = { - param: { - order: undefined, - }, - }; - - request(server) - .patch('/v4/productTemplates/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect(422, done); - }); - - it('should return 404 for non-existed product template', (done) => { - request(server) - .patch('/v4/productTemplates/122/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(body) - .expect(404, done); - }); - - it('should return 404 for non-existed milestone template', (done) => { - request(server) - .patch('/v4/productTemplates/1/milestones/111') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(body) - .expect(404, done); - }); - - it('should return 404 for deleted milestone template', (done) => { - request(server) - .patch('/v4/productTemplates/1/milestones/4') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(body) - .expect(404, done); - }); - - it('should return 200 for admin', (done) => { - request(server) - .patch('/v4/productTemplates/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(body) - .expect(200) - .end((err, res) => { - const resJson = res.body.result.content; - resJson.id.should.be.eql(1); - resJson.name.should.be.eql(body.param.name); - resJson.description.should.be.eql(body.param.description); - resJson.duration.should.be.eql(body.param.duration); - resJson.type.should.be.eql(body.param.type); - resJson.order.should.be.eql(body.param.order); - - should.exist(resJson.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(); - }); - }); - - // eslint-disable-next-line func-names - it('should return 200 for admin - order increases and replaces another milestone\'s order', function (done) { - this.timeout(10000); - - request(server) - .patch('/v4/productTemplates/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send({ param: _.assign({}, body.param, { order: 3 }) }) // 1 to 3 - .expect(200) - .end(() => { - // Milestone 1: order 3 - // Milestone 2: order 2 - 1 = 1 - // Milestone 3: order 3 - 1 = 2 - setTimeout(() => { - models.ProductMilestoneTemplate.findById(1) - .then((milestone) => { - milestone.order.should.be.eql(3); - }) - .then(() => models.ProductMilestoneTemplate.findById(2)) - .then((milestone) => { - milestone.order.should.be.eql(1); - }) - .then(() => models.ProductMilestoneTemplate.findById(3)) - .then((milestone) => { - milestone.order.should.be.eql(2); - - done(); - }); - }, 3000); - }); - }); - - // eslint-disable-next-line func-names - it('should return 200 for admin - order increases and doesnot replace another milestone\'s order', function (done) { - this.timeout(10000); - - request(server) - .patch('/v4/productTemplates/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send({ param: _.assign({}, body.param, { order: 4 }) }) // 1 to 4 - .expect(200) - .end(() => { - // Milestone 1: order 4 - // Milestone 2: order 2 - // Milestone 3: order 3 - setTimeout(() => { - models.ProductMilestoneTemplate.findById(1) - .then((milestone) => { - milestone.order.should.be.eql(4); - }) - .then(() => models.ProductMilestoneTemplate.findById(2)) - .then((milestone) => { - milestone.order.should.be.eql(2); - }) - .then(() => models.ProductMilestoneTemplate.findById(3)) - .then((milestone) => { - milestone.order.should.be.eql(3); - - done(); - }); - }, 3000); - }); - }); - - // eslint-disable-next-line func-names - it('should return 200 for admin - order decreases and replaces another milestone\'s order', function (done) { - this.timeout(10000); - - request(server) - .patch('/v4/productTemplates/1/milestones/3') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send({ param: _.assign({}, body.param, { order: 1 }) }) // 3 to 1 - .expect(200) - .end(() => { - // Milestone 1: order 2 - // Milestone 2: order 3 - // Milestone 3: order 1 - setTimeout(() => { - models.ProductMilestoneTemplate.findById(1) - .then((milestone) => { - milestone.order.should.be.eql(2); - }) - .then(() => models.ProductMilestoneTemplate.findById(2)) - .then((milestone) => { - milestone.order.should.be.eql(3); - }) - .then(() => models.ProductMilestoneTemplate.findById(3)) - .then((milestone) => { - milestone.order.should.be.eql(1); - - done(); - }); - }, 3000); - }); - }); - - // eslint-disable-next-line func-names - it('should return 200 for admin - order decreases and doesnot replace another milestone\'s order', function (done) { - this.timeout(10000); - - request(server) - .patch('/v4/productTemplates/1/milestones/3') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send({ param: _.assign({}, body.param, { order: 0 }) }) // 3 to 0 - .expect(200) - .end(() => { - // Milestone 1: order 1 - // Milestone 2: order 2 - // Milestone 3: order 0 - setTimeout(() => { - models.ProductMilestoneTemplate.findById(1) - .then((milestone) => { - milestone.order.should.be.eql(1); - }) - .then(() => models.ProductMilestoneTemplate.findById(2)) - .then((milestone) => { - milestone.order.should.be.eql(2); - }) - .then(() => models.ProductMilestoneTemplate.findById(3)) - .then((milestone) => { - milestone.order.should.be.eql(0); - - done(); - }); - }, 3000); - }); - }); - - it('should return 200 for connect admin', (done) => { - request(server) - .patch('/v4/productTemplates/1/milestones/1') - .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/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, - }) - .send(body) - .expect(200) - .end(done); - }); - }); -}); diff --git a/src/routes/milestones/create.js b/src/routes/milestones/create.js deleted file mode 100644 index f653d685..00000000 --- a/src/routes/milestones/create.js +++ /dev/null @@ -1,110 +0,0 @@ -/** - * API to add a milestone - */ -import validate from 'express-validation'; -import _ from 'lodash'; -import Joi from 'joi'; -import { middleware as tcMiddleware } from 'tc-core-library-js'; -import Sequelize from 'sequelize'; -import util from '../../util'; -import models from '../../models'; -import { EVENT } from '../../constants'; - -const permissions = tcMiddleware.permissions; - -const schema = { - params: { - timelineId: Joi.number().integer().positive().required(), - }, - body: { - param: Joi.object().keys({ - id: Joi.any().strip(), - name: Joi.string().max(255).required(), - description: Joi.string().max(255), - duration: Joi.number().integer().required(), - startDate: Joi.date().required(), - endDate: Joi.date().min(Joi.ref('startDate')).allow(null), - completionDate: Joi.date().min(Joi.ref('startDate')).allow(null), - status: Joi.string().max(45).required(), - type: Joi.string().max(45).required(), - details: Joi.object(), - order: Joi.number().integer().required(), - plannedText: Joi.string().max(512).required(), - activeText: Joi.string().max(512).required(), - completedText: Joi.string().max(512).required(), - blockedText: Joi.string().max(512).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), - // Validate and get projectId from the timelineId param, and set to request params - // for checking by the permissions middleware - util.validateTimelineIdParam, - permissions('milestone.create'), - (req, res, next) => { - const entity = _.assign(req.body.param, { - createdBy: req.authUser.userId, - updatedBy: req.authUser.userId, - timelineId: req.params.timelineId, - }); - let result; - - // Validate startDate and endDate to be within the timeline startDate and endDate - let error; - if (req.body.param.startDate < req.timeline.startDate) { - error = 'Milestone startDate must not be before the timeline startDate'; - } else if (req.body.param.endDate && req.timeline.endDate && req.body.param.endDate > req.timeline.endDate) { - error = 'Milestone endDate must not be after the timeline endDate'; - } - if (error) { - const apiErr = new Error(error); - apiErr.status = 422; - return next(apiErr); - } - - return models.sequelize.transaction(tx => - // Save to DB - models.Milestone.create(entity, { transaction: tx }) - .then((createdEntity) => { - // Omit deletedAt, deletedBy - result = _.omit(createdEntity.toJSON(), 'deletedAt', 'deletedBy'); - - // Send event to bus - req.log.debug('Sending event to RabbitMQ bus for milestone %d', result.id); - req.app.services.pubsub.publish(EVENT.ROUTING_KEY.MILESTONE_ADDED, - result, - { correlationId: req.id }, - ); - - // Increase the order of the other milestones in the same timeline, - // which have `order` >= this milestone order - return models.Milestone.update({ order: Sequelize.literal('"order" + 1') }, { - where: { - timelineId: result.timelineId, - id: { $ne: result.id }, - order: { $gte: result.order }, - }, - transaction: tx, - }); - }) - .then(() => { - // Do not send events for the updated milestones here, - // because it will make 'version conflict' error in ES. - // The order of the other milestones need to be updated in the MILESTONE_ADDED event handler - - // Write to the response - res.status(201).json(util.wrapResponse(req.id, result, 1, 201)); - return Promise.resolve(); - }) - .catch(next), - ); - }, -]; diff --git a/src/routes/milestones/create.spec.js b/src/routes/milestones/create.spec.js deleted file mode 100644 index 98e72001..00000000 --- a/src/routes/milestones/create.spec.js +++ /dev/null @@ -1,606 +0,0 @@ -/** - * Tests for create.js - */ -import chai from 'chai'; -import request from 'supertest'; -import _ from 'lodash'; -import server from '../../app'; -import testUtil from '../../tests/util'; -import models from '../../models'; -import { EVENT } from '../../constants'; - -const should = chai.should(); - -describe('CREATE milestone', () => { - let projectId1; - let projectId2; - - beforeEach((done) => { - testUtil.clearDb() - .then(() => { - models.Project.bulkCreate([ - { - type: 'generic', - billingAccountId: 1, - name: 'test1', - description: 'test project1', - status: 'draft', - details: {}, - createdBy: 1, - updatedBy: 1, - }, - { - type: 'generic', - billingAccountId: 2, - name: 'test2', - description: 'test project2', - status: 'draft', - details: {}, - createdBy: 2, - updatedBy: 2, - deletedAt: '2018-05-15T00:00:00Z', - }, - ], { returning: true }) - .then((projects) => { - projectId1 = projects[0].id; - projectId2 = projects[1].id; - - // Create member - models.ProjectMember.bulkCreate([ - { - userId: 40051332, - projectId: projectId1, - role: 'copilot', - isPrimary: true, - createdBy: 1, - updatedBy: 1, - }, - { - userId: 40051331, - projectId: projectId1, - role: 'customer', - isPrimary: true, - createdBy: 1, - updatedBy: 1, - }, - ]).then(() => - // Create phase - models.ProjectPhase.bulkCreate([ - { - projectId: projectId1, - name: 'test project phase 1', - status: 'active', - startDate: '2018-05-15T00:00:00Z', - endDate: '2018-05-15T12:00:00Z', - budget: 20.0, - progress: 1.23456, - details: { - message: 'This can be any json 2', - }, - createdBy: 1, - updatedBy: 1, - }, - { - projectId: projectId2, - name: 'test project phase 2', - status: 'active', - startDate: '2018-05-16T00:00:00Z', - endDate: '2018-05-16T12:00:00Z', - budget: 21.0, - progress: 1.234567, - details: { - message: 'This can be any json 2', - }, - createdBy: 2, - updatedBy: 2, - deletedAt: '2018-05-15T00:00:00Z', - }, - ])) - .then(() => - // Create timelines - models.Timeline.bulkCreate([ - { - name: 'name 1', - description: 'description 1', - startDate: '2018-05-02T00:00:00.000Z', - endDate: '2018-06-12T00:00:00.000Z', - reference: 'project', - referenceId: 1, - createdBy: 1, - updatedBy: 1, - }, - { - name: 'name 2', - description: 'description 2', - startDate: '2018-05-12T00:00:00.000Z', - endDate: '2018-06-13T00:00:00.000Z', - reference: 'phase', - referenceId: 1, - createdBy: 1, - updatedBy: 1, - }, - { - name: 'name 3', - description: 'description 3', - startDate: '2018-05-13T00:00:00.000Z', - endDate: '2018-06-14T00:00:00.000Z', - reference: 'phase', - referenceId: 1, - createdBy: 1, - updatedBy: 1, - deletedAt: '2018-05-14T00:00:00.000Z', - }, - ])) - .then(() => { - // Create milestones - models.Milestone.bulkCreate([ - { - timelineId: 1, - name: 'milestone 1', - duration: 2, - startDate: '2018-05-03T00:00:00.000Z', - status: 'open', - type: 'type1', - details: { - detail1: { - subDetail1A: 1, - subDetail1B: 2, - }, - detail2: [1, 2, 3], - }, - order: 1, - plannedText: 'plannedText 1', - activeText: 'activeText 1', - completedText: 'completedText 1', - blockedText: 'blockedText 1', - createdBy: 1, - updatedBy: 2, - }, - { - timelineId: 1, - name: 'milestone 2', - duration: 3, - startDate: '2018-05-04T00:00:00.000Z', - status: 'open', - type: 'type2', - order: 2, - plannedText: 'plannedText 2', - activeText: 'activeText 2', - completedText: 'completedText 2', - blockedText: 'blockedText 2', - createdBy: 2, - updatedBy: 3, - }, - { - timelineId: 1, - name: 'milestone 3', - duration: 4, - startDate: '2018-05-04T00:00:00.000Z', - status: 'open', - type: 'type3', - order: 3, - plannedText: 'plannedText 3', - activeText: 'activeText 3', - completedText: 'completedText 3', - blockedText: 'blockedText 3', - createdBy: 3, - updatedBy: 4, - }, - ]) - .then(() => done()); - }); - }); - }); - }); - - after(testUtil.clearDb); - - describe('POST /timelines/{timelineId}/milestones', () => { - const body = { - param: { - name: 'milestone 4', - description: 'description 4', - duration: 4, - startDate: '2018-05-05T00:00:00.000Z', - endDate: '2018-05-07T00:00:00.000Z', - completionDate: '2018-05-08T00:00:00.000Z', - status: 'open', - type: 'type4', - details: { - detail1: { - subDetail1C: 4, - }, - detail2: [ - 3, - 4, - 5, - ], - }, - order: 2, - plannedText: 'plannedText 4', - activeText: 'activeText 4', - completedText: 'completedText 4', - blockedText: 'blockedText 4', - }, - }; - - it('should return 403 if user is not authenticated', (done) => { - request(server) - .post('/v4/timelines/1/milestones') - .send(body) - .expect(403, done); - }); - - it('should return 403 for member who is not in the project', (done) => { - request(server) - .post('/v4/timelines/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.member2}`, - }) - .send(body) - .expect(403, done); - }); - - it('should return 422 if missing name', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - name: undefined, - }), - }; - - request(server) - .post('/v4/timelines/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if missing duration', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - duration: undefined, - }), - }; - - request(server) - .post('/v4/timelines/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if missing type', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - type: undefined, - }), - }; - - request(server) - .post('/v4/timelines/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if missing order', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - order: undefined, - }), - }; - - request(server) - .post('/v4/timelines/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if missing plannedText', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - plannedText: undefined, - }), - }; - - request(server) - .post('/v4/timelines/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if missing activeText', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - activeText: undefined, - }), - }; - - request(server) - .post('/v4/timelines/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if missing completedText', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - completedText: undefined, - }), - }; - - request(server) - .post('/v4/timelines/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if missing blockedText', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - blockedText: undefined, - }), - }; - - request(server) - .post('/v4/timelines/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if startDate is after endDate', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - startDate: '2018-05-29T00:00:00.000Z', - endDate: '2018-05-28T00:00:00.000Z', - }), - }; - - request(server) - .post('/v4/timelines/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if startDate is after completionDate', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - startDate: '2018-05-29T00:00:00.000Z', - completionDate: '2018-05-28T00:00:00.000Z', - }), - }; - - request(server) - .post('/v4/timelines/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if startDate is before the timeline startDate', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - startDate: '2018-05-01T00:00:00.000Z', - }), - }; - - request(server) - .post('/v4/timelines/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if endDate is after the timeline endDate', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - endDate: '2018-06-13T00:00:00.000Z', - }), - }; - - request(server) - .post('/v4/timelines/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if invalid timelineId param', (done) => { - request(server) - .post('/v4/timelines/0/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(body) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 404 if timeline does not exist', (done) => { - request(server) - .post('/v4/timelines/1000/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(body) - .expect('Content-Type', /json/) - .expect(404, done); - }); - - it('should return 404 if timeline was deleted', (done) => { - request(server) - .post('/v4/timelines/3/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(body) - .expect('Content-Type', /json/) - .expect(404, done); - }); - - it('should return 201 for admin', (done) => { - request(server) - .post('/v4/timelines/1/milestones') - .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.description.should.be.eql(body.param.description); - resJson.duration.should.be.eql(body.param.duration); - resJson.startDate.should.be.eql(body.param.startDate); - resJson.endDate.should.be.eql(body.param.endDate); - resJson.completionDate.should.be.eql(body.param.completionDate); - resJson.status.should.be.eql(body.param.status); - resJson.type.should.be.eql(body.param.type); - resJson.details.should.be.eql(body.param.details); - resJson.order.should.be.eql(body.param.order); - resJson.plannedText.should.be.eql(body.param.plannedText); - resJson.activeText.should.be.eql(body.param.activeText); - resJson.completedText.should.be.eql(body.param.completedText); - resJson.blockedText.should.be.eql(body.param.blockedText); - - 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); - - // eslint-disable-next-line no-unused-expressions - server.services.pubsub.publish.calledWith(EVENT.ROUTING_KEY.MILESTONE_ADDED).should.be.true; - - // Verify 'order' of the other milestones - models.Milestone.findAll({ where: { timelineId: 1 } }) - .then((milestones) => { - _.each(milestones, (milestone) => { - if (milestone.id === 1) { - milestone.order.should.be.eql(1); - } else if (milestone.id === 2) { - milestone.order.should.be.eql(2 + 1); - } else if (milestone.id === 3) { - milestone.order.should.be.eql(3 + 1); - } - }); - - done(); - }); - }); - }); - - it('should return 201 for connect manager', (done) => { - request(server) - .post('/v4/timelines/1/milestones') - .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/timelines/1/milestones') - .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(); - }); - }); - - it('should return 201 for copilot', (done) => { - request(server) - .post('/v4/timelines/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .send(body) - .expect('Content-Type', /json/) - .expect(201) - .end((err, res) => { - const resJson = res.body.result.content; - resJson.createdBy.should.be.eql(40051332); // copilot - resJson.updatedBy.should.be.eql(40051332); // copilot - done(); - }); - }); - - it('should return 201 for member', (done) => { - request(server) - .post('/v4/timelines/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.member}`, - }) - .send(body) - .expect('Content-Type', /json/) - .expect(201) - .end((err, res) => { - const resJson = res.body.result.content; - resJson.createdBy.should.be.eql(40051331); // member - resJson.updatedBy.should.be.eql(40051331); // member - done(); - }); - }); - }); -}); diff --git a/src/routes/milestones/delete.js b/src/routes/milestones/delete.js deleted file mode 100644 index f7074cc0..00000000 --- a/src/routes/milestones/delete.js +++ /dev/null @@ -1,65 +0,0 @@ -/** - * API to delete a timeline - */ -import validate from 'express-validation'; -import Joi from 'joi'; -import { middleware as tcMiddleware } from 'tc-core-library-js'; -import models from '../../models'; -import { EVENT } from '../../constants'; -import util from '../../util'; - -const permissions = tcMiddleware.permissions; - -const schema = { - params: { - timelineId: Joi.number().integer().positive().required(), - milestoneId: Joi.number().integer().positive().required(), - }, -}; - -module.exports = [ - validate(schema), - // Validate and get projectId from the timelineId param, and set to request params for - // checking by the permissions middleware - util.validateTimelineIdParam, - permissions('milestone.delete'), - (req, res, next) => { - const where = { - timelineId: req.params.timelineId, - id: req.params.milestoneId, - }; - - return models.sequelize.transaction(tx => - // Find the milestone - models.Milestone.findOne({ - where, - transaction: tx, - }) - .then((milestone) => { - // Not found - if (!milestone) { - const apiErr = new Error(`Milestone not found for milestone id ${req.params.milestoneId}`); - apiErr.status = 404; - return Promise.reject(apiErr); - } - - // Update the deletedBy, and soft delete - return milestone.update({ deletedBy: req.authUser.userId }, { transaction: tx }) - .then(() => milestone.destroy({ transaction: tx })); - }) - .then((deleted) => { - // Send event to bus - req.log.debug('Sending event to RabbitMQ bus for milestone %d', deleted.id); - req.app.services.pubsub.publish(EVENT.ROUTING_KEY.MILESTONE_REMOVED, - deleted, - { correlationId: req.id }, - ); - - // Write to response - res.status(204).end(); - return Promise.resolve(); - }) - .catch(next), - ); - }, -]; diff --git a/src/routes/milestones/delete.spec.js b/src/routes/milestones/delete.spec.js deleted file mode 100644 index 21502333..00000000 --- a/src/routes/milestones/delete.spec.js +++ /dev/null @@ -1,325 +0,0 @@ -/** - * Tests for delete.js - */ -import request from 'supertest'; - -import models from '../../models'; -import server from '../../app'; -import testUtil from '../../tests/util'; -import { EVENT } from '../../constants'; - - -describe('DELETE milestone', () => { - beforeEach((done) => { - testUtil.clearDb() - .then(() => { - models.Project.bulkCreate([ - { - type: 'generic', - billingAccountId: 1, - name: 'test1', - description: 'test project1', - status: 'draft', - details: {}, - createdBy: 1, - updatedBy: 1, - }, - { - type: 'generic', - billingAccountId: 2, - name: 'test2', - description: 'test project2', - status: 'draft', - details: {}, - createdBy: 2, - updatedBy: 2, - deletedAt: '2018-05-15T00:00:00Z', - }, - ]) - .then(() => { - // Create member - models.ProjectMember.bulkCreate([ - { - userId: 40051332, - projectId: 1, - role: 'copilot', - isPrimary: true, - createdBy: 1, - updatedBy: 1, - }, - { - userId: 40051331, - projectId: 1, - role: 'customer', - isPrimary: true, - createdBy: 1, - updatedBy: 1, - }, - ]).then(() => - // Create phase - models.ProjectPhase.bulkCreate([ - { - projectId: 1, - name: 'test project phase 1', - status: 'active', - startDate: '2018-05-15T00:00:00Z', - endDate: '2018-05-15T12:00:00Z', - budget: 20.0, - progress: 1.23456, - details: { - message: 'This can be any json 2', - }, - createdBy: 1, - updatedBy: 1, - }, - { - projectId: 2, - name: 'test project phase 2', - status: 'active', - startDate: '2018-05-16T00:00:00Z', - endDate: '2018-05-16T12:00:00Z', - budget: 21.0, - progress: 1.234567, - details: { - message: 'This can be any json 2', - }, - createdBy: 2, - updatedBy: 2, - deletedAt: '2018-05-15T00:00:00Z', - }, - ])) - .then(() => - // Create timelines - models.Timeline.bulkCreate([ - { - name: 'name 1', - description: 'description 1', - startDate: '2018-05-11T00:00:00.000Z', - endDate: '2018-05-12T00:00:00.000Z', - reference: 'project', - referenceId: 1, - createdBy: 1, - updatedBy: 1, - }, - { - name: 'name 2', - description: 'description 2', - startDate: '2018-05-12T00:00:00.000Z', - endDate: '2018-05-13T00:00:00.000Z', - reference: 'phase', - referenceId: 1, - createdBy: 1, - updatedBy: 1, - }, - { - name: 'name 3', - description: 'description 3', - startDate: '2018-05-13T00:00:00.000Z', - endDate: '2018-05-14T00:00:00.000Z', - reference: 'phase', - referenceId: 1, - createdBy: 1, - updatedBy: 1, - deletedAt: '2018-05-14T00:00:00.000Z', - }, - ])) - .then(() => { - // Create milestones - models.Milestone.bulkCreate([ - { - timelineId: 1, - name: 'milestone 1', - duration: 2, - startDate: '2018-05-03T00:00:00.000Z', - status: 'open', - type: 'type1', - details: { - detail1: { - subDetail1A: 1, - subDetail1B: 2, - }, - detail2: [1, 2, 3], - }, - order: 1, - plannedText: 'plannedText 1', - activeText: 'activeText 1', - completedText: 'completedText 1', - blockedText: 'blockedText 1', - createdBy: 1, - updatedBy: 2, - }, - { - timelineId: 1, - name: 'milestone 2', - duration: 3, - startDate: '2018-05-04T00:00:00.000Z', - status: 'open', - type: 'type2', - order: 2, - plannedText: 'plannedText 2', - activeText: 'activeText 2', - completedText: 'completedText 2', - blockedText: 'blockedText 2', - createdBy: 2, - updatedBy: 3, - }, - { - timelineId: 1, - name: 'milestone 3', - duration: 4, - startDate: '2018-05-04T00:00:00.000Z', - status: 'open', - type: 'type3', - order: 3, - plannedText: 'plannedText 3', - activeText: 'activeText 3', - completedText: 'completedText 3', - blockedText: 'blockedText 3', - createdBy: 3, - updatedBy: 4, - deletedBy: 1, - deletedAt: '2018-05-04T00:00:00.000Z', - }, - ]) - .then(() => done()); - }); - }); - }); - }); - - after(testUtil.clearDb); - - describe('DELETE /timelines/{timelineId}/milestones/{milestoneId}', () => { - it('should return 403 if user is not authenticated', (done) => { - request(server) - .delete('/v4/timelines/1/milestones/1') - .expect(403, done); - }); - - it('should return 403 for member who is not in the project', (done) => { - request(server) - .delete('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.member2}`, - }) - .expect(403, done); - }); - - it('should return 403 for member who is not in the project (timeline refers to a phase)', (done) => { - request(server) - .delete('/v4/timelines/2/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.member2}`, - }) - .expect(403, done); - }); - - it('should return 404 for non-existed timeline', (done) => { - request(server) - .delete('/v4/timelines/1234/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(404, done); - }); - - it('should return 404 for deleted timeline', (done) => { - request(server) - .delete('/v4/timelines/3/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(404, done); - }); - - it('should return 404 for non-existed milestone', (done) => { - request(server) - .delete('/v4/timelines/1/milestones/100') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(404, done); - }); - - it('should return 404 for deleted milestone', (done) => { - request(server) - .delete('/v4/timelines/1/milestones/3') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(404, done); - }); - - it('should return 422 for invalid timelineId param', (done) => { - request(server) - .delete('/v4/timelines/0/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(422, done); - }); - - it('should return 422 for invalid milestoneId param', (done) => { - request(server) - .delete('/v4/timelines/1/milestones/0') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(422, done); - }); - - it('should return 204, for admin, if timeline was successfully removed', (done) => { - request(server) - .delete('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(204) - .end(() => { - // eslint-disable-next-line no-unused-expressions - server.services.pubsub.publish.calledWith(EVENT.ROUTING_KEY.MILESTONE_REMOVED).should.be.true; - done(); - }); - }); - - it('should return 204, for connect admin, if timeline was successfully removed', (done) => { - request(server) - .delete('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, - }) - .expect(204) - .end(done); - }); - - it('should return 204, for connect manager, if timeline was successfully removed', (done) => { - request(server) - .delete('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, - }) - .expect(204) - .end(done); - }); - - it('should return 204, for copilot, if timeline was successfully removed', (done) => { - request(server) - .delete('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .expect(204) - .end(done); - }); - - it('should return 204, for member, if timeline was successfully removed', (done) => { - request(server) - .delete('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.member}`, - }) - .expect(204) - .end(done); - }); - }); -}); diff --git a/src/routes/milestones/get.js b/src/routes/milestones/get.js deleted file mode 100644 index c35a3e86..00000000 --- a/src/routes/milestones/get.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * API to get a milestone - */ -import validate from 'express-validation'; -import Joi from 'joi'; -import _ from 'lodash'; -import { middleware as tcMiddleware } from 'tc-core-library-js'; -import util from '../../util'; -import models from '../../models'; - -const permissions = tcMiddleware.permissions; - -const schema = { - params: { - timelineId: Joi.number().integer().positive().required(), - milestoneId: Joi.number().integer().positive().required(), - }, -}; - -module.exports = [ - validate(schema), - // Validate and get projectId from the timelineId param, and set to request params for - // checking by the permissions middleware - util.validateTimelineIdParam, - permissions('milestone.view'), - (req, res, next) => { - const where = { - timelineId: req.params.timelineId, - id: req.params.milestoneId, - }; - - // Find the milestone - models.Milestone.findOne({ where }) - .then((milestone) => { - // Not found - if (!milestone) { - const apiErr = new Error(`Milestone not found for milestone id ${req.params.milestoneId}`); - apiErr.status = 404; - return Promise.reject(apiErr); - } - - // Write to response - res.json(util.wrapResponse(req.id, _.omit(milestone.toJSON(), ['deletedBy', 'deletedAt']))); - return Promise.resolve(); - }) - .catch(next); - }, -]; diff --git a/src/routes/milestones/get.spec.js b/src/routes/milestones/get.spec.js deleted file mode 100644 index 919b756d..00000000 --- a/src/routes/milestones/get.spec.js +++ /dev/null @@ -1,342 +0,0 @@ -/** - * 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 milestone', () => { - before((done) => { - testUtil.clearDb() - .then(() => { - models.Project.bulkCreate([ - { - type: 'generic', - billingAccountId: 1, - name: 'test1', - description: 'test project1', - status: 'draft', - details: {}, - createdBy: 1, - updatedBy: 1, - }, - { - type: 'generic', - billingAccountId: 2, - name: 'test2', - description: 'test project2', - status: 'draft', - details: {}, - createdBy: 2, - updatedBy: 2, - deletedAt: '2018-05-15T00:00:00Z', - }, - ]) - .then(() => { - // Create member - models.ProjectMember.bulkCreate([ - { - userId: 40051332, - projectId: 1, - role: 'copilot', - isPrimary: true, - createdBy: 1, - updatedBy: 1, - }, - { - userId: 40051331, - projectId: 1, - role: 'customer', - isPrimary: true, - createdBy: 1, - updatedBy: 1, - }, - ]) - .then(() => - // Create phase - models.ProjectPhase.bulkCreate([ - { - projectId: 1, - name: 'test project phase 1', - status: 'active', - startDate: '2018-05-15T00:00:00Z', - endDate: '2018-05-15T12:00:00Z', - budget: 20.0, - progress: 1.23456, - details: { - message: 'This can be any json 2', - }, - createdBy: 1, - updatedBy: 1, - }, - { - projectId: 2, - name: 'test project phase 2', - status: 'active', - startDate: '2018-05-16T00:00:00Z', - endDate: '2018-05-16T12:00:00Z', - budget: 21.0, - progress: 1.234567, - details: { - message: 'This can be any json 2', - }, - createdBy: 2, - updatedBy: 2, - deletedAt: '2018-05-15T00:00:00Z', - }, - ])) - .then(() => - // Create timelines - models.Timeline.bulkCreate([ - { - name: 'name 1', - description: 'description 1', - startDate: '2018-05-11T00:00:00.000Z', - endDate: '2018-05-12T00:00:00.000Z', - reference: 'project', - referenceId: 1, - createdBy: 1, - updatedBy: 1, - }, - { - name: 'name 2', - description: 'description 2', - startDate: '2018-05-12T00:00:00.000Z', - endDate: '2018-05-13T00:00:00.000Z', - reference: 'phase', - referenceId: 1, - createdBy: 1, - updatedBy: 1, - }, - { - name: 'name 3', - description: 'description 3', - startDate: '2018-05-13T00:00:00.000Z', - endDate: '2018-05-14T00:00:00.000Z', - reference: 'phase', - referenceId: 1, - createdBy: 1, - updatedBy: 1, - deletedAt: '2018-05-14T00:00:00.000Z', - }, - ])) - .then(() => { - // Create milestones - models.Milestone.bulkCreate([ - { - timelineId: 1, - name: 'milestone 1', - duration: 2, - startDate: '2018-05-03T00:00:00.000Z', - status: 'open', - type: 'type1', - details: { - detail1: { - subDetail1A: 1, - subDetail1B: 2, - }, - detail2: [1, 2, 3], - }, - order: 1, - plannedText: 'plannedText 1', - activeText: 'activeText 1', - completedText: 'completedText 1', - blockedText: 'blockedText 1', - createdBy: 1, - updatedBy: 2, - }, - { - timelineId: 1, - name: 'milestone 2', - duration: 3, - startDate: '2018-05-04T00:00:00.000Z', - status: 'open', - type: 'type2', - order: 2, - plannedText: 'plannedText 2', - activeText: 'activeText 2', - completedText: 'completedText 2', - blockedText: 'blockedText 2', - createdBy: 2, - updatedBy: 3, - }, - { - timelineId: 1, - name: 'milestone 3', - duration: 4, - startDate: '2018-05-04T00:00:00.000Z', - status: 'open', - type: 'type3', - order: 3, - plannedText: 'plannedText 3', - activeText: 'activeText 3', - completedText: 'completedText 3', - blockedText: 'blockedText 3', - createdBy: 3, - updatedBy: 4, - deletedBy: 1, - deletedAt: '2018-05-04T00:00:00.000Z', - }, - ]) - .then(() => done()); - }); - }); - }); - }); - - after(testUtil.clearDb); - - describe('GET /timelines/{timelineId}/milestones/{milestoneId}', () => { - it('should return 403 if user is not authenticated', (done) => { - request(server) - .get('/v4/timelines/1/milestones/1') - .expect(403, done); - }); - - it('should return 403 for member who is not in the project', (done) => { - request(server) - .get('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.member2}`, - }) - .expect(403, done); - }); - - it('should return 404 for non-existed timeline', (done) => { - request(server) - .get('/v4/timelines/1234/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(404, done); - }); - - it('should return 404 for deleted timeline', (done) => { - request(server) - .get('/v4/timelines/3/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(404, done); - }); - - it('should return 404 for non-existed milestone', (done) => { - request(server) - .get('/v4/timelines/1/milestones/1234') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(404, done); - }); - - it('should return 404 for deleted milestone', (done) => { - request(server) - .get('/v4/timelines/1/milestones/3') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(404, done); - }); - - it('should return 422 for invalid timelineId param', (done) => { - request(server) - .get('/v4/timelines/0/milestones/3') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(422, done); - }); - - it('should return 422 for invalid milestoneId param', (done) => { - request(server) - .get('/v4/timelines/1/milestones/0') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(422, done); - }); - - it('should return 200 for admin', (done) => { - request(server) - .get('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(200) - .end((err, res) => { - const resJson = res.body.result.content; - resJson.id.should.be.eql(1); - resJson.timelineId.should.be.eql(1); - resJson.name.should.be.eql('milestone 1'); - resJson.duration.should.be.eql(2); - resJson.startDate.should.be.eql('2018-05-03T00:00:00.000Z'); - resJson.status.should.be.eql('open'); - resJson.type.should.be.eql('type1'); - resJson.details.should.be.eql({ - detail1: { - subDetail1A: 1, - subDetail1B: 2, - }, - detail2: [1, 2, 3], - }); - resJson.order.should.be.eql(1); - resJson.plannedText.should.be.eql('plannedText 1'); - resJson.activeText.should.be.eql('activeText 1'); - resJson.completedText.should.be.eql('completedText 1'); - resJson.blockedText.should.be.eql('blockedText 1'); - - resJson.createdBy.should.be.eql(1); - should.exist(resJson.createdAt); - resJson.updatedBy.should.be.eql(2); - 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/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, - }) - .expect(200) - .end(done); - }); - - it('should return 200 for connect manager', (done) => { - request(server) - .get('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, - }) - .expect(200) - .end(done); - }); - - it('should return 200 for member', (done) => { - request(server) - .get('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.member}`, - }) - .expect(200, done); - }); - - it('should return 200 for copilot', (done) => { - request(server) - .get('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .expect(200, done); - }); - }); -}); diff --git a/src/routes/milestones/list.js b/src/routes/milestones/list.js deleted file mode 100644 index 6ae2d5c2..00000000 --- a/src/routes/milestones/list.js +++ /dev/null @@ -1,68 +0,0 @@ -/** - * API to list all milestones - */ -import validate from 'express-validation'; -import Joi from 'joi'; -import config from 'config'; -import _ from 'lodash'; -import { middleware as tcMiddleware } from 'tc-core-library-js'; -import util from '../../util'; - -const permissions = tcMiddleware.permissions; - -const ES_TIMELINE_INDEX = config.get('elasticsearchConfig.timelineIndexName'); -const ES_TIMELINE_TYPE = config.get('elasticsearchConfig.timelineDocType'); - -const schema = { - params: { - timelineId: Joi.number().integer().positive().required(), - }, -}; - -module.exports = [ - validate(schema), - // Validate and get projectId from the timelineId param, and set to request params for - // checking by the permissions middleware - util.validateTimelineIdParam, - permissions('milestone.view'), - (req, res, next) => { - // Parse the sort query - let sort = req.query.sort ? decodeURIComponent(req.query.sort) : 'order'; - if (sort && sort.indexOf(' ') === -1) { - sort += ' asc'; - } - const sortableProps = [ - 'order asc', 'order desc', - ]; - if (sort && _.indexOf(sortableProps, sort) < 0) { - const apiErr = new Error('Invalid sort criteria'); - apiErr.status = 422; - return next(apiErr); - } - const sortColumnAndOrder = sort.split(' '); - - // Get timeline from ES - return util.getElasticSearchClient().get({ - index: ES_TIMELINE_INDEX, - type: ES_TIMELINE_TYPE, - id: req.params.timelineId, - }) - .then((doc) => { - if (!doc) { - const err = new Error(`Timeline not found for timeline id ${req.params.timelineId}`); - err.status = 404; - throw err; - } - - // Get the milestones - let milestones = _.isArray(doc._source.milestones) ? doc._source.milestones : []; // eslint-disable-line no-underscore-dangle - - // Sort - milestones = _.orderBy(milestones, [sortColumnAndOrder[0]], [sortColumnAndOrder[1]]); - - // Write to response - res.json(util.wrapResponse(req.id, milestones, milestones.length)); - }) - .catch(err => next(err)); - }, -]; diff --git a/src/routes/milestones/list.spec.js b/src/routes/milestones/list.spec.js deleted file mode 100644 index 0240ee43..00000000 --- a/src/routes/milestones/list.spec.js +++ /dev/null @@ -1,324 +0,0 @@ -/** - * Tests for list.js - */ -import chai from 'chai'; -import request from 'supertest'; -import sleep from 'sleep'; -import config from 'config'; - -import models from '../../models'; -import server from '../../app'; -import testUtil from '../../tests/util'; - -const ES_TIMELINE_INDEX = config.get('elasticsearchConfig.timelineIndexName'); -const ES_TIMELINE_TYPE = config.get('elasticsearchConfig.timelineDocType'); - -// eslint-disable-next-line no-unused-vars -const should = chai.should(); - -const timelines = [ - { - id: 1, - name: 'name 1', - description: 'description 1', - startDate: '2018-05-11T00:00:00.000Z', - endDate: '2018-05-12T00:00:00.000Z', - reference: 'project', - referenceId: 1, - createdBy: 1, - updatedBy: 1, - createdAt: '2018-05-11T00:00:00.000Z', - updatedAt: '2018-05-11T00:00:00.000Z', - }, -]; -const milestones = [ - { - id: 1, - timelineId: 1, - name: 'milestone 1', - duration: 2, - startDate: '2018-05-03T00:00:00.000Z', - endDate: '2018-05-04T00:00:00.000Z', - completionDate: '2018-05-05T00:00:00.000Z', - status: 'open', - type: 'type1', - details: { - detail1: { - subDetail1A: 1, - subDetail1B: 2, - }, - detail2: [1, 2, 3], - }, - order: 1, - plannedText: 'plannedText 1', - activeText: 'activeText 1', - completedText: 'completedText 1', - blockedText: 'blockedText 1', - createdBy: 1, - updatedBy: 2, - createdAt: '2018-05-11T00:00:00.000Z', - updatedAt: '2018-05-11T00:00:00.000Z', - }, - { - id: 2, - timelineId: 1, - name: 'milestone 2', - duration: 3, - startDate: '2018-05-04T00:00:00.000Z', - status: 'open', - type: 'type2', - order: 2, - plannedText: 'plannedText 2', - activeText: 'activeText 2', - completedText: 'completedText 2', - blockedText: 'blockedText 2', - createdBy: 2, - updatedBy: 3, - createdAt: '2018-05-11T00:00:00.000Z', - updatedAt: '2018-05-11T00:00:00.000Z', - }, -]; - -describe('LIST timelines', () => { - before(function beforeHook(done) { - this.timeout(10000); - testUtil.clearDb() - .then(() => { - models.Project.bulkCreate([ - { - type: 'generic', - billingAccountId: 1, - name: 'test1', - description: 'test project1', - status: 'draft', - details: {}, - createdBy: 1, - updatedBy: 1, - }, - { - type: 'generic', - billingAccountId: 2, - name: 'test2', - description: 'test project2', - status: 'draft', - details: {}, - createdBy: 2, - updatedBy: 2, - deletedAt: '2018-05-15T00:00:00Z', - }, - ]) - .then(() => { - // Create member - models.ProjectMember.bulkCreate([ - { - userId: 40051332, - projectId: 1, - role: 'copilot', - isPrimary: true, - createdBy: 1, - updatedBy: 1, - }, - { - userId: 40051331, - projectId: 1, - role: 'customer', - isPrimary: true, - createdBy: 1, - updatedBy: 1, - }, - ]).then(() => - // Create phase - models.ProjectPhase.bulkCreate([ - { - projectId: 1, - name: 'test project phase 1', - status: 'active', - startDate: '2018-05-15T00:00:00Z', - endDate: '2018-05-15T12:00:00Z', - budget: 20.0, - progress: 1.23456, - details: { - message: 'This can be any json 2', - }, - createdBy: 1, - updatedBy: 1, - }, - { - projectId: 2, - name: 'test project phase 2', - status: 'active', - startDate: '2018-05-16T00:00:00Z', - endDate: '2018-05-16T12:00:00Z', - budget: 21.0, - progress: 1.234567, - details: { - message: 'This can be any json 2', - }, - createdBy: 2, - updatedBy: 2, - }, - ])) - .then(() => - // Create timelines and milestones - models.Timeline.bulkCreate(timelines) - .then(() => models.Milestone.bulkCreate(milestones))) - .then(() => { - // Index to ES - timelines[0].milestones = milestones; - timelines[0].projectId = 1; - return server.services.es.index({ - index: ES_TIMELINE_INDEX, - type: ES_TIMELINE_TYPE, - id: timelines[0].id, - body: timelines[0], - }) - .then(() => { - // sleep for some time, let elasticsearch indices be settled - sleep.sleep(5); - done(); - }); - }); - }); - }); - }); - - after(testUtil.clearDb); - - describe('GET /timelines/{timelineId}/milestones', () => { - it('should return 403 if user is not authenticated', (done) => { - request(server) - .get('/v4/timelines') - .expect(403, done); - }); - - it('should return 403 for member with no accessible project', (done) => { - request(server) - .get('/v4/timelines/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.member2}`, - }) - .expect(403, done); - }); - - it('should return 404 for not-existed timeline', (done) => { - request(server) - .get('/v4/timelines/11/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, - }) - .expect(404, done); - }); - - it('should return 422 for invalid sort column', (done) => { - request(server) - .get('/v4/timelines/1/milestones?sort=id%20asc') - .set({ - Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, - }) - .expect(422, done); - }); - - it('should return 422 for invalid sort order', (done) => { - request(server) - .get('/v4/timelines/1/milestones?sort=order%20invalid') - .set({ - Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, - }) - .expect(422, done); - }); - - it('should return 200 for admin', (done) => { - request(server) - .get('/v4/timelines/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(200) - .end((err, res) => { - const resJson = res.body.result.content; - resJson.should.have.length(2); - - resJson[0].should.be.eql(milestones[0]); - resJson[1].should.be.eql(milestones[1]); - - done(); - }); - }); - - it('should return 200 for connect admin', (done) => { - request(server) - .get('/v4/timelines/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, - }) - .expect(200) - .end((err, res) => { - const resJson = res.body.result.content; - resJson.should.have.length(2); - - done(); - }); - }); - - it('should return 200 for connect manager', (done) => { - request(server) - .get('/v4/timelines/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, - }) - .expect(200) - .end((err, res) => { - const resJson = res.body.result.content; - resJson.should.have.length(2); - - done(); - }); - }); - - it('should return 200 for member', (done) => { - request(server) - .get('/v4/timelines/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.member}`, - }) - .end((err, res) => { - const resJson = res.body.result.content; - resJson.should.have.length(2); - - done(); - }); - }); - - it('should return 200 for copilot', (done) => { - request(server) - .get('/v4/timelines/1/milestones') - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .end((err, res) => { - const resJson = res.body.result.content; - resJson.should.have.length(2); - - done(); - }); - }); - - it('should return 200 with sort by order desc', (done) => { - request(server) - .get('/v4/timelines/1/milestones?sort=order%20desc') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(200) - .end((err, res) => { - const resJson = res.body.result.content; - resJson.should.have.length(2); - - resJson[0].should.be.eql(milestones[1]); - resJson[1].should.be.eql(milestones[0]); - - done(); - }); - }); - }); -}); diff --git a/src/routes/milestones/update.js b/src/routes/milestones/update.js deleted file mode 100644 index 5641d290..00000000 --- a/src/routes/milestones/update.js +++ /dev/null @@ -1,161 +0,0 @@ -/** - * API to update a milestone - */ -import validate from 'express-validation'; -import _ from 'lodash'; -import Joi from 'joi'; -import Sequelize from 'sequelize'; -import { middleware as tcMiddleware } from 'tc-core-library-js'; -import util from '../../util'; -import { EVENT } from '../../constants'; -import models from '../../models'; - -const permissions = tcMiddleware.permissions; - -const schema = { - params: { - timelineId: Joi.number().integer().positive().required(), - milestoneId: Joi.number().integer().positive().required(), - }, - body: { - param: Joi.object().keys({ - id: Joi.any().strip(), - name: Joi.string().max(255).required(), - description: Joi.string().max(255), - duration: Joi.number().integer().required(), - startDate: Joi.date().required(), - endDate: Joi.date().min(Joi.ref('startDate')).allow(null), - completionDate: Joi.date().min(Joi.ref('startDate')).allow(null), - status: Joi.string().max(45).required(), - type: Joi.string().max(45).required(), - details: Joi.object(), - order: Joi.number().integer().required(), - plannedText: Joi.string().max(512).required(), - activeText: Joi.string().max(512).required(), - completedText: Joi.string().max(512).required(), - blockedText: Joi.string().max(512).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), - // Validate and get projectId from the timelineId param, - // and set to request params for checking by the permissions middleware - util.validateTimelineIdParam, - permissions('milestone.edit'), - (req, res, next) => { - const where = { - timelineId: req.params.timelineId, - id: req.params.milestoneId, - }; - const entityToUpdate = _.assign(req.body.param, { - updatedBy: req.authUser.userId, - timelineId: req.params.timelineId, - }); - - // Validate startDate and endDate to be within the timeline startDate and endDate - let error; - if (req.body.param.startDate < req.timeline.startDate) { - error = 'Milestone startDate must not be before the timeline startDate'; - } else if (req.body.param.endDate && req.timeline.endDate && req.body.param.endDate > req.timeline.endDate) { - error = 'Milestone endDate must not be after the timeline endDate'; - } - if (error) { - const apiErr = new Error(error); - apiErr.status = 422; - return next(apiErr); - } - - let original; - let updated; - - return models.sequelize.transaction(() => - // Find the milestone - models.Milestone.findOne({ where }) - .then((milestone) => { - // Not found - if (!milestone) { - const apiErr = new Error(`Milestone not found for milestone id ${req.params.milestoneId}`); - apiErr.status = 404; - return Promise.reject(apiErr); - } - - original = _.omit(milestone.toJSON(), ['deletedAt', 'deletedBy']); - - // Merge JSON fields - entityToUpdate.details = util.mergeJsonObjects(milestone.details, entityToUpdate.details); - - // Update - return milestone.update(entityToUpdate); - }) - .then((updatedMilestone) => { - // Omit deletedAt, deletedBy - updated = _.omit(updatedMilestone.toJSON(), 'deletedAt', 'deletedBy'); - - // Update order of the other milestones only if the order was changed - if (original.order === updated.order) { - return Promise.resolve(); - } - - return models.Milestone.count({ - where: { - timelineId: updated.timelineId, - id: { $ne: updated.id }, - order: updated.order, - }, - }) - .then((count) => { - if (count === 0) { - return Promise.resolve(); - } - - // Increase the order from M to K: if there is an item with order K, - // orders from M+1 to K should be made M to K-1 - if (original.order < updated.order) { - return models.Milestone.update({ order: Sequelize.literal('"order" - 1') }, { - where: { - timelineId: updated.timelineId, - id: { $ne: updated.id }, - order: { $between: [original.order + 1, updated.order] }, - }, - }); - } - - // Decrease the order from M to K: if there is an item with order K, - // orders from K to M-1 should be made K+1 to M - return models.Milestone.update({ order: Sequelize.literal('"order" + 1') }, { - where: { - timelineId: updated.timelineId, - id: { $ne: updated.id }, - order: { $between: [updated.order, original.order - 1] }, - }, - }); - }); - }) - .then(() => { - // Send event to bus - req.log.debug('Sending event to RabbitMQ bus for milestone %d', updated.id); - req.app.services.pubsub.publish(EVENT.ROUTING_KEY.MILESTONE_UPDATED, - { original, updated }, - { correlationId: req.id }, - ); - - // Do not send events for the the other milestones (updated order) here, - // because it will make 'version conflict' error in ES. - // The order of the other milestones need to be updated in the MILESTONE_UPDATED event above - - // Write to response - res.json(util.wrapResponse(req.id, updated)); - return Promise.resolve(); - }) - .catch(next), - ); - }, -]; diff --git a/src/routes/milestones/update.spec.js b/src/routes/milestones/update.spec.js deleted file mode 100644 index fca57115..00000000 --- a/src/routes/milestones/update.spec.js +++ /dev/null @@ -1,981 +0,0 @@ -/** - * Tests for get.js - */ -import chai from 'chai'; -import request from 'supertest'; -import _ from 'lodash'; -import models from '../../models'; -import server from '../../app'; -import testUtil from '../../tests/util'; -import { EVENT } from '../../constants'; - -const should = chai.should(); - -describe('UPDATE Milestone', () => { - beforeEach((done) => { - testUtil.clearDb() - .then(() => { - models.Project.bulkCreate([ - { - type: 'generic', - billingAccountId: 1, - name: 'test1', - description: 'test project1', - status: 'draft', - details: {}, - createdBy: 1, - updatedBy: 1, - }, - { - type: 'generic', - billingAccountId: 2, - name: 'test2', - description: 'test project2', - status: 'draft', - details: {}, - createdBy: 2, - updatedBy: 2, - deletedAt: '2018-05-15T00:00:00Z', - }, - ]) - .then(() => { - // Create member - models.ProjectMember.bulkCreate([ - { - userId: 40051332, - projectId: 1, - role: 'copilot', - isPrimary: true, - createdBy: 1, - updatedBy: 1, - }, - { - userId: 40051331, - projectId: 1, - role: 'customer', - isPrimary: true, - createdBy: 1, - updatedBy: 1, - }, - ]).then(() => - // Create phase - models.ProjectPhase.bulkCreate([ - { - projectId: 1, - name: 'test project phase 1', - status: 'active', - startDate: '2018-05-15T00:00:00Z', - endDate: '2018-05-15T12:00:00Z', - budget: 20.0, - progress: 1.23456, - details: { - message: 'This can be any json 2', - }, - createdBy: 1, - updatedBy: 1, - }, - { - projectId: 2, - name: 'test project phase 2', - status: 'active', - startDate: '2018-05-16T00:00:00Z', - endDate: '2018-05-16T12:00:00Z', - budget: 21.0, - progress: 1.234567, - details: { - message: 'This can be any json 2', - }, - createdBy: 2, - updatedBy: 2, - deletedAt: '2018-05-15T00:00:00Z', - }, - ])) - .then(() => - // Create timelines - models.Timeline.bulkCreate([ - { - name: 'name 1', - description: 'description 1', - startDate: '2018-05-02T00:00:00.000Z', - endDate: '2018-06-12T00:00:00.000Z', - reference: 'project', - referenceId: 1, - createdBy: 1, - updatedBy: 1, - }, - { - name: 'name 2', - description: 'description 2', - startDate: '2018-05-12T00:00:00.000Z', - endDate: '2018-06-13T00:00:00.000Z', - reference: 'phase', - referenceId: 1, - createdBy: 1, - updatedBy: 1, - }, - { - name: 'name 3', - description: 'description 3', - startDate: '2018-05-13T00:00:00.000Z', - endDate: '2018-06-14T00:00:00.000Z', - reference: 'phase', - referenceId: 1, - createdBy: 1, - updatedBy: 1, - deletedAt: '2018-05-14T00:00:00.000Z', - }, - ]).then(() => models.Milestone.bulkCreate([ - { - id: 1, - timelineId: 1, - name: 'Milestone 1', - duration: 2, - startDate: '2018-05-13T00:00:00.000Z', - endDate: '2018-05-14T00:00:00.000Z', - completionDate: '2018-05-15T00:00:00.000Z', - status: 'open', - type: 'type1', - details: { - detail1: { - subDetail1A: 1, - subDetail1B: 2, - }, - detail2: [1, 2, 3], - }, - order: 1, - plannedText: 'plannedText 1', - activeText: 'activeText 1', - completedText: 'completedText 1', - blockedText: 'blockedText 1', - createdBy: 1, - updatedBy: 2, - createdAt: '2018-05-11T00:00:00.000Z', - updatedAt: '2018-05-11T00:00:00.000Z', - }, - { - id: 2, - timelineId: 1, - name: 'Milestone 2', - duration: 3, - startDate: '2018-05-14T00:00:00.000Z', - status: 'open', - type: 'type2', - order: 2, - plannedText: 'plannedText 2', - activeText: 'activeText 2', - completedText: 'completedText 2', - blockedText: 'blockedText 2', - createdBy: 2, - updatedBy: 3, - createdAt: '2018-05-11T00:00:00.000Z', - updatedAt: '2018-05-11T00:00:00.000Z', - }, - { - id: 3, - timelineId: 1, - name: 'Milestone 3', - duration: 3, - startDate: '2018-05-14T00:00:00.000Z', - status: 'open', - type: 'type3', - order: 3, - plannedText: 'plannedText 3', - activeText: 'activeText 3', - completedText: 'completedText 3', - blockedText: 'blockedText 3', - createdBy: 2, - updatedBy: 3, - createdAt: '2018-05-11T00:00:00.000Z', - updatedAt: '2018-05-11T00:00:00.000Z', - }, - { - id: 4, - timelineId: 1, - name: 'Milestone 4', - duration: 3, - startDate: '2018-05-14T00:00:00.000Z', - status: 'open', - type: 'type4', - order: 4, - plannedText: 'plannedText 4', - activeText: 'activeText 4', - completedText: 'completedText 4', - blockedText: 'blockedText 4', - createdBy: 2, - updatedBy: 3, - createdAt: '2018-05-11T00:00:00.000Z', - updatedAt: '2018-05-11T00:00:00.000Z', - }, - { - id: 5, - timelineId: 1, - name: 'Milestone 5', - duration: 3, - startDate: '2018-05-14T00:00:00.000Z', - status: 'open', - type: 'type5', - order: 5, - plannedText: 'plannedText 5', - activeText: 'activeText 5', - completedText: 'completedText 5', - blockedText: 'blockedText 5', - createdBy: 2, - updatedBy: 3, - createdAt: '2018-05-11T00:00:00.000Z', - updatedAt: '2018-05-11T00:00:00.000Z', - deletedAt: '2018-05-11T00:00:00.000Z', - }, - { - id: 6, - timelineId: 2, // Timeline 2 - name: 'Milestone 6', - duration: 3, - startDate: '2018-05-14T00:00:00.000Z', - status: 'open', - type: 'type5', - order: 1, - plannedText: 'plannedText 6', - activeText: 'activeText 6', - completedText: 'completedText 6', - blockedText: 'blockedText 6', - createdBy: 2, - updatedBy: 3, - createdAt: '2018-05-11T00:00:00.000Z', - updatedAt: '2018-05-11T00:00:00.000Z', - }, - ]))) - .then(() => done()); - }); - }); - }); - - after(testUtil.clearDb); - - describe('PATCH /timelines/{timelineId}/milestones/{milestoneId}', () => { - const body = { - param: { - name: 'Milestone 1-updated', - duration: 3, - startDate: '2018-05-14T00:00:00.000Z', - endDate: '2018-05-15T00:00:00.000Z', - completionDate: '2018-05-16T00:00:00.000Z', - description: 'description-updated', - status: 'closed', - type: 'type1-updated', - details: { - detail1: { - subDetail1A: 0, - subDetail1C: 3, - }, - detail2: [4], - detail3: 3, - }, - order: 1, - plannedText: 'plannedText 1-updated', - activeText: 'activeText 1-updated', - completedText: 'completedText 1-updated', - blockedText: 'blockedText 1-updated', - }, - }; - - it('should return 403 if user is not authenticated', (done) => { - request(server) - .patch('/v4/timelines/1/milestones/1') - .send(body) - .expect(403, done); - }); - - it('should return 403 for member who is not in the project', (done) => { - request(server) - .patch('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.member2}`, - }) - .send(body) - .expect(403, done); - }); - - it('should return 404 for non-existed timeline', (done) => { - request(server) - .patch('/v4/timelines/1234/milestones/1') - .send(body) - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(404, done); - }); - - it('should return 404 for deleted timeline', (done) => { - request(server) - .patch('/v4/timelines/3/milestones/1') - .send(body) - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(404, done); - }); - - it('should return 404 for non-existed Milestone', (done) => { - request(server) - .patch('/v4/timelines/1/milestones/111') - .send(body) - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(404, done); - }); - - it('should return 404 for deleted Milestone', (done) => { - request(server) - .patch('/v4/timelines/1/milestones/5') - .send(body) - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(404, done); - }); - - it('should return 422 for invalid timelineId param', (done) => { - request(server) - .patch('/v4/timelines/0/milestones/1') - .send(body) - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(422, done); - }); - - it('should return 422 for invalid milestoneId param', (done) => { - request(server) - .patch('/v4/timelines/1/milestones/0') - .send(body) - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(422, done); - }); - - - it('should return 422 if missing name', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - name: undefined, - }), - }; - - request(server) - .patch('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if missing duration', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - duration: undefined, - }), - }; - - request(server) - .patch('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if missing type', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - type: undefined, - }), - }; - - request(server) - .patch('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if missing order', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - order: undefined, - }), - }; - - request(server) - .patch('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if missing plannedText', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - plannedText: undefined, - }), - }; - - request(server) - .patch('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if missing activeText', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - activeText: undefined, - }), - }; - - request(server) - .patch('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if missing completedText', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - completedText: undefined, - }), - }; - - request(server) - .patch('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if missing blockedText', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - blockedText: undefined, - }), - }; - - request(server) - .patch('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if startDate is after endDate', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - startDate: '2018-05-29T00:00:00.000Z', - endDate: '2018-05-28T00:00:00.000Z', - }), - }; - - request(server) - .patch('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if startDate is after completionDate', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - startDate: '2018-05-29T00:00:00.000Z', - completionDate: '2018-05-28T00:00:00.000Z', - }), - }; - - request(server) - .patch('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if startDate is before timeline startDate', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - startDate: '2018-05-01T00:00:00.000Z', - }), - }; - - request(server) - .patch('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if endDate is after timeline endDate', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - endDate: '2018-07-01T00:00:00.000Z', - }), - }; - - request(server) - .patch('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 200 for admin', (done) => { - request(server) - .patch('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(body) - .expect(200) - .end((err, res) => { - const resJson = res.body.result.content; - should.exist(resJson.id); - resJson.name.should.be.eql(body.param.name); - resJson.description.should.be.eql(body.param.description); - resJson.duration.should.be.eql(body.param.duration); - resJson.startDate.should.be.eql(body.param.startDate); - resJson.endDate.should.be.eql(body.param.endDate); - resJson.completionDate.should.be.eql(body.param.completionDate); - resJson.status.should.be.eql(body.param.status); - resJson.type.should.be.eql(body.param.type); - resJson.details.should.be.eql({ - detail1: { subDetail1A: 0, subDetail1B: 2, subDetail1C: 3 }, - detail2: [4], - detail3: 3, - }); - resJson.order.should.be.eql(body.param.order); - resJson.plannedText.should.be.eql(body.param.plannedText); - resJson.activeText.should.be.eql(body.param.activeText); - resJson.completedText.should.be.eql(body.param.completedText); - resJson.blockedText.should.be.eql(body.param.blockedText); - - should.exist(resJson.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); - - // eslint-disable-next-line no-unused-expressions - server.services.pubsub.publish.calledWith(EVENT.ROUTING_KEY.MILESTONE_UPDATED).should.be.true; - - done(); - }); - }); - - // eslint-disable-next-line func-names - it('should return 200 for admin - order increases and replaces another milestone\'s order', function (done) { - this.timeout(10000); - - request(server) - .patch('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send({ param: _.assign({}, body.param, { order: 4 }) }) // 1 to 4 - .expect(200) - .end(() => { - // Milestone 1: order 4 - // Milestone 2: order 2 - 1 = 1 - // Milestone 3: order 3 - 1 = 2 - // Milestone 4: order 4 - 1 = 3 - setTimeout(() => { - models.Milestone.findById(1) - .then((milestone) => { - milestone.order.should.be.eql(4); - }) - .then(() => models.Milestone.findById(2)) - .then((milestone) => { - milestone.order.should.be.eql(1); - }) - .then(() => models.Milestone.findById(3)) - .then((milestone) => { - milestone.order.should.be.eql(2); - }) - .then(() => models.Milestone.findById(4)) - .then((milestone) => { - milestone.order.should.be.eql(3); - - done(); - }); - }, 3000); - }); - }); - - // eslint-disable-next-line func-names - it('should return 200 for admin - order increases and doesnot replace another milestone\'s order', function (done) { - this.timeout(10000); - - request(server) - .patch('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send({ param: _.assign({}, body.param, { order: 5 }) }) // 1 to 5 - .expect(200) - .end(() => { - // Milestone 1: order 5 - // Milestone 2: order 2 - // Milestone 3: order 3 - // Milestone 4: order 4 - setTimeout(() => { - models.Milestone.findById(1) - .then((milestone) => { - milestone.order.should.be.eql(5); - }) - .then(() => models.Milestone.findById(2)) - .then((milestone) => { - milestone.order.should.be.eql(2); - }) - .then(() => models.Milestone.findById(3)) - .then((milestone) => { - milestone.order.should.be.eql(3); - }) - .then(() => models.Milestone.findById(4)) - .then((milestone) => { - milestone.order.should.be.eql(4); - - done(); - }); - }, 3000); - }); - }); - - // eslint-disable-next-line func-names - it('should return 200 for admin - order decreases and replaces another milestone\'s order', function (done) { - this.timeout(10000); - - request(server) - .patch('/v4/timelines/1/milestones/4') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send({ param: _.assign({}, body.param, { order: 2 }) }) // 4 to 2 - .expect(200) - .end(() => { - // Milestone 1: order 1 - // Milestone 2: order 3 - // Milestone 3: order 4 - // Milestone 4: order 2 - setTimeout(() => { - models.Milestone.findById(1) - .then((milestone) => { - milestone.order.should.be.eql(1); - }) - .then(() => models.Milestone.findById(2)) - .then((milestone) => { - milestone.order.should.be.eql(3); - }) - .then(() => models.Milestone.findById(3)) - .then((milestone) => { - milestone.order.should.be.eql(4); - }) - .then(() => models.Milestone.findById(4)) - .then((milestone) => { - milestone.order.should.be.eql(2); - - done(); - }); - }, 3000); - }); - }); - - // eslint-disable-next-line func-names - it('should return 200 for admin - order decreases and doesnot replace another milestone\'s order', function (done) { - this.timeout(10000); - - request(server) - .patch('/v4/timelines/1/milestones/4') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send({ param: _.assign({}, body.param, { order: 0 }) }) // 4 to 0 - .expect(200) - .end(() => { - // Milestone 1: order 1 - // Milestone 2: order 2 - // Milestone 3: order 3 - // Milestone 4: order 0 - setTimeout(() => { - models.Milestone.findById(1) - .then((milestone) => { - milestone.order.should.be.eql(1); - }) - .then(() => models.Milestone.findById(2)) - .then((milestone) => { - milestone.order.should.be.eql(2); - }) - .then(() => models.Milestone.findById(3)) - .then((milestone) => { - milestone.order.should.be.eql(3); - }) - .then(() => models.Milestone.findById(4)) - .then((milestone) => { - milestone.order.should.be.eql(0); - - done(); - }); - }, 3000); - }); - }); - - // eslint-disable-next-line func-names - it('should return 200 for admin - changing order with only 1 item in list', function (done) { - this.timeout(10000); - - request(server) - .patch('/v4/timelines/2/milestones/6') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send({ param: _.assign({}, body.param, { order: 0 }) }) // 1 to 0 - .expect(200) - .end(() => { - // Milestone 6: order 0 - setTimeout(() => { - models.Milestone.findById(6) - .then((milestone) => { - milestone.order.should.be.eql(0); - - done(); - }); - }, 3000); - }); - }); - - // eslint-disable-next-line func-names - it('should return 200 for admin - changing order without changing other milestones\' orders', function (done) { - this.timeout(10000); - - models.Milestone.bulkCreate([ - { - id: 7, - timelineId: 2, // Timeline 2 - name: 'Milestone 7', - duration: 3, - startDate: '2018-05-14T00:00:00.000Z', - status: 'open', - type: 'type7', - order: 3, - plannedText: 'plannedText 7', - activeText: 'activeText 7', - completedText: 'completedText 7', - blockedText: 'blockedText 7', - createdBy: 2, - updatedBy: 3, - createdAt: '2018-05-11T00:00:00.000Z', - updatedAt: '2018-05-11T00:00:00.000Z', - }, - { - id: 8, - timelineId: 2, // Timeline 2 - name: 'Milestone 8', - duration: 3, - startDate: '2018-05-14T00:00:00.000Z', - status: 'open', - type: 'type7', - order: 4, - plannedText: 'plannedText 8', - activeText: 'activeText 8', - completedText: 'completedText 8', - blockedText: 'blockedText 8', - createdBy: 2, - updatedBy: 3, - createdAt: '2018-05-11T00:00:00.000Z', - updatedAt: '2018-05-11T00:00:00.000Z', - }, - ]) - .then(() => { - request(server) - .patch('/v4/timelines/2/milestones/8') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send({ param: _.assign({}, body.param, { order: 2 }) }) // 4 to 2 - .expect(200) - .end(() => { - // Milestone 6: order 1 => 1 - // Milestone 7: order 3 => 3 - // Milestone 8: order 4 => 2 - setTimeout(() => { - models.Milestone.findById(6) - .then((milestone) => { - milestone.order.should.be.eql(1); - }) - .then(() => models.Milestone.findById(7)) - .then((milestone) => { - milestone.order.should.be.eql(3); - }) - .then(() => models.Milestone.findById(8)) - .then((milestone) => { - milestone.order.should.be.eql(2); - - done(); - }); - }, 3000); - }); - }); - }); - - // eslint-disable-next-line func-names - it('should return 200 for admin - changing order withchanging other milestones\' orders', function (done) { - this.timeout(10000); - - models.Milestone.bulkCreate([ - { - id: 7, - timelineId: 2, // Timeline 2 - name: 'Milestone 7', - duration: 3, - startDate: '2018-05-14T00:00:00.000Z', - status: 'open', - type: 'type7', - order: 2, - plannedText: 'plannedText 7', - activeText: 'activeText 7', - completedText: 'completedText 7', - blockedText: 'blockedText 7', - createdBy: 2, - updatedBy: 3, - createdAt: '2018-05-11T00:00:00.000Z', - updatedAt: '2018-05-11T00:00:00.000Z', - }, - { - id: 8, - timelineId: 2, // Timeline 2 - name: 'Milestone 8', - duration: 3, - startDate: '2018-05-14T00:00:00.000Z', - status: 'open', - type: 'type7', - order: 4, - plannedText: 'plannedText 8', - activeText: 'activeText 8', - completedText: 'completedText 8', - blockedText: 'blockedText 8', - createdBy: 2, - updatedBy: 3, - createdAt: '2018-05-11T00:00:00.000Z', - updatedAt: '2018-05-11T00:00:00.000Z', - }, - ]) - .then(() => { - request(server) - .patch('/v4/timelines/2/milestones/8') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send({ param: _.assign({}, body.param, { order: 2 }) }) // 4 to 2 - .expect(200) - .end(() => { - // Milestone 6: order 1 => 1 - // Milestone 7: order 2 => 3 - // Milestone 8: order 4 => 2 - setTimeout(() => { - models.Milestone.findById(6) - .then((milestone) => { - milestone.order.should.be.eql(1); - }) - .then(() => models.Milestone.findById(7)) - .then((milestone) => { - milestone.order.should.be.eql(3); - }) - .then(() => models.Milestone.findById(8)) - .then((milestone) => { - milestone.order.should.be.eql(2); - - done(); - }); - }, 3000); - }); - }); - }); - - it('should return 200 for connect admin', (done) => { - request(server) - .patch('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, - }) - .send(body) - .expect(200) - .end(done); - }); - - it('should return 200 for connect manager', (done) => { - request(server) - .patch('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, - }) - .send(body) - .expect(200) - .end(done); - }); - - it('should return 200 for copilot', (done) => { - request(server) - .patch('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .send(body) - .expect(200) - .end(done); - }); - - it('should return 200 for member', (done) => { - request(server) - .patch('/v4/timelines/1/milestones/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.member}`, - }) - .send(body) - .expect(200) - .end(done); - }); - }); -}); diff --git a/src/routes/timelines/create.js b/src/routes/timelines/create.js deleted file mode 100644 index ada7beae..00000000 --- a/src/routes/timelines/create.js +++ /dev/null @@ -1,65 +0,0 @@ -/** - * API to add a timeline - */ -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'; -import { EVENT, TIMELINE_REFERENCES } from '../../constants'; - -const permissions = tcMiddleware.permissions; - -const schema = { - body: { - param: Joi.object().keys({ - id: Joi.any().strip(), - name: Joi.string().max(255).required(), - description: Joi.string().max(255), - startDate: Joi.date().required(), - endDate: Joi.date().min(Joi.ref('startDate')).allow(null), - reference: Joi.string().valid(_.values(TIMELINE_REFERENCES)).required(), - referenceId: Joi.number().integer().positive().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), - // Validate and get projectId from the timeline request body, and set to request params - // for checking by the permissions middleware - util.validateTimelineRequestBody, - permissions('timeline.create'), - (req, res, next) => { - const entity = _.assign(req.body.param, { - createdBy: req.authUser.userId, - updatedBy: req.authUser.userId, - }); - - // Save to DB - return models.Timeline.create(entity) - .then((createdEntity) => { - // Omit deletedAt, deletedBy - const result = _.omit(createdEntity.toJSON(), 'deletedAt', 'deletedBy'); - - // Send event to bus - req.log.debug('Sending event to RabbitMQ bus for timeline %d', result.id); - req.app.services.pubsub.publish(EVENT.ROUTING_KEY.TIMELINE_ADDED, - _.assign({ projectId: req.params.projectId }, result), - { correlationId: req.id }, - ); - - // Write to the response - res.status(201).json(util.wrapResponse(req.id, result, 1, 201)); - return Promise.resolve(); - }) - .catch(next); - }, -]; diff --git a/src/routes/timelines/create.spec.js b/src/routes/timelines/create.spec.js deleted file mode 100644 index 10e3adbe..00000000 --- a/src/routes/timelines/create.spec.js +++ /dev/null @@ -1,468 +0,0 @@ -/** - * Tests for create.js - */ -import chai from 'chai'; -import request from 'supertest'; -import _ from 'lodash'; -import server from '../../app'; -import testUtil from '../../tests/util'; -import models from '../../models'; -import { EVENT } from '../../constants'; - -const should = chai.should(); - -describe('CREATE timeline', () => { - let projectId1; - let projectId2; - - before((done) => { - testUtil.clearDb() - .then(() => { - models.Project.bulkCreate([ - { - type: 'generic', - billingAccountId: 1, - name: 'test1', - description: 'test project1', - status: 'draft', - details: {}, - createdBy: 1, - updatedBy: 1, - }, - { - type: 'generic', - billingAccountId: 2, - name: 'test2', - description: 'test project2', - status: 'draft', - details: {}, - createdBy: 2, - updatedBy: 2, - deletedAt: '2018-05-15T00:00:00Z', - }, - ], { returning: true }) - .then((projects) => { - projectId1 = projects[0].id; - projectId2 = projects[1].id; - - // Create member - models.ProjectMember.bulkCreate([ - { - userId: 40051332, - projectId: projectId1, - role: 'copilot', - isPrimary: true, - createdBy: 1, - updatedBy: 1, - }, - { - userId: 40051331, - projectId: projectId1, - role: 'customer', - isPrimary: true, - createdBy: 1, - updatedBy: 1, - }, - ]).then(() => - // Create phase - models.ProjectPhase.bulkCreate([ - { - projectId: projectId1, - name: 'test project phase 1', - status: 'active', - startDate: '2018-05-15T00:00:00Z', - endDate: '2018-05-15T12:00:00Z', - budget: 20.0, - progress: 1.23456, - details: { - message: 'This can be any json 2', - }, - createdBy: 1, - updatedBy: 1, - }, - { - projectId: projectId2, - name: 'test project phase 2', - status: 'active', - startDate: '2018-05-16T00:00:00Z', - endDate: '2018-05-16T12:00:00Z', - budget: 21.0, - progress: 1.234567, - details: { - message: 'This can be any json 2', - }, - createdBy: 2, - updatedBy: 2, - deletedAt: '2018-05-15T00:00:00Z', - }, - ])) - .then(() => { - done(); - }); - }); - }); - }); - - after(testUtil.clearDb); - - describe('POST /timelines', () => { - const body = { - param: { - name: 'new name', - description: 'new description', - startDate: '2018-05-29T00:00:00.000Z', - endDate: '2018-05-30T00:00:00.000Z', - reference: 'project', - referenceId: 1, - }, - }; - - it('should return 403 if user is not authenticated', (done) => { - request(server) - .post('/v4/timelines') - .send(body) - .expect(403, done); - }); - - it('should return 403 for member who is not in the project', (done) => { - request(server) - .post('/v4/timelines') - .set({ - Authorization: `Bearer ${testUtil.jwts.member2}`, - }) - .send(body) - .expect(403, done); - }); - - it('should return 403 for member who is not in the project (timeline refers to a phase)', (done) => { - const bodyWithPhase = { - param: _.assign({}, body.param, { - reference: 'phase', - referenceId: 1, - }), - }; - - request(server) - .post('/v4/timelines') - .set({ - Authorization: `Bearer ${testUtil.jwts.member2}`, - }) - .send(bodyWithPhase) - .expect(403, done); - }); - - it('should return 422 if missing name', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - name: undefined, - }), - }; - - request(server) - .post('/v4/timelines') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if missing startDate', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - startDate: undefined, - }), - }; - - request(server) - .post('/v4/timelines') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if startDate is after endDate', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - startDate: '2018-05-29T00:00:00.000Z', - endDate: '2018-05-28T00:00:00.000Z', - }), - }; - - request(server) - .post('/v4/timelines') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if missing reference', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - reference: undefined, - }), - }; - - request(server) - .post('/v4/timelines') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if missing referenceId', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - referenceId: undefined, - }), - }; - - request(server) - .post('/v4/timelines') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if invalid reference', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - reference: 'invalid', - }), - }; - - request(server) - .post('/v4/timelines') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if invalid referenceId', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - referenceId: 0, - }), - }; - - request(server) - .post('/v4/timelines') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if project does not exist', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - referenceId: 1110, - }), - }; - - request(server) - .post('/v4/timelines') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if project was deleted', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - referenceId: 2, - }), - }; - - request(server) - .post('/v4/timelines') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if phase does not exist', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - reference: 'phase', - referenceId: 2222, - }), - }; - - request(server) - .post('/v4/timelines') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if phase was deleted', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - reference: 'phase', - referenceId: 2, - }), - }; - - request(server) - .post('/v4/timelines') - .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/timelines') - .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.description.should.be.eql(body.param.description); - resJson.startDate.should.be.eql(body.param.startDate); - resJson.endDate.should.be.eql(body.param.endDate); - resJson.reference.should.be.eql(body.param.reference); - resJson.referenceId.should.be.eql(body.param.referenceId); - - 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); - - // eslint-disable-next-line no-unused-expressions - server.services.pubsub.publish.calledWith(EVENT.ROUTING_KEY.TIMELINE_ADDED).should.be.true; - - done(); - }); - }); - - it('should return 201 for connect manager', (done) => { - request(server) - .post('/v4/timelines') - .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/timelines') - .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(); - }); - }); - - it('should return 201 for copilot', (done) => { - request(server) - .post('/v4/timelines') - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .send(body) - .expect('Content-Type', /json/) - .expect(201) - .end((err, res) => { - const resJson = res.body.result.content; - resJson.createdBy.should.be.eql(40051332); // copilot - resJson.updatedBy.should.be.eql(40051332); // copilot - done(); - }); - }); - - it('should return 201 for member', (done) => { - request(server) - .post('/v4/timelines') - .set({ - Authorization: `Bearer ${testUtil.jwts.member}`, - }) - .send(body) - .expect('Content-Type', /json/) - .expect(201) - .end((err, res) => { - const resJson = res.body.result.content; - resJson.createdBy.should.be.eql(40051331); // member - resJson.updatedBy.should.be.eql(40051331); // member - done(); - }); - }); - - it('should return 201 for member (timeline refers to a phase)', (done) => { - const bodyWithPhase = _.merge({}, body, { - param: { - reference: 'phase', - referenceId: 1, - }, - }); - request(server) - .post('/v4/timelines') - .set({ - Authorization: `Bearer ${testUtil.jwts.member}`, - }) - .send(bodyWithPhase) - .expect('Content-Type', /json/) - .expect(201) - .end((err, res) => { - const resJson = res.body.result.content; - resJson.createdBy.should.be.eql(40051331); // member - resJson.updatedBy.should.be.eql(40051331); // member - done(); - }); - }); - }); -}); diff --git a/src/routes/timelines/delete.js b/src/routes/timelines/delete.js deleted file mode 100644 index e3d94bb7..00000000 --- a/src/routes/timelines/delete.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * API to delete a timeline - */ -import validate from 'express-validation'; -import Joi from 'joi'; -import _ from 'lodash'; -import { middleware as tcMiddleware } from 'tc-core-library-js'; -import models from '../../models'; -import { EVENT } from '../../constants'; -import util from '../../util'; - -const permissions = tcMiddleware.permissions; - -const schema = { - params: { - timelineId: Joi.number().integer().positive().required(), - }, -}; - -module.exports = [ - validate(schema), - // Validate and get projectId from the timelineId param, and set to request params for - // checking by the permissions middleware - util.validateTimelineIdParam, - permissions('timeline.delete'), - (req, res, next) => { - const timeline = req.timeline; - const deleted = _.omit(timeline.toJSON(), ['deletedAt', 'deletedBy']); - - return models.sequelize.transaction(() => - // Update the deletedBy, then delete - timeline.update({ deletedBy: req.authUser.userId }) - .then(() => timeline.destroy()) - // Cascade delete the milestones - .then(() => models.Milestone.update({ deletedBy: req.authUser.userId }, { where: { timelineId: timeline.id } })) - .then(() => models.Milestone.destroy({ where: { timelineId: timeline.id } })) - .then(() => { - // Send event to bus - req.log.debug('Sending event to RabbitMQ bus for timeline %d', deleted.id); - req.app.services.pubsub.publish(EVENT.ROUTING_KEY.TIMELINE_REMOVED, - deleted, - { correlationId: req.id }, - ); - - // Write to response - res.status(204).end(); - return Promise.resolve(); - }) - .catch(next), - ); - }, -]; diff --git a/src/routes/timelines/delete.spec.js b/src/routes/timelines/delete.spec.js deleted file mode 100644 index 76a0fb55..00000000 --- a/src/routes/timelines/delete.spec.js +++ /dev/null @@ -1,306 +0,0 @@ -/** - * Tests for delete.js - */ -import request from 'supertest'; -import chai from 'chai'; - -import models from '../../models'; -import server from '../../app'; -import testUtil from '../../tests/util'; -import { EVENT } from '../../constants'; - -const should = chai.should(); // eslint-disable-line no-unused-vars - -describe('DELETE timeline', () => { - beforeEach((done) => { - testUtil.clearDb() - .then(() => { - models.Project.bulkCreate([ - { - type: 'generic', - billingAccountId: 1, - name: 'test1', - description: 'test project1', - status: 'draft', - details: {}, - createdBy: 1, - updatedBy: 1, - }, - { - type: 'generic', - billingAccountId: 2, - name: 'test2', - description: 'test project2', - status: 'draft', - details: {}, - createdBy: 2, - updatedBy: 2, - deletedAt: '2018-05-15T00:00:00Z', - }, - ]) - .then(() => { - // Create member - models.ProjectMember.bulkCreate([ - { - userId: 40051332, - projectId: 1, - role: 'copilot', - isPrimary: true, - createdBy: 1, - updatedBy: 1, - }, - { - userId: 40051331, - projectId: 1, - role: 'customer', - isPrimary: true, - createdBy: 1, - updatedBy: 1, - }, - ]).then(() => - // Create phase - models.ProjectPhase.bulkCreate([ - { - projectId: 1, - name: 'test project phase 1', - status: 'active', - startDate: '2018-05-15T00:00:00Z', - endDate: '2018-05-15T12:00:00Z', - budget: 20.0, - progress: 1.23456, - details: { - message: 'This can be any json 2', - }, - createdBy: 1, - updatedBy: 1, - }, - { - projectId: 2, - name: 'test project phase 2', - status: 'active', - startDate: '2018-05-16T00:00:00Z', - endDate: '2018-05-16T12:00:00Z', - budget: 21.0, - progress: 1.234567, - details: { - message: 'This can be any json 2', - }, - createdBy: 2, - updatedBy: 2, - deletedAt: '2018-05-15T00:00:00Z', - }, - ])) - .then(() => - // Create timelines - models.Timeline.bulkCreate([ - { - name: 'name 1', - description: 'description 1', - startDate: '2018-05-11T00:00:00.000Z', - endDate: '2018-05-12T00:00:00.000Z', - reference: 'project', - referenceId: 1, - createdBy: 1, - updatedBy: 1, - }, - { - name: 'name 2', - description: 'description 2', - startDate: '2018-05-12T00:00:00.000Z', - endDate: '2018-05-13T00:00:00.000Z', - reference: 'phase', - referenceId: 1, - createdBy: 1, - updatedBy: 1, - }, - { - name: 'name 3', - description: 'description 3', - startDate: '2018-05-13T00:00:00.000Z', - endDate: '2018-05-14T00:00:00.000Z', - reference: 'phase', - referenceId: 1, - createdBy: 1, - updatedBy: 1, - deletedAt: '2018-05-14T00:00:00.000Z', - }, - ])) - .then(() => - // Create milestones - models.Milestone.bulkCreate([ - { - timelineId: 1, - name: 'milestone 1', - duration: 2, - startDate: '2018-05-03T00:00:00.000Z', - status: 'open', - type: 'type1', - details: { - detail1: { - subDetail1A: 1, - subDetail1B: 2, - }, - detail2: [1, 2, 3], - }, - order: 1, - plannedText: 'plannedText 1', - activeText: 'activeText 1', - completedText: 'completedText 1', - blockedText: 'blockedText 1', - createdBy: 1, - updatedBy: 2, - }, - { - timelineId: 1, - name: 'milestone 2', - duration: 2, - startDate: '2018-05-03T00:00:00.000Z', - status: 'open', - type: 'type1', - details: { - detail1: { - subDetail1A: 1, - subDetail1B: 2, - }, - detail2: [1, 2, 3], - }, - order: 1, - plannedText: 'plannedText 1', - activeText: 'activeText 1', - completedText: 'completedText 1', - blockedText: 'blockedText 1', - createdBy: 1, - updatedBy: 2, - }, - ])) - .then(() => done()); - }); - }); - }); - - after(testUtil.clearDb); - - describe('DELETE /timelines/{timelineId}', () => { - it('should return 403 if user is not authenticated', (done) => { - request(server) - .delete('/v4/timelines/1') - .expect(403, done); - }); - - it('should return 403 for member who is not in the project', (done) => { - request(server) - .delete('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.member2}`, - }) - .expect(403, done); - }); - - it('should return 403 for member who is not in the project (timeline refers to a phase)', (done) => { - request(server) - .delete('/v4/timelines/2') - .set({ - Authorization: `Bearer ${testUtil.jwts.member2}`, - }) - .expect(403, done); - }); - - it('should return 404 for non-existed timeline', (done) => { - request(server) - .delete('/v4/timelines/1234') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(404, done); - }); - - it('should return 404 for deleted timeline', (done) => { - request(server) - .delete('/v4/timelines/3') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(404, done); - }); - - it('should return 422 for invalid param', (done) => { - request(server) - .delete('/v4/timelines/0') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(422, done); - }); - - // eslint-disable-next-line func-names - it('should return 204, for admin, if timeline was successfully removed', function (done) { - this.timeout(10000); - - models.Milestone.findAll({ where: { timelineId: 1 } }) - .then((results) => { - results.should.have.length(2); - - request(server) - .delete('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(204) - .end(() => { - // eslint-disable-next-line no-unused-expressions - server.services.pubsub.publish.calledWith(EVENT.ROUTING_KEY.TIMELINE_REMOVED).should.be.true; - - // Milestones are cascade deleted - setTimeout(() => { - models.Milestone.findAll({ where: { timelineId: 1 } }) - .then((afterResults) => { - afterResults.should.have.length(0); - - done(); - }); - }, 3000); - }); - }); - }); - - it('should return 204, for connect admin, if timeline was successfully removed', (done) => { - request(server) - .delete('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, - }) - .expect(204) - .end(done); - }); - - it('should return 204, for connect manager, if timeline was successfully removed', (done) => { - request(server) - .delete('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, - }) - .expect(204) - .end(done); - }); - - it('should return 204, for copilot, if timeline was successfully removed', (done) => { - request(server) - .delete('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .expect(204) - .end(done); - }); - - it('should return 204, for member, if timeline was successfully removed', (done) => { - request(server) - .delete('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.member}`, - }) - .expect(204) - .end(done); - }); - }); -}); diff --git a/src/routes/timelines/get.js b/src/routes/timelines/get.js deleted file mode 100644 index 2e9a03b1..00000000 --- a/src/routes/timelines/get.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * API to get a timeline - */ -import validate from 'express-validation'; -import Joi from 'joi'; -import _ from 'lodash'; -import { middleware as tcMiddleware } from 'tc-core-library-js'; -import util from '../../util'; - -const permissions = tcMiddleware.permissions; - -const schema = { - params: { - timelineId: Joi.number().integer().positive().required(), - }, -}; - -module.exports = [ - validate(schema), - // Validate and get projectId from the timelineId param, and set to request params for - // checking by the permissions middleware - util.validateTimelineIdParam, - permissions('timeline.view'), - (req, res) => { - // Load the milestones - req.timeline.getMilestones() - .then((milestones) => { - const timeline = _.omit(req.timeline.toJSON(), ['deletedAt', 'deletedBy']); - timeline.milestones = - _.map(milestones, milestone => _.omit(milestone.toJSON(), ['deletedAt', 'deletedBy'])); - - // Write to response - res.json(util.wrapResponse(req.id, timeline)); - }); - }, -]; diff --git a/src/routes/timelines/get.spec.js b/src/routes/timelines/get.spec.js deleted file mode 100644 index 253013da..00000000 --- a/src/routes/timelines/get.spec.js +++ /dev/null @@ -1,304 +0,0 @@ -/** - * 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(); - -const milestones = [ - { - id: 1, - timelineId: 1, - name: 'milestone 1', - duration: 2, - startDate: '2018-05-03T00:00:00.000Z', - endDate: '2018-05-04T00:00:00.000Z', - completionDate: '2018-05-05T00:00:00.000Z', - status: 'open', - type: 'type1', - details: { - detail1: { - subDetail1A: 1, - subDetail1B: 2, - }, - detail2: [1, 2, 3], - }, - order: 1, - plannedText: 'plannedText 1', - activeText: 'activeText 1', - completedText: 'completedText 1', - blockedText: 'blockedText 1', - createdBy: 1, - updatedBy: 2, - createdAt: '2018-05-11T00:00:00.000Z', - updatedAt: '2018-05-11T00:00:00.000Z', - }, - { - id: 2, - timelineId: 1, - name: 'milestone 2', - duration: 3, - startDate: '2018-05-04T00:00:00.000Z', - status: 'open', - type: 'type2', - order: 2, - plannedText: 'plannedText 2', - activeText: 'activeText 2', - completedText: 'completedText 2', - blockedText: 'blockedText 2', - createdBy: 2, - updatedBy: 3, - createdAt: '2018-05-11T00:00:00.000Z', - updatedAt: '2018-05-11T00:00:00.000Z', - }, -]; - -describe('GET timeline', () => { - before((done) => { - testUtil.clearDb() - .then(() => { - models.Project.bulkCreate([ - { - type: 'generic', - billingAccountId: 1, - name: 'test1', - description: 'test project1', - status: 'draft', - details: {}, - createdBy: 1, - updatedBy: 1, - }, - { - type: 'generic', - billingAccountId: 2, - name: 'test2', - description: 'test project2', - status: 'draft', - details: {}, - createdBy: 2, - updatedBy: 2, - deletedAt: '2018-05-15T00:00:00Z', - }, - ]) - .then(() => { - // Create member - models.ProjectMember.bulkCreate([ - { - userId: 40051332, - projectId: 1, - role: 'copilot', - isPrimary: true, - createdBy: 1, - updatedBy: 1, - }, - { - userId: 40051331, - projectId: 1, - role: 'customer', - isPrimary: true, - createdBy: 1, - updatedBy: 1, - }, - ]).then(() => - // Create phase - models.ProjectPhase.bulkCreate([ - { - projectId: 1, - name: 'test project phase 1', - status: 'active', - startDate: '2018-05-15T00:00:00Z', - endDate: '2018-05-15T12:00:00Z', - budget: 20.0, - progress: 1.23456, - details: { - message: 'This can be any json 2', - }, - createdBy: 1, - updatedBy: 1, - }, - { - projectId: 2, - name: 'test project phase 2', - status: 'active', - startDate: '2018-05-16T00:00:00Z', - endDate: '2018-05-16T12:00:00Z', - budget: 21.0, - progress: 1.234567, - details: { - message: 'This can be any json 2', - }, - createdBy: 2, - updatedBy: 2, - deletedAt: '2018-05-15T00:00:00Z', - }, - ])) - .then(() => - // Create timelines - models.Timeline.bulkCreate([ - { - name: 'name 1', - description: 'description 1', - startDate: '2018-05-11T00:00:00.000Z', - endDate: '2018-05-12T00:00:00.000Z', - reference: 'project', - referenceId: 1, - createdBy: 1, - updatedBy: 1, - }, - { - name: 'name 2', - description: 'description 2', - startDate: '2018-05-12T00:00:00.000Z', - endDate: '2018-05-13T00:00:00.000Z', - reference: 'phase', - referenceId: 1, - createdBy: 1, - updatedBy: 1, - }, - { - name: 'name 3', - description: 'description 3', - startDate: '2018-05-13T00:00:00.000Z', - endDate: '2018-05-14T00:00:00.000Z', - reference: 'phase', - referenceId: 1, - createdBy: 1, - updatedBy: 1, - deletedAt: '2018-05-14T00:00:00.000Z', - }, - ])) - .then(() => models.Milestone.bulkCreate(milestones)) - .then(() => done()); - }); - }); - }); - - after(testUtil.clearDb); - - describe('GET /timelines/{timelineId}', () => { - it('should return 403 if user is not authenticated', (done) => { - request(server) - .get('/v4/timelines/1') - .expect(403, done); - }); - - it('should return 403 for member who is not in the project', (done) => { - request(server) - .get('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.member2}`, - }) - .expect(403, done); - }); - - it('should return 403 for member who is not in the project (timeline refers to a phase)', (done) => { - request(server) - .get('/v4/timelines/2') - .set({ - Authorization: `Bearer ${testUtil.jwts.member2}`, - }) - .expect(403, done); - }); - - it('should return 404 for non-existed timeline', (done) => { - request(server) - .get('/v4/timelines/1234') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(404, done); - }); - - it('should return 404 for deleted timeline', (done) => { - request(server) - .get('/v4/timelines/3') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(404, done); - }); - - it('should return 422 for invalid param', (done) => { - request(server) - .get('/v4/timelines/0') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(422, done); - }); - - it('should return 200 for admin', (done) => { - request(server) - .get('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(200) - .end((err, res) => { - const resJson = res.body.result.content; - resJson.id.should.be.eql(1); - resJson.name.should.be.eql('name 1'); - resJson.description.should.be.eql('description 1'); - resJson.startDate.should.be.eql('2018-05-11T00:00:00.000Z'); - resJson.endDate.should.be.eql('2018-05-12T00:00:00.000Z'); - resJson.reference.should.be.eql('project'); - resJson.referenceId.should.be.eql(1); - - resJson.createdBy.should.be.eql(1); - should.exist(resJson.createdAt); - resJson.updatedBy.should.be.eql(1); - should.exist(resJson.updatedAt); - should.not.exist(resJson.deletedBy); - should.not.exist(resJson.deletedAt); - - // Milestones - resJson.milestones.should.have.length(2); - - done(); - }); - }); - - it('should return 200 for connect admin', (done) => { - request(server) - .get('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, - }) - .expect(200) - .end(done); - }); - - it('should return 200 for connect manager', (done) => { - request(server) - .get('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, - }) - .expect(200) - .end(done); - }); - - it('should return 200 for member', (done) => { - request(server) - .get('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.member}`, - }) - .expect(200, done); - }); - - it('should return 200 for copilot', (done) => { - request(server) - .get('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .expect(200, done); - }); - }); -}); diff --git a/src/routes/timelines/list.js b/src/routes/timelines/list.js deleted file mode 100644 index 6d3ff14f..00000000 --- a/src/routes/timelines/list.js +++ /dev/null @@ -1,94 +0,0 @@ -/** - * API to list all timelines - */ -import config from 'config'; -import _ from 'lodash'; -import util from '../../util'; -import models from '../../models'; -import { USER_ROLE, TIMELINE_REFERENCES } from '../../constants'; - -const ES_TIMELINE_INDEX = config.get('elasticsearchConfig.timelineIndexName'); -const ES_TIMELINE_TYPE = config.get('elasticsearchConfig.timelineDocType'); - -/** - * Retrieve timelines from elastic search. - * @param {Array} esTerms the elastic search terms - * @returns {Promise} the promise resolves to the results - */ -function retrieveTimelines(esTerms) { - return new Promise((accept, reject) => { - const es = util.getElasticSearchClient(); - es.search({ - index: ES_TIMELINE_INDEX, - type: ES_TIMELINE_TYPE, - body: { - query: { bool: { must: esTerms } }, - }, - }).then((docs) => { - const rows = _.map(docs.hits.hits, single => _.omit(single._source, ['projectId'])); // eslint-disable-line no-underscore-dangle - accept({ rows, count: docs.hits.total }); - }).catch(reject); - }); -} - - -module.exports = [ - (req, res, next) => { - // Validate the filter - const filter = util.parseQueryFilter(req.query.filter); - if (!util.isValidFilter(filter, ['reference', 'referenceId'])) { - const apiErr = new Error('Only allowed to filter by reference and referenceId'); - apiErr.status = 422; - return next(apiErr); - } - - // Build the elastic search query - const esTerms = []; - if (filter.reference) { - if (!_.includes(TIMELINE_REFERENCES, filter.reference)) { - const apiErr = new Error(`reference filter must be in ${TIMELINE_REFERENCES}`); - apiErr.status = 422; - return next(apiErr); - } - - esTerms.push({ - term: { reference: filter.reference }, - }); - } - if (filter.referenceId) { - if (_.lt(filter.referenceId, 1)) { - const apiErr = new Error('referenceId filter must be a positive integer'); - apiErr.status = 422; - return next(apiErr); - } - - esTerms.push({ - term: { referenceId: filter.referenceId }, - }); - } - - // Admin and topcoder manager can see all timelines - if (util.hasAdminRole(req) || util.hasRole(req, USER_ROLE.MANAGER)) { - return retrieveTimelines(esTerms) - .then(result => res.json(util.wrapResponse(req.id, result.rows, result.count))) - .catch(err => next(err)); - } - - // Get project ids for copilot or member - const getProjectIds = util.hasRole(req, USER_ROLE.COPILOT) ? - models.Project.getProjectIdsForCopilot(req.authUser.userId) : - models.ProjectMember.getProjectIdsForUser(req.authUser.userId); - - return getProjectIds - .then((accessibleProjectIds) => { - // Copilot or member can see his projects - esTerms.push({ - terms: { projectId: accessibleProjectIds }, - }); - - return retrieveTimelines(esTerms); - }) - .then(result => res.json(util.wrapResponse(req.id, result.rows, result.count))) - .catch(err => next(err)); - }, -]; diff --git a/src/routes/timelines/list.spec.js b/src/routes/timelines/list.spec.js deleted file mode 100644 index f903d16c..00000000 --- a/src/routes/timelines/list.spec.js +++ /dev/null @@ -1,397 +0,0 @@ -/** - * Tests for list.js - */ -import chai from 'chai'; -import request from 'supertest'; -import sleep from 'sleep'; -import config from 'config'; -import _ from 'lodash'; - -import models from '../../models'; -import server from '../../app'; -import testUtil from '../../tests/util'; - -const ES_TIMELINE_INDEX = config.get('elasticsearchConfig.timelineIndexName'); -const ES_TIMELINE_TYPE = config.get('elasticsearchConfig.timelineDocType'); - -const should = chai.should(); - -const timelines = [ - { - name: 'name 1', - description: 'description 1', - startDate: '2018-05-11T00:00:00.000Z', - endDate: '2018-05-12T00:00:00.000Z', - reference: 'project', - referenceId: 1, - createdBy: 1, - updatedBy: 1, - }, - { - name: 'name 2', - description: 'description 2', - startDate: '2018-05-12T00:00:00.000Z', - endDate: '2018-05-13T00:00:00.000Z', - reference: 'phase', - referenceId: 1, - createdBy: 1, - updatedBy: 1, - }, - { - name: 'name 3', - description: 'description 3', - startDate: '2018-05-13T00:00:00.000Z', - endDate: '2018-05-14T00:00:00.000Z', - reference: 'phase', - referenceId: 2, - createdBy: 1, - updatedBy: 1, - }, -]; -const milestones = [ - { - id: 1, - timelineId: 1, - name: 'milestone 1', - duration: 2, - startDate: '2018-05-03T00:00:00.000Z', - endDate: '2018-05-04T00:00:00.000Z', - completionDate: '2018-05-05T00:00:00.000Z', - status: 'open', - type: 'type1', - details: { - detail1: { - subDetail1A: 1, - subDetail1B: 2, - }, - detail2: [1, 2, 3], - }, - order: 1, - plannedText: 'plannedText 1', - activeText: 'activeText 1', - completedText: 'completedText 1', - blockedText: 'blockedText 1', - createdBy: 1, - updatedBy: 2, - createdAt: '2018-05-11T00:00:00.000Z', - updatedAt: '2018-05-11T00:00:00.000Z', - }, - { - id: 2, - timelineId: 1, - name: 'milestone 2', - duration: 3, - startDate: '2018-05-04T00:00:00.000Z', - status: 'open', - type: 'type2', - order: 2, - plannedText: 'plannedText 2', - activeText: 'activeText 2', - completedText: 'completedText 2', - blockedText: 'blockedText 2', - createdBy: 2, - updatedBy: 3, - createdAt: '2018-05-11T00:00:00.000Z', - updatedAt: '2018-05-11T00:00:00.000Z', - }, -]; - - -describe('LIST timelines', () => { - before(function beforeHook(done) { - this.timeout(10000); - testUtil.clearDb() - .then(() => { - models.Project.bulkCreate([ - { - type: 'generic', - billingAccountId: 1, - name: 'test1', - description: 'test project1', - status: 'draft', - details: {}, - createdBy: 1, - updatedBy: 1, - }, - { - type: 'generic', - billingAccountId: 2, - name: 'test2', - description: 'test project2', - status: 'draft', - details: {}, - createdBy: 2, - updatedBy: 2, - deletedAt: '2018-05-15T00:00:00Z', - }, - ]) - .then(() => { - // Create member - models.ProjectMember.bulkCreate([ - { - userId: 40051332, - projectId: 1, - role: 'copilot', - isPrimary: true, - createdBy: 1, - updatedBy: 1, - }, - { - userId: 40051331, - projectId: 1, - role: 'customer', - isPrimary: true, - createdBy: 1, - updatedBy: 1, - }, - ]).then(() => - // Create phase - models.ProjectPhase.bulkCreate([ - { - projectId: 1, - name: 'test project phase 1', - status: 'active', - startDate: '2018-05-15T00:00:00Z', - endDate: '2018-05-15T12:00:00Z', - budget: 20.0, - progress: 1.23456, - details: { - message: 'This can be any json 2', - }, - createdBy: 1, - updatedBy: 1, - }, - { - projectId: 2, - name: 'test project phase 2', - status: 'active', - startDate: '2018-05-16T00:00:00Z', - endDate: '2018-05-16T12:00:00Z', - budget: 21.0, - progress: 1.234567, - details: { - message: 'This can be any json 2', - }, - createdBy: 2, - updatedBy: 2, - }, - ])) - .then(() => - // Create timelines - models.Timeline.bulkCreate(timelines, { returning: true })) - .then(createdTimelines => - // Index to ES - Promise.all(_.map(createdTimelines, (createdTimeline) => { - const timelineJson = _.omit(createdTimeline.toJSON(), 'deletedAt', 'deletedBy'); - timelineJson.projectId = createdTimeline.id !== 3 ? 1 : 2; - if (timelineJson.id === 1) { - timelineJson.milestones = milestones; - } - return server.services.es.index({ - index: ES_TIMELINE_INDEX, - type: ES_TIMELINE_TYPE, - id: timelineJson.id, - body: timelineJson, - }); - })) - .then(() => { - // sleep for some time, let elasticsearch indices be settled - sleep.sleep(5); - done(); - })); - }); - }); - }); - - after(testUtil.clearDb); - - describe('GET /timelines', () => { - it('should return 403 if user is not authenticated', (done) => { - request(server) - .get('/v4/timelines') - .expect(403, done); - }); - - it('should return 422 for invalid filter key', (done) => { - request(server) - .get('/v4/timelines?filter=invalid%3Dproject') - .set({ - Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, - }) - .expect(422) - .end(done); - }); - - it('should return 422 for invalid reference filter', (done) => { - request(server) - .get('/v4/timelines?filter=reference%3Dinvalid') - .set({ - Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, - }) - .expect(422) - .end(done); - }); - - it('should return 422 for invalid referenceId filter', (done) => { - request(server) - .get('/v4/timelines?filter=referenceId%3D0') - .set({ - Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, - }) - .expect(422) - .end(done); - }); - - it('should return 200 for admin', (done) => { - request(server) - .get('/v4/timelines') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(200) - .end((err, res) => { - const timeline = timelines[0]; - - let resJson = res.body.result.content; - resJson.should.have.length(3); - resJson = _.sortBy(resJson, o => o.id); - resJson[0].id.should.be.eql(1); - resJson[0].name.should.be.eql(timeline.name); - resJson[0].description.should.be.eql(timeline.description); - resJson[0].startDate.should.be.eql(timeline.startDate); - resJson[0].endDate.should.be.eql(timeline.endDate); - resJson[0].reference.should.be.eql(timeline.reference); - resJson[0].referenceId.should.be.eql(timeline.referenceId); - - resJson[0].createdBy.should.be.eql(timeline.createdBy); - should.exist(resJson[0].createdAt); - resJson[0].updatedBy.should.be.eql(timeline.updatedBy); - should.exist(resJson[0].updatedAt); - should.not.exist(resJson[0].deletedBy); - should.not.exist(resJson[0].deletedAt); - - // Milestones - resJson[0].milestones.should.have.length(2); - - done(); - }); - }); - - it('should return 200 for connect admin', (done) => { - request(server) - .get('/v4/timelines') - .set({ - Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, - }) - .expect(200) - .end((err, res) => { - const resJson = res.body.result.content; - resJson.should.have.length(3); - - done(); - }); - }); - - it('should return 200 for connect manager', (done) => { - request(server) - .get('/v4/timelines') - .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, - }) - .expect(200) - .end((err, res) => { - const resJson = res.body.result.content; - resJson.should.have.length(3); - - done(); - }); - }); - - it('should return 200 for member', (done) => { - request(server) - .get('/v4/timelines') - .set({ - Authorization: `Bearer ${testUtil.jwts.member}`, - }) - .end((err, res) => { - const resJson = res.body.result.content; - resJson.should.have.length(2); - - done(); - }); - }); - - it('should return 200 for copilot', (done) => { - request(server) - .get('/v4/timelines') - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .end((err, res) => { - const resJson = res.body.result.content; - resJson.should.have.length(2); - - done(); - }); - }); - - it('should return 200 for member with no accessible project', (done) => { - request(server) - .get('/v4/timelines') - .set({ - Authorization: `Bearer ${testUtil.jwts.member2}`, - }) - .end((err, res) => { - const resJson = res.body.result.content; - resJson.should.have.length(0); // no accessible timelines - - done(); - }); - }); - - it('should return 200 with reference filter', (done) => { - request(server) - .get('/v4/timelines?filter=reference%3Dproject') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(200) - .end((err, res) => { - const resJson = res.body.result.content; - resJson.should.have.length(1); - - done(); - }); - }); - - it('should return 200 with referenceId filter', (done) => { - request(server) - .get('/v4/timelines?filter=referenceId%3D2') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(200) - .end((err, res) => { - const resJson = res.body.result.content; - resJson.should.have.length(1); - - done(); - }); - }); - - it('should return 200 with reference and referenceId filter', (done) => { - request(server) - .get('/v4/timelines?filter=reference%3Dproject%26referenceId%3D1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(200) - .end((err, res) => { - const resJson = res.body.result.content; - resJson.should.have.length(1); - - done(); - }); - }); - }); -}); diff --git a/src/routes/timelines/update.js b/src/routes/timelines/update.js deleted file mode 100644 index 99343de4..00000000 --- a/src/routes/timelines/update.js +++ /dev/null @@ -1,109 +0,0 @@ -/** - * API to update a timeline - */ -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 { EVENT, TIMELINE_REFERENCES } from '../../constants'; - -const permissions = tcMiddleware.permissions; - -const schema = { - params: { - timelineId: Joi.number().integer().positive().required(), - }, - body: { - param: Joi.object().keys({ - id: Joi.any().strip(), - name: Joi.string().max(255).required(), - description: Joi.string().max(255), - startDate: Joi.date().required(), - endDate: Joi.date().min(Joi.ref('startDate')).allow(null), - reference: Joi.string().valid(_.values(TIMELINE_REFERENCES)).required(), - referenceId: Joi.number().integer().positive().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), - // Validate and get projectId from the timelineId param and request body, - // and set to request params for checking by the permissions middleware - util.validateTimelineIdParam, - util.validateTimelineRequestBody, - permissions('timeline.edit'), - (req, res, next) => { - const entityToUpdate = _.assign(req.body.param, { - updatedBy: req.authUser.userId, - }); - - const timeline = req.timeline; - const original = _.omit(timeline.toJSON(), ['deletedAt', 'deletedBy']); - let updated; - - // Update - return timeline.update(entityToUpdate) - .then((updatedTimeline) => { - // Omit deletedAt, deletedBy - updated = _.omit(updatedTimeline.toJSON(), ['deletedAt', 'deletedBy']); - - // Update milestones startDate and endDate if necessary - if (original.startDate !== updated.startDate || original.endDate !== updated.endDate) { - return updatedTimeline.getMilestones() - .then((milestones) => { - const updateMilestonePromises = _.map(milestones, (_milestone) => { - const milestone = _milestone; - if (original.startDate !== updated.startDate) { - if (milestone.startDate && milestone.startDate < updated.startDate) { - milestone.startDate = updated.startDate; - if (milestone.endDate && milestone.endDate < milestone.startDate) { - milestone.endDate = milestone.startDate; - } - milestone.updatedBy = req.authUser.userId; - } - } - - if (original.endDate !== updated.endDate) { - if (milestone.endDate && updated.endDate && updated.endDate < milestone.endDate) { - milestone.endDate = updated.endDate; - milestone.updatedBy = req.authUser.userId; - } - } - - return milestone.save(); - }); - - return Promise.all(updateMilestonePromises) - .then((updatedMilestones) => { - updated.milestones = - _.map(updatedMilestones, milestone => _.omit(milestone.toJSON(), ['deletedAt', 'deletedBy'])); - return Promise.resolve(); - }); - }); - } - - return Promise.resolve(); - }) - .then(() => { - // Send event to bus - req.log.debug('Sending event to RabbitMQ bus for timeline %d', updated.id); - req.app.services.pubsub.publish(EVENT.ROUTING_KEY.TIMELINE_UPDATED, - { original, updated }, - { correlationId: req.id }, - ); - - // Write to response - res.json(util.wrapResponse(req.id, updated)); - return Promise.resolve(); - }) - .catch(next); - }, -]; diff --git a/src/routes/timelines/update.spec.js b/src/routes/timelines/update.spec.js deleted file mode 100644 index eb887fe3..00000000 --- a/src/routes/timelines/update.spec.js +++ /dev/null @@ -1,626 +0,0 @@ -/** - * Tests for get.js - */ -import chai from 'chai'; -import request from 'supertest'; -import _ from 'lodash'; -import models from '../../models'; -import server from '../../app'; -import testUtil from '../../tests/util'; -import { EVENT } from '../../constants'; - -const should = chai.should(); - - -const milestones = [ - { - id: 1, - timelineId: 1, - name: 'milestone 1', - duration: 2, - startDate: '2018-05-13T00:00:00.000Z', - endDate: '2018-05-16T00:00:00.000Z', - completionDate: '2018-05-05T00:00:00.000Z', - status: 'open', - type: 'type1', - details: { - detail1: { - subDetail1A: 1, - subDetail1B: 2, - }, - detail2: [1, 2, 3], - }, - order: 1, - plannedText: 'plannedText 1', - activeText: 'activeText 1', - completedText: 'completedText 1', - blockedText: 'blockedText 1', - createdBy: 1, - updatedBy: 2, - createdAt: '2018-05-11T00:00:00.000Z', - updatedAt: '2018-05-11T00:00:00.000Z', - }, - { - id: 2, - timelineId: 1, - name: 'milestone 2', - duration: 3, - startDate: '2018-05-14T00:00:00.000Z', - status: 'open', - type: 'type2', - order: 2, - plannedText: 'plannedText 2', - activeText: 'activeText 2', - completedText: 'completedText 2', - blockedText: 'blockedText 2', - createdBy: 2, - updatedBy: 3, - createdAt: '2018-05-11T00:00:00.000Z', - updatedAt: '2018-05-11T00:00:00.000Z', - }, -]; - -describe('UPDATE timeline', () => { - beforeEach((done) => { - testUtil.clearDb() - .then(() => { - models.Project.bulkCreate([ - { - type: 'generic', - billingAccountId: 1, - name: 'test1', - description: 'test project1', - status: 'draft', - details: {}, - createdBy: 1, - updatedBy: 1, - }, - { - type: 'generic', - billingAccountId: 2, - name: 'test2', - description: 'test project2', - status: 'draft', - details: {}, - createdBy: 2, - updatedBy: 2, - deletedAt: '2018-05-15T00:00:00Z', - }, - ], { returning: true }) - .then(() => { - // Create member - models.ProjectMember.bulkCreate([ - { - userId: 40051332, - projectId: 1, - role: 'copilot', - isPrimary: true, - createdBy: 1, - updatedBy: 1, - }, - { - userId: 40051331, - projectId: 1, - role: 'customer', - isPrimary: true, - createdBy: 1, - updatedBy: 1, - }, - ]).then(() => - // Create phase - models.ProjectPhase.bulkCreate([ - { - projectId: 1, - name: 'test project phase 1', - status: 'active', - startDate: '2018-05-15T00:00:00Z', - endDate: '2018-05-15T12:00:00Z', - budget: 20.0, - progress: 1.23456, - details: { - message: 'This can be any json 2', - }, - createdBy: 1, - updatedBy: 1, - }, - { - projectId: 2, - name: 'test project phase 2', - status: 'active', - startDate: '2018-05-16T00:00:00Z', - endDate: '2018-05-16T12:00:00Z', - budget: 21.0, - progress: 1.234567, - details: { - message: 'This can be any json 2', - }, - createdBy: 2, - updatedBy: 2, - deletedAt: '2018-05-15T00:00:00Z', - }, - ]), { returning: true }) - .then(() => - // Create timelines - models.Timeline.bulkCreate([ - { - name: 'name 1', - description: 'description 1', - startDate: '2018-05-11T00:00:00.000Z', - endDate: '2018-05-20T00:00:00.000Z', - reference: 'project', - referenceId: 1, - createdBy: 1, - updatedBy: 1, - }, - { - name: 'name 2', - description: 'description 2', - startDate: '2018-05-12T00:00:00.000Z', - endDate: '2018-05-13T00:00:00.000Z', - reference: 'phase', - referenceId: 1, - createdBy: 1, - updatedBy: 1, - }, - { - name: 'name 3', - description: 'description 3', - startDate: '2018-05-13T00:00:00.000Z', - endDate: '2018-05-14T00:00:00.000Z', - reference: 'phase', - referenceId: 1, - createdBy: 1, - updatedBy: 1, - deletedAt: '2018-05-14T00:00:00.000Z', - }, - ])) - .then(() => models.Milestone.bulkCreate(milestones)) - .then(() => done()); - }); - }); - }); - - after(testUtil.clearDb); - - describe('PATCH /timelines/{timelineId}', () => { - const body = { - param: { - name: 'new name 1', - description: 'new description 1', - startDate: '2018-06-01T00:00:00.000Z', - endDate: '2018-06-02T00:00:00.000Z', - reference: 'project', - referenceId: 1, - }, - }; - - it('should return 403 if user is not authenticated', (done) => { - request(server) - .patch('/v4/timelines/1') - .send(body) - .expect(403, done); - }); - - it('should return 403 for member who is not in the project', (done) => { - request(server) - .patch('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.member2}`, - }) - .send(body) - .expect(403, done); - }); - - it('should return 403 for member who is not in the project (timeline refers to a phase)', (done) => { - request(server) - .patch('/v4/timelines/2') - .send(body) - .set({ - Authorization: `Bearer ${testUtil.jwts.member2}`, - }) - .expect(403, done); - }); - - it('should return 404 for non-existed timeline', (done) => { - request(server) - .patch('/v4/timelines/1234') - .send(body) - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(404, done); - }); - - it('should return 404 for deleted timeline', (done) => { - request(server) - .patch('/v4/timelines/3') - .send(body) - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(404, done); - }); - - it('should return 422 for invalid param', (done) => { - request(server) - .patch('/v4/timelines/0') - .send(body) - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect(422, done); - }); - - it('should return 404 for non-existed template', (done) => { - request(server) - .patch('/v4/timelines/1234') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(body) - .expect(404, done); - }); - - it('should return 404 for deleted template', (done) => { - request(server) - .patch('/v4/timelines/3') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(body) - .expect(404, done); - }); - - it('should return 422 if missing name', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - name: undefined, - }), - }; - - request(server) - .patch('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if missing startDate', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - startDate: undefined, - }), - }; - - request(server) - .patch('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if startDate is after endDate', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - startDate: '2018-05-29T00:00:00.000Z', - endDate: '2018-05-28T00:00:00.000Z', - }), - }; - - request(server) - .patch('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if missing reference', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - reference: undefined, - }), - }; - - request(server) - .patch('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if missing referenceId', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - referenceId: undefined, - }), - }; - - request(server) - .patch('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if invalid reference', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - reference: 'invalid', - }), - }; - - request(server) - .patch('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if invalid referenceId', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - referenceId: 0, - }), - }; - - request(server) - .patch('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if project does not exist', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - referenceId: 1110, - }), - }; - - request(server) - .patch('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if project was deleted', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - referenceId: 2, - }), - }; - - request(server) - .patch('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if phase does not exist', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - reference: 'phase', - referenceId: 2222, - }), - }; - - request(server) - .patch('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 422 if phase was deleted', (done) => { - const invalidBody = { - param: _.assign({}, body.param, { - reference: 'phase', - referenceId: 2, - }), - }; - - request(server) - .patch('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(invalidBody) - .expect('Content-Type', /json/) - .expect(422, done); - }); - - it('should return 200 for admin', (done) => { - request(server) - .patch('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(body) - .expect(200) - .end((err, res) => { - const resJson = res.body.result.content; - should.exist(resJson.id); - resJson.name.should.be.eql(body.param.name); - resJson.description.should.be.eql(body.param.description); - resJson.startDate.should.be.eql(body.param.startDate); - resJson.endDate.should.be.eql(body.param.endDate); - resJson.reference.should.be.eql(body.param.reference); - resJson.referenceId.should.be.eql(body.param.referenceId); - - resJson.createdBy.should.be.eql(1); - should.exist(resJson.createdAt); - resJson.updatedBy.should.be.eql(40051333); // admin - should.exist(resJson.updatedAt); - should.not.exist(resJson.deletedAt); - should.not.exist(resJson.deletedBy); - - // eslint-disable-next-line no-unused-expressions - server.services.pubsub.publish.calledWith(EVENT.ROUTING_KEY.TIMELINE_UPDATED).should.be.true; - - done(); - }); - }); - - // eslint-disable-next-line func-names - it('should return 200 for admin with changed startDate', function (done) { - this.timeout(10000); - - request(server) - .patch('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send({ - param: _.assign({}, body.param, { - startDate: '2018-05-15T00:00:00.000Z', - endDate: '2018-05-17T00:00:00.000Z', // no affect to milestones - }), - }) - .expect(200) - .end(() => { - setTimeout(() => { - models.Milestone.findById(1) - .then((milestone) => { - milestone.startDate.should.be.eql(new Date('2018-05-15T00:00:00.000Z')); - milestone.endDate.should.be.eql(new Date('2018-05-16T00:00:00.000Z')); - }) - .then(() => models.Milestone.findById(2)) - .then((milestone) => { - milestone.startDate.should.be.eql(new Date('2018-05-15T00:00:00.000Z')); - should.not.exist(milestone.endDate); - - done(); - }); - }, 3000); - }); - }); - - // eslint-disable-next-line func-names - it('should return 200 for admin with changed endDate', function (done) { - this.timeout(10000); - - request(server) - .patch('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send({ - param: _.assign({}, body.param, { - startDate: '2018-05-12T00:00:00.000Z', // no affect to milestones - endDate: '2018-05-15T00:00:00.000Z', - }), - }) - .expect(200) - .end(() => { - setTimeout(() => { - models.Milestone.findById(1) - .then((milestone) => { - milestone.startDate.should.be.eql(new Date('2018-05-13T00:00:00.000Z')); - milestone.endDate.should.be.eql(new Date('2018-05-15T00:00:00.000Z')); - }) - .then(() => models.Milestone.findById(2)) - .then((milestone) => { - milestone.startDate.should.be.eql(new Date('2018-05-14T00:00:00.000Z')); - should.not.exist(milestone.endDate); - - done(); - }); - }, 3000); - }); - }); - - it('should return 200 for connect admin', (done) => { - request(server) - .patch('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, - }) - .send(body) - .expect(200) - .end(done); - }); - - it('should return 200 for connect manager', (done) => { - request(server) - .patch('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, - }) - .send(body) - .expect(200) - .end(done); - }); - - it('should return 200 for copilot', (done) => { - request(server) - .patch('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .send(body) - .expect(200) - .end(done); - }); - - it('should return 200 for member', (done) => { - request(server) - .patch('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.member}`, - }) - .send(body) - .expect(200) - .end(done); - }); - - it('should return 200 if changing reference and referenceId', (done) => { - const newBody = { - param: _.assign({}, body.param, { - reference: 'phase', - referenceId: 1, - }), - }; - - request(server) - .patch('/v4/timelines/1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send(newBody) - .expect(200) - .end(done); - }); - }); -}); diff --git a/src/tests/seed.js b/src/tests/seed.js index 3ef8b098..350480c4 100644 --- a/src/tests/seed.js +++ b/src/tests/seed.js @@ -1,5 +1,4 @@ import models from '../models'; -import { TIMELINE_REFERENCES } from '../constants'; models.sequelize.sync({ force: true }) .then(() => @@ -434,132 +433,6 @@ models.sequelize.sync({ force: true }) createdBy: 1, updatedBy: 2, }, - ], { returning: true })) - // Product milestone templates - .then(productTemplates => models.ProductMilestoneTemplate.bulkCreate([ - { - name: 'milestoneTemplate 1', - duration: 3, - type: 'type1', - order: 1, - productTemplateId: productTemplates[0].id, - createdBy: 1, - updatedBy: 2, - }, - { - name: 'milestoneTemplate 2', - duration: 4, - type: 'type2', - order: 2, - productTemplateId: productTemplates[0].id, - createdBy: 2, - updatedBy: 3, - }, - ])) - // Project phases - .then(() => models.ProjectPhase.bulkCreate([ - { - name: 'phase 1', - projectId: 1, - createdBy: 1, - updatedBy: 2, - }, - { - name: 'phase 2', - projectId: 1, - createdBy: 2, - updatedBy: 3, - }, - ], { returning: true })) - // Timelines - .then(projectPhases => models.Timeline.bulkCreate([ - { - name: 'timeline 1', - startDate: '2018-05-01T00:00:00.000Z', - reference: TIMELINE_REFERENCES.PROJECT, - referenceId: projectPhases[0].projectId, - createdBy: 1, - updatedBy: 2, - }, - { - name: 'timeline 2', - startDate: '2018-05-02T00:00:00.000Z', - reference: TIMELINE_REFERENCES.PHASE, - referenceId: projectPhases[0].id, - createdBy: 2, - updatedBy: 3, - }, - { - name: 'timeline 3', - startDate: '2018-05-03T00:00:00.000Z', - reference: TIMELINE_REFERENCES.PROJECT, - referenceId: projectPhases[0].projectId, - createdBy: 3, - updatedBy: 4, - }, - { - name: 'timeline 4', - startDate: '2018-05-04T00:00:00.000Z', - reference: TIMELINE_REFERENCES.PROJECT, - referenceId: 2, - createdBy: 4, - updatedBy: 5, - }, - ], { returning: true })) - // Milestones - .then(timelines => models.Milestone.bulkCreate([ - { - timelineId: timelines[0].id, - name: 'milestone 1', - duration: 2, - startDate: '2018-05-03T00:00:00.000Z', - status: 'open', - type: 'type1', - details: { - detail1: { - subDetail1A: 1, - subDetail1B: 2, - }, - detail2: [1, 2, 3], - }, - order: 1, - plannedText: 'plannedText 1', - activeText: 'activeText 1', - completedText: 'completedText 1', - blockedText: 'blockedText 1', - createdBy: 1, - updatedBy: 2, - }, - { - timelineId: timelines[0].id, - name: 'milestone 2', - duration: 3, - startDate: '2018-05-04T00:00:00.000Z', - status: 'open', - type: 'type2', - order: 2, - plannedText: 'plannedText 2', - activeText: 'activeText 2', - completedText: 'completedText 2', - blockedText: 'blockedText 2', - createdBy: 2, - updatedBy: 3, - }, - { - timelineId: timelines[2].id, - name: 'milestone 3', - duration: 4, - startDate: '2018-05-04T00:00:00.000Z', - status: 'open', - type: 'type3', - order: 3, - plannedText: 'plannedText 3', - activeText: 'activeText 3', - completedText: 'completedText 3', - blockedText: 'blockedText 3', - createdBy: 3, - updatedBy: 4, - }, ])) .then(() => models.ProjectType.bulkCreate([ { diff --git a/src/util.js b/src/util.js index 843ff371..add05eb6 100644 --- a/src/util.js +++ b/src/util.js @@ -17,7 +17,7 @@ import urlencode from 'urlencode'; import elasticsearch from 'elasticsearch'; import Promise from 'bluebird'; import AWS from 'aws-sdk'; -import { ADMIN_ROLES, TOKEN_SCOPES, TIMELINE_REFERENCES } from './constants'; +import { ADMIN_ROLES, TOKEN_SCOPES } from './constants'; const exec = require('child_process').exec; const models = require('./models').default; @@ -381,102 +381,6 @@ _.assignIn(util, { return source; } }), - - /** - * The middleware to validate and get the projectId specified by the timeline request object, - * and set to the request params. This should be called after the validate() middleware, - * and before the permissions() middleware. - * @param {Object} req the express request instance - * @param {Object} res the express response instance - * @param {Function} next the express next middleware - */ - // eslint-disable-next-line valid-jsdoc - validateTimelineRequestBody: (req, res, next) => { - // The timeline refers to a project - if (req.body.param.reference === TIMELINE_REFERENCES.PROJECT) { - // Set projectId to the params so it can be used in the permission check middleware - req.params.projectId = req.body.param.referenceId; - - // Validate projectId to be existed - return models.Project.findOne({ - where: { - id: req.params.projectId, - deletedAt: { $eq: null }, - }, - }) - .then((project) => { - if (!project) { - const apiErr = new Error(`Project not found for project id ${req.params.projectId}`); - apiErr.status = 422; - return next(apiErr); - } - - return next(); - }); - } - - // The timeline refers to a phase - return models.ProjectPhase.findOne({ - where: { - id: req.body.param.referenceId, - deletedAt: { $eq: null }, - }, - }) - .then((phase) => { - if (!phase) { - const apiErr = new Error(`Phase not found for phase id ${req.body.param.referenceId}`); - apiErr.status = 422; - return next(apiErr); - } - - // Set projectId to the params so it can be used in the permission check middleware - req.params.projectId = req.body.param.referenceId; - return next(); - }); - }, - - /** - * The middleware to validate and get the projectId specified by the timelineId from request - * path parameter, and set to the request params. This should be called after the validate() - * middleware, and before the permissions() middleware. - * @param {Object} req the express request instance - * @param {Object} res the express response instance - * @param {Function} next the express next middleware - */ - // eslint-disable-next-line valid-jsdoc - validateTimelineIdParam: (req, res, next) => { - models.Timeline.findById(req.params.timelineId) - .then((timeline) => { - if (!timeline) { - const apiErr = new Error(`Timeline not found for timeline id ${req.params.timelineId}`); - apiErr.status = 404; - return next(apiErr); - } - - // Set timeline to the request to be used in the next middleware - req.timeline = timeline; - - // The timeline refers to a project - if (timeline.reference === TIMELINE_REFERENCES.PROJECT) { - // Set projectId to the params so it can be used in the permission check middleware - req.params.projectId = timeline.referenceId; - return next(); - } - - // The timeline refers to a phase - return models.ProjectPhase.findOne({ - where: { - id: timeline.referenceId, - deletedAt: { $eq: null }, - }, - }) - .then((phase) => { - // Set projectId to the params so it can be used in the permission check middleware - req.params.projectId = phase.projectId; - return next(); - }); - }); - }, }); export default util; diff --git a/swagger.yaml b/swagger.yaml index 3fa16cb4..39bad0d3 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -27,7 +27,7 @@ paths: operationId: findProjects security: - Bearer: [] - description: Retrieve projects that match the filter + description: Retreive projects that match the filter responses: '422': description: Invalid input @@ -344,7 +344,7 @@ paths: operationId: findProjectPhases security: - Bearer: [] - description: Retrieve all project phases. All users who can edit project can access this endpoint. + description: Retreive all project phases. All users who can edit project can access this endpoint. parameters: - name: fields required: false @@ -489,7 +489,7 @@ paths: operationId: findPhaseProducts security: - Bearer: [] - description: Retrieve all phase products. All users who can edit project can access this endpoint. + description: Retreive all phase products. All users who can edit project can access this endpoint. responses: '403': description: No permission or wrong token @@ -655,7 +655,7 @@ paths: operationId: findProjectTemplates security: - Bearer: [] - description: Retrieve all project templates. All user roles can access this endpoint. + description: Retreive all project templates. All user roles can access this endpoint. responses: '403': description: No permission or wrong token @@ -781,7 +781,7 @@ paths: operationId: findProductTemplates security: - Bearer: [] - description: Retrieve all product templates. All user roles can access this endpoint. + description: Retreive all product templates. All user roles can access this endpoint. responses: '403': description: No permission or wrong token @@ -907,7 +907,7 @@ paths: operationId: findProjectTypes security: - Bearer: [] - description: Retrieve all project types. All user roles can access this endpoint. + description: Retreive all project types. All user roles can access this endpoint. responses: '403': description: No permission or wrong token @@ -1025,441 +1025,6 @@ paths: description: Project type successfully removed - /timelines: - get: - tags: - - timeline - operationId: findTimelines - security: - - Bearer: [] - description: Retrieve timelines which its projects are accessible by the user. - parameters: - - name: filter - required: false - type: string - in: query - description: | - Url encoded list of supported filters - - reference - - referenceId - responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" - '200': - description: A list of timelines - schema: - $ref: "#/definitions/TimelineListResponse" - '422': - description: Invalid input - schema: - $ref: "#/definitions/ErrorModel" - post: - tags: - - timeline - operationId: addTimeline - security: - - Bearer: [] - description: Create a timeline. All users who can edit the project can access this endpoint. - parameters: - - in: body - name: body - required: true - schema: - $ref: '#/definitions/TimelineBodyParam' - responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" - '201': - description: Returns the newly created timeline - schema: - $ref: "#/definitions/TimelineResponse" - '422': - description: Invalid input - schema: - $ref: "#/definitions/ErrorModel" - - /timelines/{timelineId}: - get: - tags: - - timeline - description: Retrieve timeline by id. All users who can view the 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" - '422': - description: Invalid input - schema: - $ref: "#/definitions/ErrorModel" - '200': - description: a timeline - schema: - $ref: "#/definitions/TimelineResponse" - parameters: - - $ref: "#/parameters/timelineIdParam" - operationId: getTimeline - - patch: - tags: - - timeline - operationId: updateTimeline - security: - - Bearer: [] - description: Update a timeline. All users who can edit the project can access this endpoint. - responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" - '404': - description: Not found - schema: - $ref: "#/definitions/ErrorModel" - '200': - description: Successfully updated timeline. - schema: - $ref: "#/definitions/TimelineResponse" - '422': - description: Invalid input - schema: - $ref: "#/definitions/ErrorModel" - default: - description: error payload - schema: - $ref: '#/definitions/ErrorModel' - parameters: - - $ref: "#/parameters/timelineIdParam" - - name: body - in: body - required: true - schema: - $ref: "#/definitions/TimelineBodyParam" - - delete: - tags: - - timeline - description: Remove an existing timeline. All users who can edit the project can access this endpoint. - security: - - Bearer: [] - parameters: - - $ref: "#/parameters/timelineIdParam" - responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" - '404': - description: Not found - schema: - $ref: "#/definitions/ErrorModel" - '422': - description: Invalid input - schema: - $ref: "#/definitions/ErrorModel" - '204': - description: Timeline successfully removed - - /timelines/{timelineId}/milestones: - parameters: - - $ref: "#/parameters/timelineIdParam" - get: - tags: - - milestone - operationId: findMilestones - security: - - Bearer: [] - description: Retrieve all milestones. All users who can view the timeline can access this endpoint. - parameters: - - name: sort - required: false - description: sort by `order`. Default is `order asc` - in: query - type: string - responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" - '422': - description: Invalid input - schema: - $ref: "#/definitions/ErrorModel" - '200': - description: A list of milestones - schema: - $ref: "#/definitions/MilestoneListResponse" - post: - tags: - - milestone - operationId: addMilestone - security: - - Bearer: [] - description: Create a milestone. All users who can edit the timeline can access this endpoint. - It also updates the `order` field of all other milestones in the same timeline which have `order` greater than or equal to the `order` specified in the POST body. - parameters: - - in: body - name: body - required: true - schema: - $ref: '#/definitions/MilestoneBodyParam' - responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" - '201': - description: Returns the newly created milestone - schema: - $ref: "#/definitions/MilestoneResponse" - '422': - description: Invalid input - schema: - $ref: "#/definitions/ErrorModel" - - /timelines/{timelineId}/milestones/{milestoneId}: - parameters: - - $ref: "#/parameters/timelineIdParam" - - $ref: "#/parameters/milestoneIdParam" - get: - tags: - - milestone - description: Retrieve milestone by id. All users who can view the timeline can access this endpoint. - security: - - Bearer: [] - responses: - '404': - description: Not found - schema: - $ref: "#/definitions/ErrorModel" - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" - '422': - description: Invalid input - schema: - $ref: "#/definitions/ErrorModel" - '200': - description: a milestone - schema: - $ref: "#/definitions/MilestoneResponse" - operationId: getMilestone - - patch: - tags: - - milestone - operationId: updateMilestone - security: - - Bearer: [] - description: Update a milestone. All users who can edit the timeline can access this endpoint. - For attributes with JSON object type, it would overwrite the existing fields, or add new if the fields don't exist in the JSON object. - responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" - '404': - description: Not found - schema: - $ref: "#/definitions/ErrorModel" - '200': - description: Successfully updated milestone. - schema: - $ref: "#/definitions/MilestoneResponse" - '422': - description: Invalid input - schema: - $ref: "#/definitions/ErrorModel" - default: - description: error payload - schema: - $ref: '#/definitions/ErrorModel' - parameters: - - name: body - in: body - required: true - schema: - $ref: "#/definitions/MilestoneBodyParam" - - delete: - tags: - - milestone - description: Remove an existing milestone. All users who can edit the timeline can access this endpoint. - security: - - Bearer: [] - responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" - '404': - description: Not found - schema: - $ref: "#/definitions/ErrorModel" - '422': - description: Invalid input - schema: - $ref: "#/definitions/ErrorModel" - '204': - description: Milestone successfully removed - - - /productTemplates/{productTemplateId}/milestones: - parameters: - - $ref: "#/parameters/productTemplateIdParam" - get: - tags: - - productMilestoneTemplate - operationId: findMilestoneTemplates - security: - - Bearer: [] - description: Retrieve all milestone templates. All user roles can access this endpoint. - parameters: - - name: sort - required: false - description: sort by `order`. Default is `order asc` - in: query - type: string - responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" - '422': - description: Invalid input - schema: - $ref: "#/definitions/ErrorModel" - '200': - description: A list of milestone templates - schema: - $ref: "#/definitions/MilestoneTemplateListResponse" - post: - tags: - - productMilestoneTemplate - operationId: addMilestoneTemplate - security: - - Bearer: [] - description: Create a milestone template. Only connect manager, connect admin, and admin can access this endpoint. It also updates the `order` field of all other milestone templates in the same product template which have `order` greater than or equal to the `order` specified in the POST body. - parameters: - - in: body - name: body - required: true - schema: - $ref: '#/definitions/MilestoneTemplateBodyParam' - responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" - '201': - description: Returns the newly created milestone template - schema: - $ref: "#/definitions/MilestoneTemplateResponse" - '422': - description: Invalid input - schema: - $ref: "#/definitions/ErrorModel" - - /productTemplates/{productTemplateId}/milestones/{milestoneTemplateId}: - parameters: - - $ref: "#/parameters/productTemplateIdParam" - - $ref: "#/parameters/milestoneTemplateIdParam" - get: - tags: - - productMilestoneTemplate - description: Retrieve milestone 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" - '422': - description: Invalid input - schema: - $ref: "#/definitions/ErrorModel" - '200': - description: a milestone template - schema: - $ref: "#/definitions/MilestoneTemplateResponse" - operationId: getMilestoneTemplate - - patch: - tags: - - productMilestoneTemplate - operationId: updateMilestoneTemplate - security: - - Bearer: [] - description: Update a milestone template. Only connect manager, connect admin, and admin can access this endpoint. - responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" - '404': - description: Not found - schema: - $ref: "#/definitions/ErrorModel" - '200': - description: Successfully updated milestone template. - schema: - $ref: "#/definitions/MilestoneTemplateResponse" - '422': - description: Invalid input - schema: - $ref: "#/definitions/ErrorModel" - default: - description: error payload - schema: - $ref: '#/definitions/ErrorModel' - parameters: - - name: body - in: body - required: true - schema: - $ref: "#/definitions/MilestoneTemplateBodyParam" - - delete: - tags: - - productMilestoneTemplate - description: Remove an existing milestone template. Only connect manager, connect admin, and admin can access this endpoint. - security: - - Bearer: [] - responses: - '403': - description: No permission or wrong token - schema: - $ref: "#/definitions/ErrorModel" - '404': - description: Not found - schema: - $ref: "#/definitions/ErrorModel" - '422': - description: Invalid input - schema: - $ref: "#/definitions/ErrorModel" - '204': - description: Milestone template successfully removed - - - - parameters: @@ -1500,38 +1065,6 @@ parameters: description: project type key required: true type: string - timelineIdParam: - name: timelineId - in: path - description: timeline identifier - required: true - type: integer - format: int64 - minimum: 1 - milestoneIdParam: - name: milestoneId - in: path - description: milestone identifier - required: true - type: integer - format: int64 - minimum: 1 - productTemplateIdParam: - name: productTemplateId - in: path - description: product template identifier - required: true - type: integer - format: int64 - minimum: 1 - milestoneTemplateIdParam: - name: milestoneTemplateId - in: path - description: milestone template identifier - required: true - type: integer - format: int64 - minimum: 1 offsetParam: name: offset description: "number of items to skip. Defaults to 0" @@ -1872,362 +1405,72 @@ definitions: 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 + NewProjectMemberBodyParam: 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" + param: + $ref: "#/definitions/NewProjectMember" - ProjectListResponse: - title: List response + 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: - 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" + param: + $ref: "#/definitions/UpdateProjectMember" - ProjectTemplateRequest: - title: Project template request object + NewProjectAttachment: + title: Project attachment request type: object required: - - name - - key - - category - - scope - - phases + - filePath + - s3Bucket + - title + - contentType properties: - name: + filePath: type: string - description: the project template name - key: + description: path where file is stored + s3Bucket: type: string - description: the project template key + 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: the project template category - scope: - type: object - description: the project template scope - phases: - type: object - description: the project template phases + description: Category of attachment + size: + type: number + format: float + description: The size of attachment - ProjectTemplateBodyParam: - title: Project template body param + NewProjectAttachmentBodyParam: 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" - + $ref: "#/definitions/NewProjectAttachment" - ProjectTemplateResponse: - title: Single project template response object + NewProjectAttachmentResponse: + title: Project attachment object response type: object properties: id: @@ -2243,114 +1486,99 @@ definitions: status: type: string description: http status code - metadata: - $ref: "#/definitions/ResponseMetadata" content: - $ref: "#/definitions/ProjectTemplate" + $ref: "#/definitions/ProjectAttachment" - ProjectTemplateListResponse: - title: Project template list response object + 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 - description: unique id identifying the request - version: + createdBy: + type: integer + format: int64 + description: READ-ONLY. User who created this task + readOnly: true + updatedAt: 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 + 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 - required: - - name - - key - - category - - scope - - phases properties: - name: - type: string - description: the product template name - productKey: - type: string - description: the product template key - icon: + 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: the product template icon - brief: + description: member role on specified project + enum: ["customer", "manager", "copilot"] + createdAt: type: string - description: the product template brief - details: + 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: the product template details - aliases: - type: object - description: the product template aliases - template: - type: object - description: the product template template + 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 - 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" - ProjectUpgradeResponse: - title: Project upgrade response object + NewProjectMemberResponse: + title: Project member object response type: object properties: id: @@ -2366,11 +1594,11 @@ definitions: status: type: string description: http status code - metadata: - $ref: "#/definitions/ResponseMetadata" + content: + $ref: "#/definitions/ProjectMember" - ProductTemplateResponse: - title: Single product template response object + UpdateProjectMemberResponse: + title: Project member object response type: object properties: id: @@ -2386,18 +1614,16 @@ definitions: status: type: string description: http status code - metadata: - $ref: "#/definitions/ResponseMetadata" content: - $ref: "#/definitions/ProductTemplate" + $ref: "#/definitions/ProjectMember" - ProductTemplateListResponse: - title: Product template list response object + + ProjectResponse: + title: Single project object type: object properties: id: type: string - readOnly: true description: unique id identifying the request version: type: string @@ -2409,93 +1635,11 @@ definitions: 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" - + $ref: "#/definitions/Project" - ProjectPhaseResponse: - title: Single project phase response object + UpdateProjectResponse: + title: response with original and updated project object type: object properties: id: @@ -2511,13 +1655,16 @@ definitions: status: type: string description: http status code - metadata: - $ref: "#/definitions/ResponseMetadata" content: - $ref: "#/definitions/ProjectPhase" + type: object + properties: + original: + $ref: "#/definitions/Project" + updated: + $ref: "#/definitions/Project" - ProjectPhaseListResponse: - title: Project phase list response object + ProjectListResponse: + title: List response type: object properties: id: @@ -2539,49 +1686,45 @@ definitions: content: type: array items: - $ref: "#/definitions/ProjectPhase" - + $ref: "#/definitions/Project" - PhaseProductRequest: - title: Phase product request object + ProjectTemplateRequest: + title: Project template request object type: object + required: + - name + - key + - category + - scope + - phases 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: + description: the project template name + key: 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: + description: the project template key + category: + type: string + description: the project template category + scope: type: object - description: the phase product details + description: the project template scope + phases: + type: object + description: the project template phases - PhaseProductBodyParam: - title: Phase product body param + ProjectTemplateBodyParam: + title: Project template body param type: object required: - param properties: param: - $ref: "#/definitions/PhaseProductRequest" + $ref: "#/definitions/ProjectTemplateRequest" - PhaseProduct: - title: Phase product object + ProjectTemplate: + title: Project template object allOf: - type: object required: @@ -2613,11 +1756,11 @@ definitions: format: int64 description: READ-ONLY. User that last updated this object readOnly: true - - $ref: "#/definitions/PhaseProductRequest" + - $ref: "#/definitions/ProjectTemplateRequest" - PhaseProductResponse: - title: Single phase product response object + ProjectTemplateResponse: + title: Single project template response object type: object properties: id: @@ -2636,10 +1779,10 @@ definitions: metadata: $ref: "#/definitions/ResponseMetadata" content: - $ref: "#/definitions/PhaseProduct" + $ref: "#/definitions/ProjectTemplate" - PhaseProductListResponse: - title: Phase product list response object + ProjectTemplateListResponse: + title: Project template list response object type: object properties: id: @@ -2659,66 +1802,66 @@ definitions: metadata: $ref: "#/definitions/ResponseMetadata" content: - type: array - items: - $ref: "#/definitions/PhaseProduct" - - + type: array + items: + $ref: "#/definitions/ProjectTemplate" - ProjectTypeRequest: - title: Project type request object + ProductTemplateRequest: + title: Product template request object type: object required: - - displayName + - name + - key + - category + - scope + - phases properties: - displayName: + name: type: string - description: the project type display name - - ProjectTypeBodyParam: - title: Project type body param - type: object - required: - - param - properties: - param: - $ref: "#/definitions/ProjectTypeRequest" - - ProjectTypeCreateRequest: - title: Project type creation request object - type: object - allOf: - - type: object - required: - - key - properties: - key: - type: string - description: the project type key - - $ref: "#/definitions/ProjectTypeRequest" + 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 - ProjectTypeCreateBodyParam: - title: Project type creation body param + ProductTemplateBodyParam: + title: Product template body param type: object required: - param properties: param: - $ref: "#/definitions/ProjectTypeCreateRequest" + $ref: "#/definitions/ProductTemplateRequest" - ProjectType: - title: Project type object + ProductTemplate: + title: Product template object allOf: - type: object required: + - id - createdAt - createdBy - updatedAt - updatedBy properties: - key: - type: string - description: the project type key + id: + type: number + format: int64 + description: the id createdAt: type: string description: Datetime (GMT) when object was created @@ -2737,11 +1880,30 @@ definitions: format: int64 description: READ-ONLY. User that last updated this object readOnly: true - - $ref: "#/definitions/ProjectTypeCreateRequest" + - $ref: "#/definitions/ProductTemplateRequest" + ProjectUpgradeResponse: + title: Project upgrade 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" - ProjectTypeResponse: - title: Single project type response object + ProductTemplateResponse: + title: Single product template response object type: object properties: id: @@ -2760,10 +1922,10 @@ definitions: metadata: $ref: "#/definitions/ResponseMetadata" content: - $ref: "#/definitions/ProjectType" + $ref: "#/definitions/ProductTemplate" - ProjectTypeListResponse: - title: Project type list response object + ProductTemplateListResponse: + title: Product template list response object type: object properties: id: @@ -2785,54 +1947,52 @@ definitions: content: type: array items: - $ref: "#/definitions/ProjectType" - + $ref: "#/definitions/ProductTemplate" - TimelineRequest: - title: Timeline request object + ProjectPhaseRequest: + title: Project phase request object type: object required: - name + - status - startDate - - reference - - referenceId + - endDate properties: name: type: string - description: the timeline name - description: + description: the project phase name + status: type: string - description: the timeline description + description: the project phase status startDate: type: string format: date - description: the timeline start date + description: the project phase start date endDate: type: string format: date - description: the timeline end date - reference: - type: string - enum: - - project - - phase - description: the timeline reference - referenceId: + description: the project phase end date + budget: type: number - format: long - description: the timeline reference id (project id or phase id, corresponding to the `reference`) + description: the project phase budget + progress: + type: number + description: the project phase progress + details: + type: object + description: the project phase details - TimelineBodyParam: - title: Timeline body param + ProjectPhaseBodyParam: + title: Project phase body param type: object required: - param properties: param: - $ref: "#/definitions/TimelineRequest" + $ref: "#/definitions/ProjectPhaseRequest" - Timeline: - title: Timeline object + ProjectPhase: + title: Project phase object allOf: - type: object required: @@ -2864,10 +2024,11 @@ definitions: format: int64 description: READ-ONLY. User that last updated this object readOnly: true - - $ref: "#/definitions/TimelineRequest" + - $ref: "#/definitions/ProjectPhaseRequest" - TimelineResponse: - title: Single timeline response object + + ProjectPhaseResponse: + title: Single project phase response object type: object properties: id: @@ -2886,10 +2047,10 @@ definitions: metadata: $ref: "#/definitions/ResponseMetadata" content: - $ref: "#/definitions/Timeline" + $ref: "#/definitions/ProjectPhase" - TimelineListResponse: - title: Timeline list response object + ProjectPhaseListResponse: + title: Project phase list response object type: object properties: id: @@ -2911,82 +2072,49 @@ definitions: content: type: array items: - $ref: "#/definitions/Timeline" + $ref: "#/definitions/ProjectPhase" + - MilestoneRequest: - title: Milestone request object + PhaseProductRequest: + title: Phase product request object type: object - required: - - name - - duration - - startDate - - status - - type - - order - - plannedText - - activeText - - completedText - - blockedText properties: name: type: string - description: the milestone name - description: - type: string - description: the milestone description - duration: + description: the phase product name + directProjectId: type: number - format: integer - description: the milestone duration - startDate: - type: string - format: date - description: the milestone start date - endDate: - type: string - format: date - description: the milestone end date - completionDate: - type: string - format: date - description: the milestone completion date - status: - type: string - description: the milestone status + 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 milestone type + 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 milestone details - order: - type: number - format: integer - description: the milestone order - plannedText: - type: string - description: the milestone planned text - activeText: - type: string - description: the milestone active text - completedText: - type: string - description: the milestone completed text - blockedText: - type: string - description: the milestone blocked text + description: the phase product details - MilestoneBodyParam: - title: Milestone body param + PhaseProductBodyParam: + title: Phase product body param type: object required: - param properties: param: - $ref: "#/definitions/MilestoneRequest" + $ref: "#/definitions/PhaseProductRequest" - Milestone: - title: Milestone object + PhaseProduct: + title: Phase product object allOf: - type: object required: @@ -3018,10 +2146,11 @@ definitions: format: int64 description: READ-ONLY. User that last updated this object readOnly: true - - $ref: "#/definitions/MilestoneRequest" + - $ref: "#/definitions/PhaseProductRequest" - MilestoneResponse: - title: Single milestone response object + + PhaseProductResponse: + title: Single phase product response object type: object properties: id: @@ -3040,10 +2169,10 @@ definitions: metadata: $ref: "#/definitions/ResponseMetadata" content: - $ref: "#/definitions/Milestone" + $ref: "#/definitions/PhaseProduct" - MilestoneListResponse: - title: Milestone list response object + PhaseProductListResponse: + title: Phase product list response object type: object properties: id: @@ -3065,60 +2194,64 @@ definitions: content: type: array items: - $ref: "#/definitions/Milestone" - - - MilestoneTemplateRequest: - title: Milestone template request object + $ref: "#/definitions/PhaseProduct" + + + + ProjectTypeRequest: + title: Project type request object type: object required: - - name - - duration - - type - - order + - displayName properties: - name: - type: string - description: the milestone template name - description: - type: string - description: the milestone template description - duration: - type: number - format: integer - description: the milestone template duration - type: + displayName: type: string - description: the milestone template type - order: - type: number - format: integer - description: the milestone template order + description: the project type display name - MilestoneTemplateBodyParam: - title: Milestone template body param + ProjectTypeBodyParam: + title: Project type body param type: object required: - param properties: param: - $ref: "#/definitions/MilestoneTemplateRequest" + $ref: "#/definitions/ProjectTypeRequest" - MilestoneTemplate: - title: Milestone template object + ProjectTypeCreateRequest: + title: Project type creation request object + type: object + allOf: + - type: object + required: + - key + properties: + key: + type: string + description: the project type key + - $ref: "#/definitions/ProjectTypeRequest" + + ProjectTypeCreateBodyParam: + title: Project type creation body param + type: object + required: + - param + properties: + param: + $ref: "#/definitions/ProjectTypeCreateRequest" + + ProjectType: + title: Project type object allOf: - type: object required: - - id - createdAt - createdBy - updatedAt - updatedBy properties: - id: - type: number - format: int64 - description: the id + key: + type: string + description: the project type key createdAt: type: string description: Datetime (GMT) when object was created @@ -3137,10 +2270,11 @@ definitions: format: int64 description: READ-ONLY. User that last updated this object readOnly: true - - $ref: "#/definitions/MilestoneTemplateRequest" + - $ref: "#/definitions/ProjectTypeCreateRequest" + - MilestoneTemplateResponse: - title: Single milestone template response object + ProjectTypeResponse: + title: Single project type response object type: object properties: id: @@ -3159,10 +2293,10 @@ definitions: metadata: $ref: "#/definitions/ResponseMetadata" content: - $ref: "#/definitions/MilestoneTemplate" + $ref: "#/definitions/ProjectType" - MilestoneTemplateListResponse: - title: Milestone template list response object + ProjectTypeListResponse: + title: Project type list response object type: object properties: id: @@ -3184,4 +2318,4 @@ definitions: content: type: array items: - $ref: "#/definitions/MilestoneTemplate" + $ref: "#/definitions/ProjectType"