From 1bccaa1991d2171261b570949584433d0ebe279b Mon Sep 17 00:00:00 2001 From: eisbilir Date: Mon, 19 Jul 2021 02:35:06 +0300 Subject: [PATCH 01/17] phase members --- config/default.json | 12 +- docs/Project API.postman_collection.json | 961 +++++++++--------- docs/swagger.yaml | 147 +++ .../20210718_project_phase_member_table.sql | 13 + package-lock.json | 363 ++++--- src/events/projects/index.js | 4 +- src/models/projectPhase.js | 1 + src/models/projectPhaseMember.js | 62 ++ src/permissions/index.js | 4 + src/routes/index.js | 7 + src/routes/phaseMembers/delete.js | 80 ++ src/routes/phaseMembers/delete.spec.js | 158 +++ src/routes/phaseMembers/list.js | 66 ++ src/routes/phaseMembers/list.spec.js | 160 +++ src/routes/phaseMembers/update.js | 91 ++ src/routes/phaseMembers/update.spec.js | 178 ++++ src/routes/phases/create.spec.js | 1 - src/routes/phases/get.js | 22 +- src/routes/phases/list.js | 67 +- src/routes/phases/list.spec.js | 76 +- 20 files changed, 1805 insertions(+), 668 deletions(-) create mode 100644 migrations/20210718_project_phase_member_table.sql create mode 100644 src/models/projectPhaseMember.js create mode 100644 src/routes/phaseMembers/delete.js create mode 100644 src/routes/phaseMembers/delete.spec.js create mode 100644 src/routes/phaseMembers/list.js create mode 100644 src/routes/phaseMembers/list.spec.js create mode 100644 src/routes/phaseMembers/update.js create mode 100644 src/routes/phaseMembers/update.spec.js diff --git a/config/default.json b/config/default.json index 9e333c67..9cf176ae 100644 --- a/config/default.json +++ b/config/default.json @@ -27,9 +27,9 @@ "metadataDocType": "doc", "metadataDocDefaultId": 1 }, - "connectProjectUrl":"", + "connectProjectUrl": "", "dbConfig": { - "masterUrl": "", + "masterUrl": "postgres://coder:mysecretpassword@localhost:5432/projectsdb", "maxPoolSize": 50, "minPoolSize": 4, "idleTimeout": 1000 @@ -48,11 +48,11 @@ "maxPhaseProductCount": 1, "TOKEN_CACHE_TIME": "86000", "whitelistedOriginsForUserIdAuth": "[\"https:\/\/topcoder-newauth.auth0.com\/\",\"https:\/\/api.topcoder-dev.com\"]", - "EMAIL_INVITE_FROM_NAME":"Topcoder", - "EMAIL_INVITE_FROM_EMAIL":"noreply@connect.topcoder.com", + "EMAIL_INVITE_FROM_NAME": "Topcoder", + "EMAIL_INVITE_FROM_EMAIL": "noreply@connect.topcoder.com", "inviteEmailSubject": "You are invited to Topcoder", "inviteEmailSectionTitle": "Project Invitation", - "connectUrl":"https://connect.topcoder-dev.com", + "connectUrl": "https://connect.topcoder-dev.com", "accountsAppUrl": "https://accounts.topcoder-dev.com", "MAX_REVISION_NUMBER": 100, "UNIQUE_GMAIL_VALIDATION": false, @@ -84,4 +84,4 @@ "CLIENT_ID": "" }, "sfdcBillingAccountNameField": "Billing_Account_Name__c" -} +} \ No newline at end of file diff --git a/docs/Project API.postman_collection.json b/docs/Project API.postman_collection.json index 71167a99..0146caa6 100644 --- a/docs/Project API.postman_collection.json +++ b/docs/Project API.postman_collection.json @@ -1,17 +1,15 @@ { "info": { - "_postman_id": "c69ab4dd-8c6a-48c9-ba48-c6663b1a0c81", + "_postman_id": "3eba12ae-a066-4d5a-bdd5-3121377e476b", "name": "Project API", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, "item": [ { "name": "Project Attachments", - "description": null, "item": [ { "name": "bookmarks", - "description": null, "item": [ { "name": " Create project without bookmarks", @@ -117,10 +115,6 @@ "value": "application/json" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}", "host": [ @@ -237,10 +231,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects", "host": [ @@ -253,8 +243,7 @@ }, "response": [] } - ], - "_postman_isSubFolder": true + ] }, { "name": "Upload file attachment", @@ -262,7 +251,6 @@ { "listen": "test", "script": { - "id": "6547ada6-53f5-4e2d-bda0-f0ec5bfbe38f", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -310,7 +298,6 @@ { "listen": "test", "script": { - "id": "6547ada6-53f5-4e2d-bda0-f0ec5bfbe38f", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -508,10 +495,6 @@ "value": "application/json" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/attachments", "host": [ @@ -599,7 +582,6 @@ }, { "name": "Project With TemplateId issue", - "description": null, "item": [ { "name": "Create project with templateId (not existed)", @@ -637,7 +619,6 @@ { "listen": "test", "script": { - "id": "ed52d7f8-829b-40ec-8583-7e8dbcc6741c", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -680,7 +661,6 @@ }, { "name": "Project Members", - "description": null, "item": [ { "name": "Create project member with no payload", @@ -754,7 +734,6 @@ { "listen": "test", "script": { - "id": "82218ddf-4bd4-485f-bcd4-1d653c674680", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -835,7 +814,6 @@ { "listen": "test", "script": { - "id": "ba7b3265-aaba-4cca-8a53-819c9e96cfae", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -883,7 +861,6 @@ { "listen": "test", "script": { - "id": "87ab173c-8a4b-4d12-bfe7-ebcd272289f1", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -1085,10 +1062,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/members", "host": [ @@ -1114,10 +1087,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/members?role=customer", "host": [ @@ -1153,10 +1122,6 @@ "value": "application/json" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/members/{{memberId}}", "host": [ @@ -1174,7 +1139,7 @@ "response": [] }, { - "name": "Delete project member Copy", + "name": "Delete project member", "request": { "method": "DELETE", "header": [ @@ -1244,7 +1209,6 @@ }, { "name": "Project Members Invites", - "description": null, "item": [ { "name": "List project member invite", @@ -1327,7 +1291,6 @@ { "listen": "test", "script": { - "id": "320b75fe-958d-44ee-b2d2-1716b7b3e207", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -1382,7 +1345,6 @@ { "listen": "test", "script": { - "id": "3835313a-bb42-487a-b17e-4d687535d7e5", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -1483,10 +1445,6 @@ "value": "application/json" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/invites/{{inviteId}}", "host": [ @@ -1661,7 +1619,6 @@ { "listen": "prerequest", "script": { - "id": "522949a9-3d94-4103-92b2-976af332f203", "type": "text/javascript", "exec": [ "" @@ -1671,7 +1628,6 @@ { "listen": "test", "script": { - "id": "df2755ee-59a5-4d8d-a6ad-6416b697c894", "type": "text/javascript", "exec": [ "" @@ -1682,7 +1638,6 @@ }, { "name": "Projects", - "description": "Requests for all things projects.", "item": [ { "name": "Create project without payload", @@ -1752,7 +1707,6 @@ { "listen": "test", "script": { - "id": "5d951b47-5aac-4af6-a24b-ef6c998b913e", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -1833,10 +1787,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}", "host": [ @@ -1861,10 +1811,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}?fields=id,name,description,members.id,members.projectId", "host": [ @@ -1895,10 +1841,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects", "host": [ @@ -1922,10 +1864,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects?perPage=1&page=1", "host": [ @@ -1959,10 +1897,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects?type=generic", "host": [ @@ -1992,10 +1926,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects?id=1&id=2", "host": [ @@ -2029,10 +1959,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects?sort=type asc", "host": [ @@ -2062,10 +1988,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects?fields=id,name,description", "host": [ @@ -2095,10 +2017,6 @@ "value": "Bearer {{jwt-token-copilot-40051332}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects", "host": [ @@ -2648,11 +2566,11 @@ }, "response": [] } - ] + ], + "description": "Requests for all things projects." }, { "name": "Workstream", - "description": "Requests for all things projects.", "item": [ { "name": "Create workstream without payload", @@ -2693,7 +2611,6 @@ { "listen": "test", "script": { - "id": "15506f7a-77d3-46cb-9b37-41015ffbfdbc", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -2778,10 +2695,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}", "host": [ @@ -2808,10 +2721,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/workstreams", "host": [ @@ -2891,11 +2800,11 @@ }, "response": [] } - ] + ], + "description": "Requests for all things projects." }, { "name": "Work", - "description": "Requests for all things projects.", "item": [ { "name": "Create work without payload", @@ -2938,7 +2847,6 @@ { "listen": "test", "script": { - "id": "34d06fac-76f6-47b8-9b70-6e9c558e2bf1", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -3062,10 +2970,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}/works/{{workId}}", "host": [ @@ -3094,10 +2998,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}/works", "host": [ @@ -3125,10 +3025,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}/works?sort=startDate desc", "host": [ @@ -3162,10 +3058,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}/works?fields=status,name,budget", "host": [ @@ -3291,11 +3183,11 @@ }, "response": [] } - ] + ], + "description": "Requests for all things projects." }, { "name": "Work Item", - "description": "Requests for all things projects.", "item": [ { "name": "Create work item without payload", @@ -3431,10 +3323,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}/works/{{workId}}/workitems/{{itemId}}", "host": [ @@ -3465,10 +3353,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}/works/{{workId}}/workitems", "host": [ @@ -3560,11 +3444,11 @@ }, "response": [] } - ] + ], + "description": "Requests for all things projects." }, { "name": "Work Management Permission", - "description": "Requests for all things projects.", "item": [ { "name": "Create work management permission without payload", @@ -3605,7 +3489,6 @@ { "listen": "test", "script": { - "id": "305f3d68-6e9f-4c4d-b031-7487986a93e2", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -3653,7 +3536,6 @@ { "listen": "test", "script": { - "id": "305f3d68-6e9f-4c4d-b031-7487986a93e2", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -3738,10 +3620,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/workManagementPermission/{{workManagementPermissionId}}", "host": [ @@ -3768,10 +3646,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/workManagementPermission", "host": [ @@ -3797,10 +3671,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/workManagementPermission?filter=template", "host": [ @@ -3832,10 +3702,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/workManagementPermission?filter=invalid%3D2%26projectTemplateId%3D1", "host": [ @@ -3867,10 +3733,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/workManagementPermission?filter=projectTemplateId%3D1", "host": [ @@ -3956,11 +3818,11 @@ }, "response": [] } - ] + ], + "description": "Requests for all things projects." }, { "name": "Permissions", - "description": null, "item": [ { "name": "Get permissions - 404", @@ -3972,10 +3834,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/9999/permissions", "host": [ @@ -4069,10 +3927,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/permissions", "host": [ @@ -4098,10 +3952,6 @@ "value": "Bearer {{jwt-token-manager-40051334}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/permissions", "host": [ @@ -4122,7 +3972,6 @@ { "listen": "prerequest", "script": { - "id": "5197d1ec-429c-4a6f-9e9c-3ec3cd6f292a", "type": "text/javascript", "exec": [ "" @@ -4132,7 +3981,6 @@ { "listen": "test", "script": { - "id": "cc0cbbf1-54d1-481f-b8fa-a6dc4c80e993", "type": "text/javascript", "exec": [ "" @@ -4143,7 +3991,6 @@ }, { "name": "WorkManagementForTemplate", - "description": "Requests for all things projects.", "item": [ { "name": "Create workstream with valid values", @@ -4477,11 +4324,11 @@ }, "response": [] } - ] + ], + "description": "Requests for all things projects." }, { "name": "EventHandling and Integration with Direct Project API", - "description": null, "item": [ { "name": "mock direct projects", @@ -4497,10 +4344,6 @@ "value": "application/json" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "https://api.topcoder-dev.com/v3/direct/projects", "protocol": "https", @@ -4714,7 +4557,6 @@ { "listen": "prerequest", "script": { - "id": "ef96ac6a-0fc0-4a64-a4fe-5390e17afe67", "type": "text/javascript", "exec": [ "" @@ -4724,7 +4566,6 @@ { "listen": "test", "script": { - "id": "12f9d794-0872-4058-aafa-77b89e72025b", "type": "text/javascript", "exec": [ "" @@ -4735,7 +4576,6 @@ }, { "name": "Project Phase", - "description": null, "item": [ { "name": "Create Phase", @@ -4743,7 +4583,6 @@ { "listen": "test", "script": { - "id": "7050133a-b934-4faf-8101-d2e80b5c0710", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -4790,7 +4629,6 @@ { "listen": "test", "script": { - "id": "2f771afe-7b4e-4260-b04d-324e880eb61b", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -4837,7 +4675,6 @@ { "listen": "test", "script": { - "id": "8415ad98-b3f6-4330-88b6-e1830da2e4f9", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -4892,10 +4729,6 @@ "value": "application/json" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/phases", "host": [ @@ -4924,10 +4757,6 @@ "value": "application/json" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/phases?fields=status,name,budget", "host": [ @@ -4948,6 +4777,40 @@ }, "response": [] }, + { + "name": "List Phase with member field", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{api-url}}/projects/{{projectId}}/phases?fields=status,name,budget,members,products", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "{{projectId}}", + "phases" + ], + "query": [ + { + "key": "fields", + "value": "status,name,budget,members,products" + } + ] + } + }, + "response": [] + }, { "name": "List Phase with sort", "request": { @@ -4962,10 +4825,6 @@ "value": "application/json" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/phases?sort=status desc", "host": [ @@ -5000,10 +4859,6 @@ "value": "application/json" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/phases?sort=order desc", "host": [ @@ -5024,6 +4879,40 @@ }, "response": [] }, + { + "name": "List Phase with memberOnly", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{api-url}}/projects/{{projectId}}/phases?memberOnly=true", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "{{projectId}}", + "phases" + ], + "query": [ + { + "key": "memberOnly", + "value": "true" + } + ] + } + }, + "response": [] + }, { "name": "Get Phase", "request": { @@ -5038,10 +4927,6 @@ "value": "application/json" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}", "host": [ @@ -5160,7 +5045,6 @@ }, { "name": "Phase Products", - "description": null, "item": [ { "name": "Create Phase Product", @@ -5168,7 +5052,6 @@ { "listen": "test", "script": { - "id": "77f089b3-cbe6-4fb4-b54f-2a52d138a050", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -5221,12 +5104,100 @@ "value": "Bearer {{jwt-token}}" } ], + "url": { + "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/products", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "{{projectId}}", + "phases", + "{{phaseId}}", + "products" + ] + } + }, + "response": [] + }, + { + "name": "Get Phase Product", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + } + ], + "url": { + "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/products/{{phaseProductId}}", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "{{projectId}}", + "phases", + "{{phaseId}}", + "products", + "{{phaseProductId}}" + ] + } + }, + "response": [] + }, + { + "name": "Update Phase Product", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"name\": \"test phase product xxx\",\n\t\"type\": \"type 2\",\n\t\"templateId\": 10,\n\t\"estimatedPrice\": 1.234567,\n\t\"actualPrice\": 2.34567,\n\t\"details\": {\n\t\t\"message\": \"this is a JSON type. You can use any json\"\n\t}\n}" + }, + "url": { + "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/products/{{phaseProductId}}", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "{{projectId}}", + "phases", + "{{phaseId}}", + "products", + "{{phaseProductId}}" + ] + } + }, + "response": [] + }, + { + "name": "Delete Phase Product", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + } + ], "body": { "mode": "raw", "raw": "" }, "url": { - "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/products", + "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/products/{{phaseProductId}}", "host": [ "{{api-url}}" ], @@ -5235,28 +5206,302 @@ "{{projectId}}", "phases", "{{phaseId}}", - "products" + "products", + "{{phaseProductId}}" ] } - }, - "response": [] + }, + "response": [] + } + ] + }, + { + "name": "Phase Members", + "item": [ + { + "name": "Before Start", + "item": [ + { + "name": "Create project type", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 201\", function () {", + " pm.response.to.have.status(201);", + " if(pm.response.status === \"Created\") {", + " const response = pm.response.json()", + " pm.environment.set(\"projectTypeId\", response.key);", + " }", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"key\": \"new key\",\r\n \"displayName\": \"new displayName\",\r\n \"icon\": \"http://example.com/icon4.ico\",\r\n \t\"question\": \"question 4\",\r\n \t\"info\": \"info 4\",\r\n \t\"aliases\": [\"key-41\", \"key_42\"],\r\n \t\"metadata\": {}\r\n }" + }, + "url": { + "raw": "{{api-url}}/projects/metadata/projectTypes", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "metadata", + "projectTypes" + ] + } + }, + "response": [] + }, + { + "name": "Create project", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 201\", function () {", + " pm.response.to.have.status(201);", + " if(pm.response.status === \"Created\") {", + " const response = pm.response.json()", + " pm.environment.set(\"projectId\", response.id);", + " }", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"name\": \"test project\",\n\t\"description\": \"Hello I am a test project\",\n\t\"type\": \"{{projectTypeId}}\"\n}" + }, + "url": { + "raw": "{{api-url}}/projects", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects" + ] + }, + "description": "Valid request body. Project should be created successfully." + }, + "response": [] + }, + { + "name": "Create project member - 1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 201\", function () {", + " pm.response.to.have.status(201);", + " if(pm.response.status === \"Created\") {", + " const response = pm.response.json()", + " pm.environment.set(\"phaseMemberId-1\", response.userId);", + " }", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"userId\": \"40158994\",\n \"role\": \"copilot\"\n}" + }, + "url": { + "raw": "{{api-url}}/projects/{{projectId}}/members", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "{{projectId}}", + "members" + ] + }, + "description": "If the request payload is valid, than project member should be created." + }, + "response": [] + }, + { + "name": "Create project member - 2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 201\", function () {", + " pm.response.to.have.status(201);", + " if(pm.response.status === \"Created\") {", + " const response = pm.response.json()", + " pm.environment.set(\"phaseMemberId-2\", response.userId);", + " }", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"userId\": \"40153800\",\n \"role\": \"copilot\"\n}" + }, + "url": { + "raw": "{{api-url}}/projects/{{projectId}}/members", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "{{projectId}}", + "members" + ] + }, + "description": "If the request payload is valid, than project member should be created." + }, + "response": [] + }, + { + "name": "Create Phase", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 201\", function () {", + " pm.response.to.have.status(201);", + " if(pm.response.status === \"Created\") {", + " const response = pm.response.json()", + " pm.environment.set(\"phaseId\", response.id);", + " }", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"name\": \"test project phase\",\n\t\"status\": \"active\",\n\t\"startDate\": \"2018-05-15T00:00:00\",\n\t\"endDate\": \"2018-05-16T00:00:00\",\n\t\"budget\": 20,\n\t\"details\": {\n\t\t\"aDetails\": \"a details\"\n\t}\n}" + }, + "url": { + "raw": "{{api-url}}/projects/{{projectId}}/phases", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "{{projectId}}", + "phases" + ] + } + }, + "response": [] + } + ] }, { - "name": "Get Phase Product", + "name": "Update Phase Members", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], "request": { - "method": "GET", + "method": "POST", "header": [ { "key": "Authorization", "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" } ], "body": { "mode": "raw", - "raw": "" + "raw": "{\n\t\"userIds\": [{{phaseMemberId-1}},{{phaseMemberId-2}}]\n}" }, "url": { - "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/products/{{phaseProductId}}", + "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/members", "host": [ "{{api-url}}" ], @@ -5265,17 +5510,29 @@ "{{projectId}}", "phases", "{{phaseId}}", - "products", - "{{phaseProductId}}" + "members" ] } }, "response": [] }, { - "name": "Update Phase Product", + "name": "List Phase Members", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], "request": { - "method": "PATCH", + "method": "GET", "header": [ { "key": "Authorization", @@ -5286,12 +5543,8 @@ "value": "application/json" } ], - "body": { - "mode": "raw", - "raw": "{\n\t\"name\": \"test phase product xxx\",\n\t\"type\": \"type 2\",\n\t\"templateId\": 10,\n\t\"estimatedPrice\": 1.234567,\n\t\"actualPrice\": 2.34567,\n\t\"details\": {\n\t\t\"message\": \"this is a JSON type. You can use any json\"\n\t}\n}" - }, "url": { - "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/products/{{phaseProductId}}", + "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/members", "host": [ "{{api-url}}" ], @@ -5300,21 +5553,37 @@ "{{projectId}}", "phases", "{{phaseId}}", - "products", - "{{phaseProductId}}" + "members" ] } }, "response": [] }, { - "name": "Delete Phase Product", + "name": "Delete Phase Member", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 204\", function () {\r", + " pm.response.to.have.status(204);\r", + "});" + ], + "type": "text/javascript" + } + } + ], "request": { "method": "DELETE", "header": [ { "key": "Authorization", "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" } ], "body": { @@ -5322,7 +5591,7 @@ "raw": "" }, "url": { - "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/products/{{phaseProductId}}", + "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/members/{{phaseMemberId-2}}", "host": [ "{{api-url}}" ], @@ -5331,8 +5600,8 @@ "{{projectId}}", "phases", "{{phaseId}}", - "products", - "{{phaseProductId}}" + "members", + "{{phaseMemberId-2}}" ] } }, @@ -5342,7 +5611,6 @@ }, { "name": "Project Templates", - "description": null, "item": [ { "name": "Create project template", @@ -5350,7 +5618,6 @@ { "listen": "test", "script": { - "id": "2f79c07b-8076-4715-abf7-1d6903df444f", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -5397,7 +5664,6 @@ { "listen": "test", "script": { - "id": "4c442ea3-0834-4a30-8044-a4e94fd4ea2d", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -5444,7 +5710,6 @@ { "listen": "test", "script": { - "id": "7d0ae3ca-fe2d-40eb-b5c8-9b03955babec", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -5563,10 +5828,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/projectTemplates", "host": [ @@ -5595,10 +5856,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/projectTemplates/{{projectTemplateId}}", "host": [ @@ -5852,7 +6109,6 @@ }, { "name": "Product Templates", - "description": null, "item": [ { "name": "Create product template", @@ -5860,7 +6116,6 @@ { "listen": "test", "script": { - "id": "b5aaf185-6026-4b58-b9b8-56616109cd5a", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -5907,7 +6162,6 @@ { "listen": "test", "script": { - "id": "d5a2af2e-97d2-415c-a533-1d52dd4003c7", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -6026,10 +6280,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/productTemplates", "host": [ @@ -6058,10 +6308,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/productTemplates/{{productTemplateId}}", "host": [ @@ -6255,7 +6501,6 @@ }, { "name": "Project Type", - "description": null, "item": [ { "name": "Create project type", @@ -6263,7 +6508,6 @@ { "listen": "test", "script": { - "id": "fbc45946-a3f2-433a-8ec5-0af82b69d2bd", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -6318,10 +6562,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/projectTypes", "host": [ @@ -6350,10 +6590,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/projectTypes/{{projectTypeId}}", "host": [ @@ -6439,7 +6675,6 @@ }, { "name": "Org Config", - "description": null, "item": [ { "name": "Create org config", @@ -6447,7 +6682,6 @@ { "listen": "test", "script": { - "id": "fbc45946-a3f2-433a-8ec5-0af82b69d2bd", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -6505,10 +6739,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/orgConfig", "host": [ @@ -6537,10 +6767,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/orgConfig?orgId={{orgStrId}}", "host": [ @@ -6576,10 +6802,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/orgConfig?orgId={{orgStrId}}&configName={{orgConfigName}}", "host": [ @@ -6676,7 +6898,6 @@ { "listen": "prerequest", "script": { - "id": "2e274cc9-22e6-4dd2-9eee-c4f1fd98253d", "type": "text/javascript", "exec": [ "" @@ -6686,7 +6907,6 @@ { "listen": "test", "script": { - "id": "9d171dbf-2a50-4483-b172-ce240ac09413", "type": "text/javascript", "exec": [ "" @@ -6697,7 +6917,6 @@ }, { "name": "Product Category", - "description": null, "item": [ { "name": "Create product category", @@ -6705,7 +6924,6 @@ { "listen": "test", "script": { - "id": "06156797-ceb2-4f8c-9448-5c453adb7b7a", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -6760,10 +6978,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/productCategories", "host": [ @@ -6792,10 +7006,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/productCategories/{{productCategoryId}}", "host": [ @@ -6892,7 +7102,6 @@ { "listen": "prerequest", "script": { - "id": "f0092ef5-e624-4c25-87b2-b6a9e4c81ec8", "type": "text/javascript", "exec": [ "" @@ -6902,7 +7111,6 @@ { "listen": "test", "script": { - "id": "9183c429-a5e0-4bf9-96a2-89f4d66e9b0d", "type": "text/javascript", "exec": [ "" @@ -6913,7 +7121,6 @@ }, { "name": "Project upgrade", - "description": "Request to migrate projects.", "item": [ { "name": "Migrate project", @@ -7043,11 +7250,11 @@ }, "response": [] } - ] + ], + "description": "Request to migrate projects." }, { "name": "Timeline", - "description": null, "item": [ { "name": "Create timeline", @@ -7055,7 +7262,6 @@ { "listen": "test", "script": { - "id": "c066e7d4-537f-406e-a768-ec4bf73a2634", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -7100,7 +7306,6 @@ { "listen": "test", "script": { - "id": "ee729ed9-0072-4821-9141-3615ff66f728", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -7189,10 +7394,6 @@ "type": "text" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/timelines?reference=project&referenceId={{projectId}}", "host": [ @@ -7229,10 +7430,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/timelines/{{timelineId}}", "host": [ @@ -7260,10 +7457,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/timelines/{{timelineId}}", "host": [ @@ -7405,7 +7598,6 @@ }, { "name": "Milestone", - "description": null, "item": [ { "name": "Create milestone", @@ -7413,7 +7605,6 @@ { "listen": "test", "script": { - "id": "8fd1d5e9-8e6e-4cd7-9010-b855308be069", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -7501,10 +7692,6 @@ "type": "text" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/timelines/{{timelineId}}/milestones", "host": [ @@ -7533,10 +7720,6 @@ "value": "Bearer {{jwt-token-copilot-40051332}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/timelines/{{timelineId}}/milestones?sort=order desc", "host": [ @@ -7571,10 +7754,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/timelines/{{timelineId}}/milestones/{{milestoneId}}", "host": [ @@ -7596,7 +7775,6 @@ { "listen": "test", "script": { - "id": "8fd1d5e9-8e6e-4cd7-9010-b855308be069", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -7971,7 +8149,6 @@ }, { "name": "Milestone Template", - "description": null, "item": [ { "name": "Create milestone template", @@ -7979,7 +8156,6 @@ { "listen": "test", "script": { - "id": "3dbf8b29-2498-4b05-93de-14d809ccc285", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -8197,10 +8373,6 @@ "value": "Bearer {{jwt-token-copilot-40051332}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/timelines/metadata/milestoneTemplates", "host": [ @@ -8229,10 +8401,6 @@ "value": "Bearer {{jwt-token-copilot-40051332}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/timelines/metadata/milestoneTemplates?reference=productTemplate&referenceId={{productTemplateId}}", "host": [ @@ -8271,10 +8439,6 @@ "value": "Bearer {{jwt-token-copilot-40051332}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/timelines/metadata/milestoneTemplates?reference=productTemplate&referenceId={{productTemplateId}}&sort=order desc", "host": [ @@ -8317,10 +8481,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/timelines/metadata/milestoneTemplates/{{milestoneTemplateId}}", "host": [ @@ -8571,7 +8731,6 @@ }, { "name": "Metadata", - "description": null, "item": [ { "name": "Get all metadata", @@ -8594,10 +8753,6 @@ "type": "text" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata", "host": [ @@ -8626,10 +8781,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata?includeAllReferred=true", "host": [ @@ -8653,7 +8804,6 @@ }, { "name": "Form Version", - "description": null, "item": [ { "name": "List forms", @@ -8670,10 +8820,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/form/{{formKey}}/versions", "host": [ @@ -8705,10 +8851,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/form/{{formKey}}/versions/{{formVersion}}", "host": [ @@ -8741,10 +8883,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/form/{{formKey}}", "host": [ @@ -8766,7 +8904,6 @@ { "listen": "test", "script": { - "id": "94f6be66-34cc-40c8-80c2-b27dd93ed527", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -8908,7 +9045,6 @@ }, { "name": "Form Revision", - "description": null, "item": [ { "name": "List all revision for version", @@ -8925,10 +9061,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/form/{{formKey}}/versions/{{formVersion}}/revisions", "host": [ @@ -8962,10 +9094,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/form/{{formKey}}/versions/{{formVersion}}/revisions/{{formRevision}}", "host": [ @@ -8991,7 +9119,6 @@ { "listen": "test", "script": { - "id": "dbe5ec9f-022c-4ec5-b58c-d19c15430b61", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -9137,7 +9264,6 @@ }, { "name": "Price Config Version", - "description": null, "item": [ { "name": "List price configs", @@ -9154,10 +9280,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/priceConfig/dev/versions", "host": [ @@ -9189,10 +9311,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/priceConfig/{{priceKey}}/versions/{{priceVersion}}", "host": [ @@ -9225,10 +9343,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/priceConfig/{{priceKey}}", "host": [ @@ -9250,7 +9364,6 @@ { "listen": "test", "script": { - "id": "e440c87c-49ff-4443-b9bf-b44d4e9a480f", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -9393,7 +9506,6 @@ { "listen": "prerequest", "script": { - "id": "59182724-4332-4d76-90ea-f7520a7b1be9", "type": "text/javascript", "exec": [ "" @@ -9403,7 +9515,6 @@ { "listen": "test", "script": { - "id": "abc13dca-e8a4-4995-970f-00e5889a5f2d", "type": "text/javascript", "exec": [ "" @@ -9414,7 +9525,6 @@ }, { "name": "Price Config Revision", - "description": null, "item": [ { "name": "List all revision for version", @@ -9431,10 +9541,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/priceConfig/dev/versions/3/revisions", "host": [ @@ -9468,10 +9574,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/priceConfig/{{priceKey}}/versions/{{priceVersion}}/revisions/{{priceRevision}}", "host": [ @@ -9497,7 +9599,6 @@ { "listen": "test", "script": { - "id": "d53ed608-b21c-4d6f-bb68-c2beda1d631d", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -9643,7 +9744,6 @@ }, { "name": "Plan Config Version", - "description": null, "item": [ { "name": "List plan configs", @@ -9660,10 +9760,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/planConfig/dev/versions", "host": [ @@ -9695,10 +9791,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/planConfig/{{planKey}}/versions/{{planVersion}}", "host": [ @@ -9731,10 +9823,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/planConfig/{{planKey}}", "host": [ @@ -9756,7 +9844,6 @@ { "listen": "test", "script": { - "id": "97bc350a-0c4f-46a6-a315-a62b203b3ad2", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -9898,7 +9985,6 @@ }, { "name": "Plan Config Revision", - "description": null, "item": [ { "name": "List all revision for version", @@ -9915,10 +10001,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/planConfig/{{planKey}}/versions/{{planVersion}}/revisions", "host": [ @@ -9952,10 +10034,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/planConfig/{{planKey}}/versions/{{planVersion}}/revisions/{{planRevision}}", "host": [ @@ -9981,7 +10059,6 @@ { "listen": "test", "script": { - "id": "a5373f1f-4beb-46f9-8538-10c938c204ba", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -10127,11 +10204,9 @@ }, { "name": "Project Reports", - "description": null, "item": [ { "name": "summary", - "description": null, "item": [ { "name": "get report by admin", @@ -10144,10 +10219,6 @@ "type": "text" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/reports?reportName=summary", "host": [ @@ -10179,10 +10250,6 @@ "type": "text" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/reports?reportName=summary", "host": [ @@ -10214,10 +10281,6 @@ "value": "Bearer {{jwt-token-admin-40051333}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/123456/reports?reportName=summary", "host": [ @@ -10249,10 +10312,6 @@ "value": "Bearer {{jwt-token-admin-40051333}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/reports?reportName=summary123", "host": [ @@ -10273,12 +10332,10 @@ }, "response": [] } - ], - "_postman_isSubFolder": true + ] }, { "name": "projectBudget", - "description": null, "item": [ { "name": "get report by admin", @@ -10291,10 +10348,6 @@ "value": "Bearer {{jwt-token-admin-40051333}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/reports?reportName=projectBudget", "host": [ @@ -10315,14 +10368,12 @@ }, "response": [] } - ], - "_postman_isSubFolder": true + ] } ] }, { "name": "Project Setting", - "description": null, "item": [ { "name": "Create project setting - double", @@ -10330,7 +10381,6 @@ { "listen": "test", "script": { - "id": "7350de08-5111-44f8-8a4c-3d0c48bcd8d4", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -10377,7 +10427,6 @@ { "listen": "test", "script": { - "id": "bf3aa19f-517c-4103-9250-82d7847e7477", "exec": [ "" ], @@ -10421,7 +10470,6 @@ { "listen": "test", "script": { - "id": "7350de08-5111-44f8-8a4c-3d0c48bcd8d4", "exec": [ "" ], @@ -10465,7 +10513,6 @@ { "listen": "test", "script": { - "id": "7350de08-5111-44f8-8a4c-3d0c48bcd8d4", "exec": [ "" ], @@ -10541,7 +10588,6 @@ { "listen": "test", "script": { - "id": "7350de08-5111-44f8-8a4c-3d0c48bcd8d4", "exec": [ "" ], @@ -10713,7 +10759,6 @@ { "listen": "test", "script": { - "id": "7350de08-5111-44f8-8a4c-3d0c48bcd8d4", "exec": [ "" ], @@ -10765,10 +10810,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/settings", "host": [ @@ -10797,10 +10838,6 @@ "value": "Bearer {{jwt-token-copilot-40051332}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/settings", "host": [ @@ -10829,10 +10866,6 @@ "value": "Bearer {{jwt-token-manager-40051334}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/settings", "host": [ @@ -11048,4 +11081,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index d8c6a839..125c87be 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -620,6 +620,7 @@ paths: startDate asc in: query type: string + - $ref: '#/parameters/memberOnlyQueryParam' responses: '200': description: A list of project phases @@ -1710,6 +1711,122 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/ErrorModel' + '/projects/{projectId}/phases/{phaseId}/members': + parameters: + - $ref: '#/parameters/projectIdParam' + - $ref: '#/parameters/phaseIdParam' + get: + tags: + - phase members + description: >- + Retrieve phase members by id. All users who can see project members can access + this endpoint. + security: + - Bearer: [] + responses: + '200': + description: phase members + schema: + type: array + items: + $ref: '#/definitions/PhaseMember' + '400': + description: Bad request + schema: + $ref: '#/definitions/ErrorModel' + '401': + description: Unauthorized + schema: + $ref: '#/definitions/ErrorModel' + '403': + description: Forbidden + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: Not found + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Internal Server Error + schema: + $ref: '#/definitions/ErrorModel' + post: + tags: + - phase members + security: + - Bearer: [] + description: >- + Update phase members. Only admin roles can access this + endpoint. + parameters: + - in: body + name: body + required: true + schema: + $ref: '#/definitions/NewPhaseMember' + responses: + '200': + description: Successfully updated phase members. + schema: + type: array + items: + $ref: '#/definitions/PhaseMember' + '401': + description: Unauthorized + schema: + $ref: '#/definitions/ErrorModel' + '403': + description: Forbidden + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: Not found + schema: + $ref: '#/definitions/ErrorModel' + '400': + description: Bad request + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Internal Server Error + schema: + $ref: '#/definitions/ErrorModel' + '/projects/{projectId}/phases/{phaseId}/members/{userId}': + parameters: + - $ref: '#/parameters/projectIdParam' + - $ref: '#/parameters/phaseIdParam' + - $ref: '#/parameters/userIdParam' + delete: + tags: + - phase members + description: >- + Remove an existing phase member. Only admin roles can + access this endpoint. + security: + - Bearer: [] + responses: + '204': + description: Phase member successfully removed + '400': + description: Bad request + schema: + $ref: '#/definitions/ErrorModel' + '401': + description: Unauthorized + schema: + $ref: '#/definitions/ErrorModel' + '403': + description: Forbidden + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: Not found + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Internal Server Error + schema: + $ref: '#/definitions/ErrorModel' '/projects/{projectId}/upgrade': post: tags: @@ -4693,6 +4810,14 @@ parameters: type: integer format: int64 minimum: 1 + userIdParam: + name: userId + in: path + description: project phase user id param + required: true + type: integer + format: int64 + minimum: 1 workStreamIdParam: name: workStreamId in: path @@ -5789,6 +5914,28 @@ definitions: description: READ-ONLY. User that last updated this object readOnly: true - $ref: '#/definitions/ProductCategoryCreateRequest' + PhaseMember: + title: Phase member object + type: object + required: + - phaseId + - userId + properties: + phaseId: + type: integer + format: int64 + description: references to project phase id + userId: + type: integer + format: int64 + description: references to member's userId + NewPhaseMember: + title: New Phase members to Update + type: array + items: + type: integer + format: int64 + description: "The user id." ProjectTypeRequest: title: Project type request object type: object diff --git a/migrations/20210718_project_phase_member_table.sql b/migrations/20210718_project_phase_member_table.sql new file mode 100644 index 00000000..ac27ad5f --- /dev/null +++ b/migrations/20210718_project_phase_member_table.sql @@ -0,0 +1,13 @@ +CREATE TABLE "public"."project_phase_member" ( + "id" int8 NOT NULL DEFAULT nextval('project_phase_member_id_seq'::regclass), + "userId" int8 NOT NULL, + "deletedAt" timestamptz, + "createdAt" timestamptz, + "updatedAt" timestamptz, + "deletedBy" int4, + "createdBy" int4 NOT NULL, + "updatedBy" int4 NOT NULL, + "phaseId" int8, + CONSTRAINT "project_phase_member_phaseId_fkey" FOREIGN KEY ("phaseId") REFERENCES "public"."project_phases"("id") ON DELETE SET NULL ON UPDATE CASCADE, + PRIMARY KEY ("id") +); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2c8bc171..55a9bf14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -82,15 +82,20 @@ "join-component": "^1.1.0" } }, + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" + }, "@types/bluebird": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.0.tgz", "integrity": "sha1-JjNHCk6r6aR82aRf2yDtX5NAe8o=" }, "@types/body-parser": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz", + "integrity": "sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg==", "requires": { "@types/connect": "*", "@types/node": "*" @@ -103,20 +108,20 @@ "dev": true }, "@types/connect": { - "version": "3.4.33", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", - "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==", + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", "requires": { "@types/node": "*" } }, "@types/express": { - "version": "4.17.6", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.6.tgz", - "integrity": "sha512-n/mr9tZI83kd4azlPG5y997C/M4DNABK9yErhFM6hKdym4kkmd9j0vtsJyjFIwfRBxtrxZtAfGZCNRIBMFLK5w==", + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", "requires": { "@types/body-parser": "*", - "@types/express-serve-static-core": "*", + "@types/express-serve-static-core": "^4.17.18", "@types/qs": "*", "@types/serve-static": "*" } @@ -131,18 +136,19 @@ } }, "@types/express-serve-static-core": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.4.tgz", - "integrity": "sha512-dPs6CaRWxsfHbYDVU51VjEJaUJEcli4UI0fFMT4oWmgCvHj+j7oIxz5MLHVL0Rv++N004c21ylJNdWQvPkkb5w==", + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz", + "integrity": "sha512-3UJuW+Qxhzwjq3xhwXm2onQcFHn76frIYVbTu+kn24LFxI+dEhdfISDFovPB8VpEgW8oQCTpRuCe+0zJxB7NEA==", "requires": { "@types/node": "*", + "@types/qs": "*", "@types/range-parser": "*" } }, "@types/express-unless": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.1.tgz", - "integrity": "sha512-5fuvg7C69lemNgl0+v+CUxDYWVPSfXHhJPst4yTLcqi4zKJpORCxnDrnnilk3k0DTq/WrAUdvXFs01+vUqUZHw==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.2.tgz", + "integrity": "sha512-Q74UyYRX/zIgl1HSp9tUX2PlG8glkVm+59r7aK4KGKzC5jqKIOX6rrVLRQrzpZUQ84VukHtRoeAuon2nIssHPQ==", "requires": { "@types/express": "*" } @@ -158,9 +164,9 @@ "integrity": "sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==" }, "@types/mime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", - "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==" + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" }, "@types/node": { "version": "13.7.0", @@ -168,22 +174,22 @@ "integrity": "sha512-GnZbirvmqZUzMgkFn70c74OQpTTUcCzlhQliTzYjQMqg+hVKcDnxdL19Ne3UdYzdMA/+W3eb646FWn/ZaT1NfQ==" }, "@types/qs": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.1.tgz", - "integrity": "sha512-lhbQXx9HKZAPgBkISrBcmAcMpZsmpe/Cd/hY7LGZS5OfkySUBItnPZHgQPssWYUET8elF+yCFBbP1Q0RZPTdaw==" + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" }, "@types/range-parser": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", - "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" }, "@types/serve-static": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz", - "integrity": "sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g==", + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", "requires": { - "@types/express-serve-static-core": "*", - "@types/mime": "*" + "@types/mime": "^1", + "@types/node": "*" } }, "abbrev": { @@ -213,6 +219,29 @@ "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", "dev": true }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "agentkeepalive": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-3.5.2.tgz", @@ -225,6 +254,7 @@ "version": "6.11.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz", "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==", + "dev": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -479,40 +509,6 @@ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true }, - "auth0-js": { - "version": "9.13.2", - "resolved": "https://registry.npmjs.org/auth0-js/-/auth0-js-9.13.2.tgz", - "integrity": "sha512-gWlf+X3XhCT9JboYpGviflv0pHcaHFPGtkLXiebyJohHDKddiu2rZkezp9kZHEoXqxhtNqgWuuaXkcla5JtnXg==", - "requires": { - "base64-js": "^1.3.0", - "idtoken-verifier": "^2.0.2", - "js-cookie": "^2.2.0", - "qs": "^6.7.0", - "superagent": "^3.8.3", - "url-join": "^4.0.1", - "winchan": "^0.2.2" - }, - "dependencies": { - "crypto-js": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz", - "integrity": "sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==" - }, - "idtoken-verifier": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/idtoken-verifier/-/idtoken-verifier-2.0.2.tgz", - "integrity": "sha512-9UN83SKT9dtN3d7vNz3EMTqoaJi3D02Zg5XMqF6+bLrGL+Akbx4oj4SEWsgXtLF6cy46XrUcVzokFY+SWO+/MA==", - "requires": { - "base64-js": "^1.3.0", - "crypto-js": "^3.2.1", - "es6-promise": "^4.2.8", - "jsbn": "^1.1.0", - "unfetch": "^4.1.0", - "url-join": "^4.0.1" - } - } - } - }, "aws-sdk": { "version": "2.610.0", "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.610.0.tgz", @@ -535,9 +531,9 @@ "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", - "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" }, "axios": { "version": "0.19.2", @@ -2797,13 +2793,6 @@ "requires": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" - }, - "dependencies": { - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" - } } }, "ecdsa-sig-formatter": { @@ -2992,11 +2981,6 @@ "es6-symbol": "^3.1.1" } }, - "es6-promise": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" - }, "es6-symbol": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", @@ -4543,12 +4527,25 @@ "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" }, "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", "requires": { - "ajv": "^6.5.5", + "ajv": "^6.12.3", "har-schema": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + } } }, "has": { @@ -4700,6 +4697,31 @@ } } }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -4710,6 +4732,30 @@ "sshpk": "^1.7.0" } }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "requires": { + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -5473,11 +5519,6 @@ } } }, - "js-cookie": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", - "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" - }, "js-tokens": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", @@ -5503,9 +5544,9 @@ } }, "jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha1-sBMHyym2GKHtJux56RH4A8TaAEA=" + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, "jsesc": { "version": "1.3.0", @@ -5656,31 +5697,54 @@ } }, "jwks-rsa": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-1.7.0.tgz", - "integrity": "sha512-tq7DVJt9J6wTvl9+AQfwZIiPSuY2Vf0F+MovfRTFuBqLB1xgDVhegD33ChEAQ6yBv9zFvUIyj4aiwrSA5VehUw==", + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-1.12.3.tgz", + "integrity": "sha512-cFipFDeYYaO9FhhYJcZWX/IyZgc0+g316rcHnDpT2dNRNIE/lMOmWKKqp09TkJoYlNFzrEVODsR4GgXJMgWhnA==", "requires": { "@types/express-jwt": "0.0.42", + "axios": "^0.21.1", "debug": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", "jsonwebtoken": "^8.5.1", - "limiter": "^1.1.4", - "lru-memoizer": "^2.0.1", + "limiter": "^1.1.5", + "lru-memoizer": "^2.1.2", "ms": "^2.1.2", - "request": "^2.88.0" + "proxy-from-env": "^1.1.0" }, "dependencies": { + "axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } } }, + "follow-redirects": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz", + "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==" + }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" } } }, @@ -5717,31 +5781,6 @@ "integrity": "sha1-fQ0U7vPslwLG8wxg6oHxqNP5APs=", "dev": true }, - "le_node": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/le_node/-/le_node-1.8.0.tgz", - "integrity": "sha512-NXzjxBskZ4QawTNwlGdRG05jYU0LhV2nxxmP3x7sRMHyROV0jPdyyikO9at+uYrWX3VFt0Y/am11oKITedx0iw==", - "requires": { - "babel-runtime": "6.6.1", - "codependency": "0.1.4", - "json-stringify-safe": "5.0.1", - "lodash": "4.17.11", - "reconnect-core": "1.3.0", - "semver": "5.1.0" - }, - "dependencies": { - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" - }, - "semver": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.1.0.tgz", - "integrity": "sha1-hfLPhVBGXE3wAM99hvawVBBqueU=" - } - } - }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -5914,9 +5953,9 @@ } }, "lru-memoizer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.2.tgz", - "integrity": "sha512-N5L5xlnVcbIinNn/TJ17vHBZwBMt9t7aJDz2n97moWubjNl6VO9Ao2XuAGBBddkYdjrwR9HfzXbT6NfMZXAZ/A==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.4.tgz", + "integrity": "sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==", "requires": { "lodash.clonedeep": "^4.5.0", "lru-cache": "~4.0.0" @@ -7437,6 +7476,11 @@ "ipaddr.js": "1.9.0" } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -7468,6 +7512,26 @@ "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" }, + "r7insight_node": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/r7insight_node/-/r7insight_node-1.8.4.tgz", + "integrity": "sha512-6cQrzLkaOxdv/SRFXWRJjgFr8a3nXUOT/4IMFuBv+mWzBnu5DJl+HzONAsWYvclrlZnvfa54PaIPqPuPRSlbrQ==", + "requires": { + "babel-runtime": "6.6.1", + "codependency": "0.1.4", + "json-stringify-safe": "5.0.1", + "lodash": "4.17.15", + "reconnect-core": "1.3.0", + "semver": "5.1.0" + }, + "dependencies": { + "semver": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.1.0.tgz", + "integrity": "sha1-hfLPhVBGXE3wAM99hvawVBBqueU=" + } + } + }, "randomatic": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", @@ -8661,13 +8725,6 @@ "jsbn": "~0.1.0", "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" - }, - "dependencies": { - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" - } } }, "static-eval": { @@ -8905,17 +8962,16 @@ } }, "tc-core-library-js": { - "version": "github:appirio-tech/tc-core-library-js#f45352974dafe5a10c86fc50bdd59ef399b50c65", - "from": "github:appirio-tech/tc-core-library-js#v2.6.3", + "version": "github:appirio-tech/tc-core-library-js#c4ab01f468a98dc7e22f188a176794b5ea4f2f9d", + "from": "github:appirio-tech/tc-core-library-js#v2.6.6", "requires": { - "auth0-js": "^9.4.2", "axios": "^0.19.0", "bunyan": "^1.8.12", - "jsonwebtoken": "^8.3.0", - "jwks-rsa": "^1.3.0", - "le_node": "^1.3.1", - "lodash": "^4.17.10", + "jsonwebtoken": "^8.5.1", + "jwks-rsa": "^1.6.0", + "lodash": "^4.17.15", "millisecond": "^0.1.2", + "r7insight_node": "^1.8.4", "request": "^2.88.0" } }, @@ -9177,11 +9233,6 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" }, - "unfetch": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.1.0.tgz", - "integrity": "sha512-crP/n3eAPUJxZXM9T80/yv0YhkTEx2K1D3h7D1AJM6fzsWZrxdyRuLN0JH/dkZh1LNH8LxCnBzoPFCPbb2iGpg==" - }, "union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", @@ -9363,11 +9414,6 @@ "querystring": "0.2.0" } }, - "url-join": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", - "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" - }, "url-parse-lax": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", @@ -9536,11 +9582,6 @@ } } }, - "winchan": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/winchan/-/winchan-0.2.2.tgz", - "integrity": "sha512-pvN+IFAbRP74n/6mc6phNyCH8oVkzXsto4KCHPJ2AScniAnA1AmeLI03I2BzjePpaClGSI4GUMowzsD3qz5PRQ==" - }, "wkx": { "version": "0.4.8", "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.4.8.tgz", diff --git a/src/events/projects/index.js b/src/events/projects/index.js index b3401098..ea9a36e1 100644 --- a/src/events/projects/index.js +++ b/src/events/projects/index.js @@ -56,7 +56,7 @@ async function projectUpdatedKafkaHandler(app, topic, payload) { const doc = await eClient.get({ index: ES_PROJECT_INDEX, type: ES_PROJECT_TYPE, id: previousValue.id }); // console.log(doc._source, 'Received project from ES');// eslint-disable-line no-underscore-dangle const merged = _.merge(doc._source, project.get({ plain: true })); // eslint-disable-line no-underscore-dangle - console.log(merged, 'Merged project'); + app.logger.debug(merged, 'Merged project'); // update the merged document await eClient.update({ index: ES_PROJECT_INDEX, @@ -66,7 +66,7 @@ async function projectUpdatedKafkaHandler(app, topic, payload) { doc: merged, }, }); - console.log(`Succesfully updated project document in ES (projectId: ${previousValue.id})`); + app.logger.debug(`Succesfully updated project document in ES (projectId: ${previousValue.id})`); } catch (error) { throw Error(`failed to updated project document in elasitcsearch index (projectId: ${previousValue.id})` + `. Details: '${error}'.`); diff --git a/src/models/projectPhase.js b/src/models/projectPhase.js index 5c260e1d..7c22c7fe 100644 --- a/src/models/projectPhase.js +++ b/src/models/projectPhase.js @@ -42,6 +42,7 @@ module.exports = function defineProjectPhase(sequelize, DataTypes) { ProjectPhase.associate = (models) => { ProjectPhase.hasMany(models.PhaseProduct, { as: 'products', foreignKey: 'phaseId' }); + ProjectPhase.hasMany(models.ProjectPhaseMember, { as: 'members', foreignKey: 'phaseId' }); ProjectPhase.belongsToMany(models.WorkStream, { through: models.PhaseWorkStream, foreignKey: 'phaseId' }); }; diff --git a/src/models/projectPhaseMember.js b/src/models/projectPhaseMember.js new file mode 100644 index 00000000..91dbd22f --- /dev/null +++ b/src/models/projectPhaseMember.js @@ -0,0 +1,62 @@ +module.exports = function defineProjectPhaseMember(sequelize, DataTypes) { + const ProjectPhaseMember = sequelize.define('ProjectPhaseMember', { + id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, + userId: { type: DataTypes.BIGINT, allowNull: false }, + + deletedAt: { type: DataTypes.DATE, allowNull: true }, + createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + deletedBy: { type: DataTypes.INTEGER, allowNull: true }, + createdBy: { type: DataTypes.INTEGER, allowNull: false }, + updatedBy: { type: DataTypes.INTEGER, allowNull: false }, + }, + { + tableName: 'project_phase_member', + paranoid: true, + timestamps: true, + updatedAt: 'updatedAt', + createdAt: 'createdAt', + deletedAt: 'deletedAt', + defaultScope: { + attributes: { + exclude: ['deletedAt', 'deletedBy'], + }, + }, + hooks: { + afterCreate: (projectPhaseMember) => { + // eslint-disable-next-line no-param-reassign + delete projectPhaseMember.dataValues.deletedAt; + // eslint-disable-next-line no-param-reassign + delete projectPhaseMember.dataValues.deletedBy; + }, + }, + indexes: [ + { + unique: true, + fields: ['phaseId', 'userId'], + where: { + deletedAt: null, + }, + }, + ], + }); + + ProjectPhaseMember.getPhaseMembers = (phaseId, raw = true) => ProjectPhaseMember.findAll({ + where: { + phaseId, + }, + raw, + }); + + ProjectPhaseMember.getMemberPhases = (userId, raw = true) => ProjectPhaseMember.findAll({ + where: { + userId, + }, + raw, + }); + + ProjectPhaseMember.associate = (models) => { + ProjectPhaseMember.belongsTo(models.ProjectPhase, { foreignKey: 'phaseId' }); + }; + return ProjectPhaseMember; +}; diff --git a/src/permissions/index.js b/src/permissions/index.js index 9e4bc1e4..0abd4c7b 100644 --- a/src/permissions/index.js +++ b/src/permissions/index.js @@ -98,6 +98,10 @@ module.exports = () => { Authorizer.setPolicy('project.updatePhaseProduct', copilotAndAbove); Authorizer.setPolicy('project.deletePhaseProduct', copilotAndAbove); + Authorizer.setPolicy('phaseMember.update', projectAdmin); + Authorizer.setPolicy('phaseMember.delete', projectAdmin); + Authorizer.setPolicy('phaseMember.view', generalPermission(PERMISSION.READ_PROJECT_MEMBER)); + Authorizer.setPolicy('milestoneTemplate.clone', projectAdmin); Authorizer.setPolicy('milestoneTemplate.create', projectAdmin); Authorizer.setPolicy('milestoneTemplate.edit', projectAdmin); diff --git a/src/routes/index.js b/src/routes/index.js index 6b725728..a20e1a04 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -183,6 +183,13 @@ router.route('/v5/projects/:projectId(\\d+)/phases/:phaseId(\\d+)/products/:prod .patch(require('./phaseProducts/update')) .delete(require('./phaseProducts/delete')); +router.route('/v5/projects/:projectId(\\d+)/phases/:phaseId(\\d+)/members') + .get(require('./phaseMembers/list')) + .post(require('./phaseMembers/update')); + +router.route('/v5/projects/:projectId(\\d+)/phases/:phaseId(\\d+)/members/:userId(\\d+)') + .delete(require('./phaseMembers/delete')); + router.route('/v5/projects/metadata/productCategories') .post(require('./productCategories/create')); diff --git a/src/routes/phaseMembers/delete.js b/src/routes/phaseMembers/delete.js new file mode 100644 index 00000000..032cf6d7 --- /dev/null +++ b/src/routes/phaseMembers/delete.js @@ -0,0 +1,80 @@ +import _ from 'lodash'; +import Joi from 'joi'; +import validate from 'express-validation'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import models from '../../models'; +import util from '../../util'; +import { EVENT, RESOURCES, ROUTES } from '../../constants'; + +/** + * API to update a project phase members. + */ +const permissions = tcMiddleware.permissions; + +const deletePhaseMemberValidations = { + params: { + projectId: Joi.number().integer().positive().required(), + phaseId: Joi.number().integer().positive().required(), + userId: Joi.number().integer().positive().required(), + }, +}; + +module.exports = [ + // handles request validations + validate(deletePhaseMemberValidations), + permissions('phaseMember.delete'), + async (req, res, next) => { + const projectId = _.parseInt(req.params.projectId); + const phaseId = _.parseInt(req.params.phaseId); + const userId = _.parseInt(req.params.userId); + let transaction; + try { + // check if project and phase exist + const phase = await models.ProjectPhase.findOne({ + where: { + id: phaseId, + projectId, + deletedAt: { $eq: null }, + }, + raw: true, + }); + if (!phase) { + const err = new Error('No active project phase found for project id ' + + `${projectId} and phase id ${phaseId}`); + err.status = 404; + throw (err); + } + // get current phase members. + const phaseMembers = await models.ProjectPhaseMember.getPhaseMembers(phaseId); + // find out which is to be deleted + const memberToDelete = _.find(phaseMembers, ['userId', userId]); + if (memberToDelete) { + transaction = await models.sequelize.transaction(); + const phaseMember = await models.ProjectPhaseMember.findOne({ + where: { + phaseId, + userId, + deletedAt: { $eq: null }, + }, + }); + await phaseMember.update({ deletedBy: req.authUser.userId }, { transaction }); + await phaseMember.destroy({ transaction }); + const updatedPhase = _.cloneDeep(phase); + util.sendResourceToKafkaBus( + req, + EVENT.ROUTING_KEY.PROJECT_PHASE_UPDATED, + RESOURCES.PHASE, + _.assign(updatedPhase, { members: _.filter(phaseMembers, member => member.userId !== userId) }), + _.assign(phase, { members: phaseMembers }), + ROUTES.PHASES.UPDATE); + await transaction.commit(); + } + res.status(204).end(); + } catch (err) { + if (transaction) { + await transaction.rollback(); + } + next(err); + } + }, +]; diff --git a/src/routes/phaseMembers/delete.spec.js b/src/routes/phaseMembers/delete.spec.js new file mode 100644 index 00000000..9d48b0ed --- /dev/null +++ b/src/routes/phaseMembers/delete.spec.js @@ -0,0 +1,158 @@ +/** + * Tests for delete.js + */ +import _ from 'lodash'; +import config from 'config'; +import request from 'supertest'; +import util from '../../util'; +import models from '../../models'; +import server from '../../app'; +import testUtil from '../../tests/util'; + +const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName'); +const ES_PROJECT_TYPE = config.get('elasticsearchConfig.docType'); +const eClient = util.getElasticSearchClient(); + +describe('Delete phase member', () => { + let id; + let project; + let phaseId; + const copilotUser = { + handle: testUtil.getDecodedToken(testUtil.jwts.copilot).handle, + userId: testUtil.getDecodedToken(testUtil.jwts.copilot).userId, + firstName: 'fname', + lastName: 'lName', + email: 'some@abc.com', + }; + const memberUser = { + handle: testUtil.getDecodedToken(testUtil.jwts.member).handle, + userId: testUtil.getDecodedToken(testUtil.jwts.member).userId, + firstName: 'fname', + lastName: 'lName', + email: 'some@abc.com', + }; + before(function beforeHook(done) { + this.timeout(20000); + // mocks + testUtil.clearDb() + .then(() => testUtil.clearES()) + .then(() => { + models.Project.create({ + type: 'generic', + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', + }).then((p) => { + id = p.id; + project = p.toJSON(); + // create members + models.ProjectMember.bulkCreate([{ + id: 1, + userId: copilotUser.userId, + projectId: id, + role: 'copilot', + isPrimary: false, + createdBy: 1, + updatedBy: 1, + }, { + id: 2, + userId: memberUser.userId, + projectId: id, + role: 'customer', + isPrimary: true, + createdBy: 1, + updatedBy: 1, + }]).then(() => { + models.ProjectPhase.create({ + name: 'test project phase', + projectId: id, + 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', + }, + createdBy: 1, + updatedBy: 1, + }).then((ph) => { + const phase = ph.toJSON(); + phaseId = phase.id; + models.ProjectPhaseMember.create({ + phaseId, + userId: copilotUser.userId, + }).then((phaseMember) => { + _.assign(phase, { members: [phaseMember.toJSON()] }); + // Index to ES + // Overwrite lastActivityAt as otherwise ES fill not be able to parse it + project.lastActivityAt = 1; + project.phases = [phase]; + return eClient.index({ + index: ES_PROJECT_INDEX, + type: ES_PROJECT_TYPE, + id, + body: project, + }).then(() => { + done(); + }); + }); + }); + }); + }); + }); + }); + + after((done) => { + testUtil.clearDb(done); + }); + describe('DELETE /projects/{projectId}/phases/{phaseId}/members/{memberId}', () => { + it('should return 403 for anonymous user', (done) => { + request(server) + .delete(`/v5/projects/${id}/phases/${phaseId}/members/${copilotUser.userId}`) + .expect(403, done); + }); + + it('should return 403 for regular user', (done) => { + request(server) + .delete(`/v5/projects/${id}/phases/${phaseId}/members/${copilotUser.userId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(403, done); + }); + + it('should return 200 for connect admin', (done) => { + request(server) + .delete(`/v5/projects/${id}/phases/${phaseId}/members/${copilotUser.userId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(204, done); + }); + + it('should return 204 for project admin', (done) => { + request(server) + .delete(`/v5/projects/${id}/phases/${phaseId}/members/${copilotUser.userId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(204, done); + }); + + it('should return 403 for copilot', (done) => { + request(server) + .delete(`/v5/projects/${id}/phases/${phaseId}/members/${copilotUser.userId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(403, done); + }); + }); +}); diff --git a/src/routes/phaseMembers/list.js b/src/routes/phaseMembers/list.js new file mode 100644 index 00000000..92e99a0a --- /dev/null +++ b/src/routes/phaseMembers/list.js @@ -0,0 +1,66 @@ +import _ from 'lodash'; +import config from 'config'; +import Joi from 'joi'; +import validate from 'express-validation'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import models from '../../models'; +import util from '../../util'; + +const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName'); +const ES_PROJECT_TYPE = config.get('elasticsearchConfig.docType'); + +/** + * API to list a project phase members. + */ +const permissions = tcMiddleware.permissions; + +const listPhaseMemberValidations = { + params: { + projectId: Joi.number().integer().positive().required(), + phaseId: Joi.number().integer().positive().required(), + }, +}; + +module.exports = [ + // handles request validations + validate(listPhaseMemberValidations), + permissions('phaseMember.view'), + async (req, res, next) => { + const projectId = _.parseInt(req.params.projectId); + const phaseId = _.parseInt(req.params.phaseId); + try { + const esClient = util.getElasticSearchClient(); + const project = await esClient.get({ index: ES_PROJECT_INDEX, type: ES_PROJECT_TYPE, id: projectId }); + // eslint-disable-next-line no-underscore-dangle + const phases = _.isArray(project._source.phases) ? project._source.phases : []; // eslint-disable-line no-underscore-dangle + const phase = _.find(phases, ['id', phaseId]); + const phaseMembers = phase.members || []; + res.json(phaseMembers); + return; + } catch (err) { + req.log.debug('No active project phase found in ES for project id ' + + `${projectId} and phase id ${phaseId}`); + } + try { + req.log.debug('Fall back to DB'); + const phase = await models.ProjectPhase.findOne({ + where: { + id: phaseId, + projectId, + deletedAt: { $eq: null }, + }, + raw: true, + }); + if (!phase) { + const err = new Error('No active project phase found for project id ' + + `${projectId} and phase id ${phaseId}`); + err.status = 404; + throw (err); + } + const phaseMembers = await models.ProjectPhaseMember.getPhaseMembers(phaseId); + res.json(phaseMembers); + } catch (err) { + next(err); + } + }, +]; diff --git a/src/routes/phaseMembers/list.spec.js b/src/routes/phaseMembers/list.spec.js new file mode 100644 index 00000000..e9b0a8c0 --- /dev/null +++ b/src/routes/phaseMembers/list.spec.js @@ -0,0 +1,160 @@ +/** + * Tests for list.js + */ +import _ from 'lodash'; +import config from 'config'; +import request from 'supertest'; +import chai from 'chai'; +import util from '../../util'; +import models from '../../models'; +import server from '../../app'; +import testUtil from '../../tests/util'; + +const should = chai.should(); + +const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName'); +const ES_PROJECT_TYPE = config.get('elasticsearchConfig.docType'); +const eClient = util.getElasticSearchClient(); + +describe('List phase members', () => { + let id; + let project; + let phaseId; + let memberId; + const copilotUser = { + handle: testUtil.getDecodedToken(testUtil.jwts.copilot).handle, + userId: testUtil.getDecodedToken(testUtil.jwts.copilot).userId, + firstName: 'fname', + lastName: 'lName', + email: 'some@abc.com', + }; + before(function beforeHook(done) { + this.timeout(20000); + // mocks + testUtil.clearDb() + .then(() => testUtil.clearES()) + .then(() => { + models.Project.create({ + type: 'generic', + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', + }).then((p) => { + id = p.id; + project = p.toJSON(); + // create members + models.ProjectMember.create({ + id: 1, + userId: copilotUser.userId, + projectId: id, + role: 'copilot', + isPrimary: false, + createdBy: 1, + updatedBy: 1, + }).then((member) => { + memberId = member.id; + models.ProjectPhase.create({ + name: 'test project phase', + projectId: id, + 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', + }, + createdBy: 1, + updatedBy: 1, + }).then((ph) => { + const phase = ph.toJSON(); + phaseId = phase.id; + models.ProjectPhaseMember.create({ + phaseId, + memberId, + userId: copilotUser.userId, + }).then((phaseMember) => { + _.assign(phase, { members: [phaseMember.toJSON()] }); + // Index to ES + // Overwrite lastActivityAt as otherwise ES fill not be able to parse it + project.lastActivityAt = 1; + project.phases = [phase]; + return eClient.index({ + index: ES_PROJECT_INDEX, + type: ES_PROJECT_TYPE, + id, + body: project, + }).then(() => { + done(); + }); + }); + }); + }); + }); + }); + }); + + after((done) => { + testUtil.clearDb(done); + }); + describe('GET /projects/{projectId}/phases/{phaseId}/members', () => { + it('should return 403 for anonymous user', (done) => { + request(server) + .get(`/v5/projects/${id}/phases/${phaseId}/members`) + .expect(403, done); + }); + + it('should return 403 for non project member user', (done) => { + request(server) + .get(`/v5/projects/${id}/phases/${phaseId}/members`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member2}`, + }) + .expect(403, done); + }); + + it('should return 200 for connect admin', (done) => { + request(server) + .get(`/v5/projects/${id}/phases/${phaseId}/members`) + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for project admin', (done) => { + request(server) + .get(`/v5/projects/${id}/phases/${phaseId}/members`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for copilot', (done) => { + request(server) + .get(`/v5/projects/${id}/phases/${phaseId}/members`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(200) + .end((err, res) => { + const resJson = res.body; + should.exist(resJson); + resJson.should.have.length(1); + resJson[0].userId.should.be.eql(copilotUser.userId); + resJson[0].memberId.should.be.eql(1); + resJson[0].phaseId.should.be.eql(phaseId); + done(); + }); + }); + }); +}); diff --git a/src/routes/phaseMembers/update.js b/src/routes/phaseMembers/update.js new file mode 100644 index 00000000..b05aef57 --- /dev/null +++ b/src/routes/phaseMembers/update.js @@ -0,0 +1,91 @@ +import _ from 'lodash'; +import Joi from 'joi'; +import validate from 'express-validation'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import models from '../../models'; +import util from '../../util'; +import { EVENT, RESOURCES, ROUTES } from '../../constants'; + +/** + * API to update a project phase members. + */ +const permissions = tcMiddleware.permissions; + +const updatePhaseMemberValidations = { + body: Joi.object().keys({ + userIds: Joi.array().items(Joi.number().integer()).required(), + }), + params: { + projectId: Joi.number().integer().positive().required(), + phaseId: Joi.number().integer().positive().required(), + }, +}; + +module.exports = [ + // handles request validations + validate(updatePhaseMemberValidations), + permissions('phaseMember.update'), + async (req, res, next) => { + const projectId = _.parseInt(req.params.projectId); + const phaseId = _.parseInt(req.params.phaseId); + const createdBy = _.parseInt(req.authUser.userId); + const updatedBy = _.parseInt(req.authUser.userId); + const newPhaseMembers = req.body.userIds; + const transaction = await models.sequelize.transaction(); + try { + // chekc if project and phase exist + const phase = await models.ProjectPhase.findOne({ + where: { + id: phaseId, + projectId, + deletedAt: { $eq: null }, + }, + raw: true, + }); + if (!phase) { + const err = new Error('No active project phase found for project id ' + + `${projectId} and phase id ${phaseId}`); + err.status = 404; + throw (err); + } + const projectMembers = _.map(await models.ProjectMember.getActiveProjectMembers(projectId), 'userId'); + const notProjectMembers = _.difference(newPhaseMembers, projectMembers); + if (notProjectMembers.length > 0) { + const err = new Error(`Members with id: ${notProjectMembers} are not members of project ${projectId}`); + err.status = 404; + throw (err); + } + const phaseMembers = await models.ProjectPhaseMember.getPhaseMembers(phaseId); + const existentPhaseMembers = _.map(phaseMembers, 'userId'); + let updatedPhaseMembers = _.cloneDeep(phaseMembers); + const updatedPhase = _.cloneDeep(phase); + const membersToAdd = _.difference(newPhaseMembers, existentPhaseMembers); + const membersToRemove = _.differenceBy(existentPhaseMembers, newPhaseMembers); + if (membersToRemove.length > 0) { + await models.ProjectPhaseMember.destroy({ where: { phaseId, userId: membersToRemove }, transaction }); + updatedPhaseMembers = _.filter(updatedPhaseMembers, row => !_.includes(membersToRemove, row.userId)); + } + if (membersToAdd.length > 0) { + const createData = _.map(membersToAdd, userId => ({ phaseId, userId, createdBy, updatedBy })); + const result = await models.ProjectPhaseMember.bulkCreate(createData, { transaction }); + updatedPhaseMembers.push(..._.map(result, item => item.toJSON())); + } + req.log.debug('updated phase members', JSON.stringify(newPhaseMembers, null, 2)); + // emit event + if (membersToRemove.length > 0 || membersToAdd.length > 0) { + util.sendResourceToKafkaBus( + req, + EVENT.ROUTING_KEY.PROJECT_PHASE_UPDATED, + RESOURCES.PHASE, + _.assign(updatedPhase, { members: updatedPhaseMembers }), + _.assign(phase, { members: phaseMembers }), + ROUTES.PHASES.UPDATE); + } + await transaction.commit(); + res.json(updatedPhaseMembers); + } catch (err) { + await transaction.rollback(); + next(err); + } + }, +]; diff --git a/src/routes/phaseMembers/update.spec.js b/src/routes/phaseMembers/update.spec.js new file mode 100644 index 00000000..88bb462c --- /dev/null +++ b/src/routes/phaseMembers/update.spec.js @@ -0,0 +1,178 @@ +/** + * Tests for update.js + */ +import _ from 'lodash'; +import config from 'config'; +import request from 'supertest'; +import chai from 'chai'; +import util from '../../util'; +import models from '../../models'; +import server from '../../app'; +import testUtil from '../../tests/util'; + +const should = chai.should(); + +const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName'); +const ES_PROJECT_TYPE = config.get('elasticsearchConfig.docType'); +const eClient = util.getElasticSearchClient(); + +describe('Update phase members', () => { + let id; + let project; + let phaseId; + const copilotUser = { + handle: testUtil.getDecodedToken(testUtil.jwts.copilot).handle, + userId: testUtil.getDecodedToken(testUtil.jwts.copilot).userId, + firstName: 'fname', + lastName: 'lName', + email: 'some@abc.com', + }; + const memberUser = { + handle: testUtil.getDecodedToken(testUtil.jwts.member).handle, + userId: testUtil.getDecodedToken(testUtil.jwts.member).userId, + firstName: 'fname', + lastName: 'lName', + email: 'some@abc.com', + }; + before(function beforeHook(done) { + this.timeout(20000); + // mocks + testUtil.clearDb() + .then(() => testUtil.clearES()) + .then(() => { + models.Project.create({ + type: 'generic', + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', + }).then((p) => { + id = p.id; + project = p.toJSON(); + // create members + models.ProjectMember.bulkCreate([{ + id: 1, + userId: copilotUser.userId, + projectId: id, + role: 'copilot', + isPrimary: false, + createdBy: 1, + updatedBy: 1, + }, { + id: 2, + userId: memberUser.userId, + projectId: id, + role: 'customer', + isPrimary: true, + createdBy: 1, + updatedBy: 1, + }]).then(() => { + models.ProjectPhase.create({ + name: 'test project phase', + projectId: id, + 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', + }, + createdBy: 1, + updatedBy: 1, + }).then((ph) => { + const phase = ph.toJSON(); + phaseId = phase.id; + models.ProjectPhaseMember.create({ + phaseId, + userId: copilotUser.userId, + }).then((phaseMember) => { + _.assign(phase, { members: [phaseMember.toJSON()] }); + // Index to ES + // Overwrite lastActivityAt as otherwise ES fill not be able to parse it + project.lastActivityAt = 1; + project.phases = [phase]; + return eClient.index({ + index: ES_PROJECT_INDEX, + type: ES_PROJECT_TYPE, + id, + body: project, + }).then(() => { + done(); + }); + }); + }); + }); + }); + }); + }); + + after((done) => { + testUtil.clearDb(done); + }); + describe('POST /projects/{projectId}/phases/{phaseId}/members', () => { + it('should return 403 for anonymous user', (done) => { + request(server) + .post(`/v5/projects/${id}/phases/${phaseId}/members`) + .send({ memberIds: [copilotUser.userId, memberUser.userId] }) + .expect(403, done); + }); + + it('should return 403 for regular user', (done) => { + request(server) + .post(`/v5/projects/${id}/phases/${phaseId}/members`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send({ memberIds: [copilotUser.userId, memberUser.userId] }) + .expect(403, done); + }); + + it('should return 200 for connect admin', (done) => { + request(server) + .post(`/v5/projects/${id}/phases/${phaseId}/members`) + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .send({ memberIds: [copilotUser.userId, memberUser.userId] }) + .expect(200) + .end((err, res) => { + const resJson = res.body; + should.exist(resJson); + resJson.should.have.length(2); + done(); + }); + }); + + it('should return 200 for project admin', (done) => { + request(server) + .post(`/v5/projects/${id}/phases/${phaseId}/members`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send({ memberIds: [] }) + .expect(200) + .end((err, res) => { + const resJson = res.body; + should.exist(resJson); + resJson.should.have.length(0); + done(); + }); + }); + + it('should return 403 for copilot', (done) => { + request(server) + .post(`/v5/projects/${id}/phases/${phaseId}/members`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ memberIds: [copilotUser.userId, memberUser.userId] }) + .expect(403, done); + }); + }); +}); diff --git a/src/routes/phases/create.spec.js b/src/routes/phases/create.spec.js index 983ced88..68200ff1 100644 --- a/src/routes/phases/create.spec.js +++ b/src/routes/phases/create.spec.js @@ -293,7 +293,6 @@ describe('Project Phases', () => { done(err); } else { const resJson = res.body; - console.log(resJson); validatePhase(resJson, body); resJson.products.should.have.length(1); diff --git a/src/routes/phases/get.js b/src/routes/phases/get.js index 12c1bd3e..64c34983 100644 --- a/src/routes/phases/get.js +++ b/src/routes/phases/get.js @@ -5,7 +5,15 @@ import util from '../../util'; import models from '../../models'; const permissions = tcMiddleware.permissions; - +const populateMemberDetails = async (phase, logger, id) => { + if (phase.members && phase.members.length > 0) { + const details = await util.getMemberDetailsByUserIds(_.map(phase.members, 'userId'), logger, id); + _.forEach(phase.members, (member) => { + _.assign(member, _.find(details, detail => detail.userId === member.userId)); + }); + } + return phase; +}; module.exports = [ permissions('project.view'), (req, res, next) => { @@ -39,7 +47,10 @@ module.exports = [ return models.ProjectPhase .findOne({ where: { id: phaseId, projectId }, - raw: true, + include: [{ + model: models.ProjectPhaseMember, + as: 'members', + }], }) .then((phase) => { if (!phase) { @@ -49,12 +60,15 @@ module.exports = [ err.status = 404; throw err; } - res.json(phase); + return populateMemberDetails(phase.toJSON(), req.log, req.id) + .then(result => res.json(result)); }) .catch(err => next(err)); } req.log.debug('phase found in ES'); - return res.json(data[0].inner_hits.phases.hits.hits[0]._source); // eslint-disable-line no-underscore-dangle + // eslint-disable-next-line no-underscore-dangle + return populateMemberDetails(data[0].inner_hits.phases.hits.hits[0]._source, req.log, req.id) + .then(phase => res.json(phase)); }) .catch(next); }, diff --git a/src/routes/phases/list.js b/src/routes/phases/list.js index cd6aaabf..c7c80d75 100644 --- a/src/routes/phases/list.js +++ b/src/routes/phases/list.js @@ -4,6 +4,7 @@ import config from 'config'; import { middleware as tcMiddleware } from 'tc-core-library-js'; import util from '../../util'; import models from '../../models'; +import { ADMIN_ROLES } from '../../constants'; const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName'); const ES_PROJECT_TYPE = config.get('elasticsearchConfig.docType'); @@ -14,6 +15,18 @@ const PHASE_ATTRIBUTES = _.keys(models.ProjectPhase.rawAttributes); const permissions = tcMiddleware.permissions; +const populateMemberDetails = async (phases, logger, id) => { + const userIds = _.reduce(phases, (acc, phase) => _.concat(acc, _.map(phase.members, 'userId')), []); + if (userIds.length > 0) { + const details = await util.getMemberDetailsByUserIds(userIds, logger, id); + _.forEach(phases, (phase) => { + _.forEach(phase.members, (member) => { + _.assign(member, _.find(details, detail => detail.userId === member.userId)); + }); + }); + } + return phases; +}; module.exports = [ permissions('project.view'), (req, res, next) => { @@ -22,6 +35,8 @@ module.exports = [ // Parse the fields string to determine what fields are to be returned let fields = req.query.fields ? decodeURIComponent(req.query.fields).split(',') : PHASE_ATTRIBUTES; let sort = req.query.sort ? decodeURIComponent(req.query.sort) : 'startDate'; + const memberOnly = req.query.memberOnly ? req.query.memberOnly.toLowerCase() === 'true' : false; + const isAdmin = util.hasRoles(req, ADMIN_ROLES); if (sort && sort.indexOf(' ') === -1) { sort += ' asc'; } @@ -43,32 +58,45 @@ module.exports = [ // Get the phases let phases = _.isArray(doc._source.phases) ? doc._source.phases : []; // eslint-disable-line no-underscore-dangle + if (memberOnly && !isAdmin) { + phases = _.filter(phases, phase => _.includes(_.map(_.get(phase, 'members'), 'userId')), req.authUser.userId); + } // Sort phases = _.orderBy(phases, [sortColumnAndOrder[0]], [sortColumnAndOrder[1]]); - fields = _.intersection(fields, [...PHASE_ATTRIBUTES, 'products']); + fields = _.intersection(fields, [...PHASE_ATTRIBUTES, 'products', 'members']); if (_.indexOf(fields, 'id') < 0) { fields.push('id'); } phases = _.map(phases, phase => _.pick(phase, fields)); - - res.json(phases); + return populateMemberDetails(phases, req.log, req.id) + .then(result => res.json(result)); }) .catch((err) => { if (err.status === 404) { req.log.debug('No phases found in ES'); + const include = { + model: models.ProjectPhase, + as: 'phases', + order: [['startDate', 'asc']], + include: [], + }; + if (_.indexOf(fields, 'products') >= 0) { + include.include.push({ + model: models.PhaseProduct, + as: 'products', + }); + } + if (_.indexOf(fields, 'members') >= 0) { + include.include.push({ + model: models.ProjectPhaseMember, + as: 'members', + }); + } // Load the phases return models.Project.findByPk(projectId, { - include: [{ - model: models.ProjectPhase, - as: 'phases', - order: [['startDate', 'asc']], - include: [{ - model: models.PhaseProduct, - as: 'products', - }], - }], + include: [include], }) .then((project) => { if (!project) { @@ -79,17 +107,22 @@ module.exports = [ // Get the phases let phases = _.isArray(project.phases) ? project.phases : []; - + phases = _.map(phases, phase => phase.toJSON()); + if (memberOnly && !isAdmin) { + phases = _.filter(phases, phase => + _.includes(_.map(_.get(phase, 'members'), 'userId')), req.authUser.userId); + } // Sort phases = _.orderBy(phases, [sortColumnAndOrder[0]], [sortColumnAndOrder[1]]); - - fields = _.intersection(fields, [...PHASE_ATTRIBUTES, 'products']); + _.remove(PHASE_ATTRIBUTES, attribute => _.includes(['deletedAt', 'deletedBy'], attribute)); + fields = _.intersection(fields, [...PHASE_ATTRIBUTES, 'products', 'members']); if (_.indexOf(fields, 'id') < 0) { fields.push('id'); } - + phases = _.map(phases, phase => _.pick(phase, fields)); // Write to response - return res.json(_.map(phases, p => _.omit(p.toJSON(), ['deletedAt', 'deletedBy']))); + return populateMemberDetails(phases, req.log, req.id) + .then(result => res.json(result)); }); } return next(err); diff --git a/src/routes/phases/list.spec.js b/src/routes/phases/list.spec.js index 6058643b..80ad7637 100644 --- a/src/routes/phases/list.spec.js +++ b/src/routes/phases/list.spec.js @@ -46,7 +46,7 @@ describe('Project Phases', () => { email: 'some@abc.com', }; before(function beforeHook(done) { - this.timeout(10000); + this.timeout(20000); // mocks testUtil.clearDb() .then(() => testUtil.clearES()) @@ -85,18 +85,26 @@ describe('Project Phases', () => { }]).then(() => { _.assign(body, { projectId }); return models.ProjectPhase.create(body); - }).then((phase) => { - // Index to ES - // Overwrite lastActivityAt as otherwise ES fill not be able to parse it - project.lastActivityAt = 1; - project.phases = [phase]; - return eClient.index({ - index: ES_PROJECT_INDEX, - type: ES_PROJECT_TYPE, - id: projectId, - body: project, - }).then(() => { - done(); + }).then((ph) => { + const phase = ph.toJSON(); + models.ProjectPhaseMember.create({ + phaseId: phase.id, + memberId: 1, + userId: copilotUser.userId, + }).then((phaseMember) => { + _.assign(phase, { members: [phaseMember.toJSON()] }); + // Index to ES + // Overwrite lastActivityAt as otherwise ES fill not be able to parse it + project.lastActivityAt = 1; + project.phases = [phase]; + return eClient.index({ + index: ES_PROJECT_INDEX, + type: ES_PROJECT_TYPE, + id: projectId, + body: project, + }).then(() => { + done(); + }); }); }); }); @@ -171,5 +179,47 @@ describe('Project Phases', () => { } }); }); + + it('should return 1 phase when user have project permission (copilot) with memberOnly', (done) => { + request(server) + .get(`/v5/projects/${projectId}/phases/`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .query({ memberOnly: true }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body; + should.exist(resJson); + resJson.should.have.lengthOf(1); + done(); + } + }); + }); + + it('should return 0 phase when user have project permission (customer) with memberOnly', (done) => { + request(server) + .get(`/v5/projects/${projectId}/phases/`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .query({ memberOnly: true }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body; + should.exist(resJson); + resJson.should.have.lengthOf(0); + done(); + } + }); + }); }); }); From 3646aef4e10ea13f275ba5257cd3b1637c56feb3 Mon Sep 17 00:00:00 2001 From: Ahmad Alkhawaja Date: Mon, 19 Jul 2021 11:46:47 +0300 Subject: [PATCH 02/17] Adding support to store linking between copilots and phases (aka milestones in the new concept) Added cross ref table and CRUD api endpoints --- config/default.json | 12 +- docs/Project API.postman_collection.json | 961 +++++++++--------- docs/swagger.yaml | 147 +++ .../20210718_project_phase_member_table.sql | 13 + package-lock.json | 363 ++++--- src/events/projects/index.js | 4 +- src/models/projectPhase.js | 1 + src/models/projectPhaseMember.js | 62 ++ src/permissions/index.js | 4 + src/routes/index.js | 7 + src/routes/phaseMembers/delete.js | 80 ++ src/routes/phaseMembers/delete.spec.js | 158 +++ src/routes/phaseMembers/list.js | 66 ++ src/routes/phaseMembers/list.spec.js | 160 +++ src/routes/phaseMembers/update.js | 91 ++ src/routes/phaseMembers/update.spec.js | 178 ++++ src/routes/phases/create.spec.js | 1 - src/routes/phases/get.js | 22 +- src/routes/phases/list.js | 67 +- src/routes/phases/list.spec.js | 76 +- 20 files changed, 1805 insertions(+), 668 deletions(-) create mode 100644 migrations/20210718_project_phase_member_table.sql create mode 100644 src/models/projectPhaseMember.js create mode 100644 src/routes/phaseMembers/delete.js create mode 100644 src/routes/phaseMembers/delete.spec.js create mode 100644 src/routes/phaseMembers/list.js create mode 100644 src/routes/phaseMembers/list.spec.js create mode 100644 src/routes/phaseMembers/update.js create mode 100644 src/routes/phaseMembers/update.spec.js diff --git a/config/default.json b/config/default.json index 9e333c67..9cf176ae 100644 --- a/config/default.json +++ b/config/default.json @@ -27,9 +27,9 @@ "metadataDocType": "doc", "metadataDocDefaultId": 1 }, - "connectProjectUrl":"", + "connectProjectUrl": "", "dbConfig": { - "masterUrl": "", + "masterUrl": "postgres://coder:mysecretpassword@localhost:5432/projectsdb", "maxPoolSize": 50, "minPoolSize": 4, "idleTimeout": 1000 @@ -48,11 +48,11 @@ "maxPhaseProductCount": 1, "TOKEN_CACHE_TIME": "86000", "whitelistedOriginsForUserIdAuth": "[\"https:\/\/topcoder-newauth.auth0.com\/\",\"https:\/\/api.topcoder-dev.com\"]", - "EMAIL_INVITE_FROM_NAME":"Topcoder", - "EMAIL_INVITE_FROM_EMAIL":"noreply@connect.topcoder.com", + "EMAIL_INVITE_FROM_NAME": "Topcoder", + "EMAIL_INVITE_FROM_EMAIL": "noreply@connect.topcoder.com", "inviteEmailSubject": "You are invited to Topcoder", "inviteEmailSectionTitle": "Project Invitation", - "connectUrl":"https://connect.topcoder-dev.com", + "connectUrl": "https://connect.topcoder-dev.com", "accountsAppUrl": "https://accounts.topcoder-dev.com", "MAX_REVISION_NUMBER": 100, "UNIQUE_GMAIL_VALIDATION": false, @@ -84,4 +84,4 @@ "CLIENT_ID": "" }, "sfdcBillingAccountNameField": "Billing_Account_Name__c" -} +} \ No newline at end of file diff --git a/docs/Project API.postman_collection.json b/docs/Project API.postman_collection.json index 71167a99..0146caa6 100644 --- a/docs/Project API.postman_collection.json +++ b/docs/Project API.postman_collection.json @@ -1,17 +1,15 @@ { "info": { - "_postman_id": "c69ab4dd-8c6a-48c9-ba48-c6663b1a0c81", + "_postman_id": "3eba12ae-a066-4d5a-bdd5-3121377e476b", "name": "Project API", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, "item": [ { "name": "Project Attachments", - "description": null, "item": [ { "name": "bookmarks", - "description": null, "item": [ { "name": " Create project without bookmarks", @@ -117,10 +115,6 @@ "value": "application/json" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}", "host": [ @@ -237,10 +231,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects", "host": [ @@ -253,8 +243,7 @@ }, "response": [] } - ], - "_postman_isSubFolder": true + ] }, { "name": "Upload file attachment", @@ -262,7 +251,6 @@ { "listen": "test", "script": { - "id": "6547ada6-53f5-4e2d-bda0-f0ec5bfbe38f", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -310,7 +298,6 @@ { "listen": "test", "script": { - "id": "6547ada6-53f5-4e2d-bda0-f0ec5bfbe38f", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -508,10 +495,6 @@ "value": "application/json" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/attachments", "host": [ @@ -599,7 +582,6 @@ }, { "name": "Project With TemplateId issue", - "description": null, "item": [ { "name": "Create project with templateId (not existed)", @@ -637,7 +619,6 @@ { "listen": "test", "script": { - "id": "ed52d7f8-829b-40ec-8583-7e8dbcc6741c", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -680,7 +661,6 @@ }, { "name": "Project Members", - "description": null, "item": [ { "name": "Create project member with no payload", @@ -754,7 +734,6 @@ { "listen": "test", "script": { - "id": "82218ddf-4bd4-485f-bcd4-1d653c674680", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -835,7 +814,6 @@ { "listen": "test", "script": { - "id": "ba7b3265-aaba-4cca-8a53-819c9e96cfae", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -883,7 +861,6 @@ { "listen": "test", "script": { - "id": "87ab173c-8a4b-4d12-bfe7-ebcd272289f1", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -1085,10 +1062,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/members", "host": [ @@ -1114,10 +1087,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/members?role=customer", "host": [ @@ -1153,10 +1122,6 @@ "value": "application/json" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/members/{{memberId}}", "host": [ @@ -1174,7 +1139,7 @@ "response": [] }, { - "name": "Delete project member Copy", + "name": "Delete project member", "request": { "method": "DELETE", "header": [ @@ -1244,7 +1209,6 @@ }, { "name": "Project Members Invites", - "description": null, "item": [ { "name": "List project member invite", @@ -1327,7 +1291,6 @@ { "listen": "test", "script": { - "id": "320b75fe-958d-44ee-b2d2-1716b7b3e207", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -1382,7 +1345,6 @@ { "listen": "test", "script": { - "id": "3835313a-bb42-487a-b17e-4d687535d7e5", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -1483,10 +1445,6 @@ "value": "application/json" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/invites/{{inviteId}}", "host": [ @@ -1661,7 +1619,6 @@ { "listen": "prerequest", "script": { - "id": "522949a9-3d94-4103-92b2-976af332f203", "type": "text/javascript", "exec": [ "" @@ -1671,7 +1628,6 @@ { "listen": "test", "script": { - "id": "df2755ee-59a5-4d8d-a6ad-6416b697c894", "type": "text/javascript", "exec": [ "" @@ -1682,7 +1638,6 @@ }, { "name": "Projects", - "description": "Requests for all things projects.", "item": [ { "name": "Create project without payload", @@ -1752,7 +1707,6 @@ { "listen": "test", "script": { - "id": "5d951b47-5aac-4af6-a24b-ef6c998b913e", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -1833,10 +1787,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}", "host": [ @@ -1861,10 +1811,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}?fields=id,name,description,members.id,members.projectId", "host": [ @@ -1895,10 +1841,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects", "host": [ @@ -1922,10 +1864,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects?perPage=1&page=1", "host": [ @@ -1959,10 +1897,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects?type=generic", "host": [ @@ -1992,10 +1926,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects?id=1&id=2", "host": [ @@ -2029,10 +1959,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects?sort=type asc", "host": [ @@ -2062,10 +1988,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects?fields=id,name,description", "host": [ @@ -2095,10 +2017,6 @@ "value": "Bearer {{jwt-token-copilot-40051332}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects", "host": [ @@ -2648,11 +2566,11 @@ }, "response": [] } - ] + ], + "description": "Requests for all things projects." }, { "name": "Workstream", - "description": "Requests for all things projects.", "item": [ { "name": "Create workstream without payload", @@ -2693,7 +2611,6 @@ { "listen": "test", "script": { - "id": "15506f7a-77d3-46cb-9b37-41015ffbfdbc", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -2778,10 +2695,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}", "host": [ @@ -2808,10 +2721,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/workstreams", "host": [ @@ -2891,11 +2800,11 @@ }, "response": [] } - ] + ], + "description": "Requests for all things projects." }, { "name": "Work", - "description": "Requests for all things projects.", "item": [ { "name": "Create work without payload", @@ -2938,7 +2847,6 @@ { "listen": "test", "script": { - "id": "34d06fac-76f6-47b8-9b70-6e9c558e2bf1", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -3062,10 +2970,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}/works/{{workId}}", "host": [ @@ -3094,10 +2998,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}/works", "host": [ @@ -3125,10 +3025,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}/works?sort=startDate desc", "host": [ @@ -3162,10 +3058,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}/works?fields=status,name,budget", "host": [ @@ -3291,11 +3183,11 @@ }, "response": [] } - ] + ], + "description": "Requests for all things projects." }, { "name": "Work Item", - "description": "Requests for all things projects.", "item": [ { "name": "Create work item without payload", @@ -3431,10 +3323,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}/works/{{workId}}/workitems/{{itemId}}", "host": [ @@ -3465,10 +3353,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}/works/{{workId}}/workitems", "host": [ @@ -3560,11 +3444,11 @@ }, "response": [] } - ] + ], + "description": "Requests for all things projects." }, { "name": "Work Management Permission", - "description": "Requests for all things projects.", "item": [ { "name": "Create work management permission without payload", @@ -3605,7 +3489,6 @@ { "listen": "test", "script": { - "id": "305f3d68-6e9f-4c4d-b031-7487986a93e2", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -3653,7 +3536,6 @@ { "listen": "test", "script": { - "id": "305f3d68-6e9f-4c4d-b031-7487986a93e2", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -3738,10 +3620,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/workManagementPermission/{{workManagementPermissionId}}", "host": [ @@ -3768,10 +3646,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/workManagementPermission", "host": [ @@ -3797,10 +3671,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/workManagementPermission?filter=template", "host": [ @@ -3832,10 +3702,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/workManagementPermission?filter=invalid%3D2%26projectTemplateId%3D1", "host": [ @@ -3867,10 +3733,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/workManagementPermission?filter=projectTemplateId%3D1", "host": [ @@ -3956,11 +3818,11 @@ }, "response": [] } - ] + ], + "description": "Requests for all things projects." }, { "name": "Permissions", - "description": null, "item": [ { "name": "Get permissions - 404", @@ -3972,10 +3834,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/9999/permissions", "host": [ @@ -4069,10 +3927,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/permissions", "host": [ @@ -4098,10 +3952,6 @@ "value": "Bearer {{jwt-token-manager-40051334}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/permissions", "host": [ @@ -4122,7 +3972,6 @@ { "listen": "prerequest", "script": { - "id": "5197d1ec-429c-4a6f-9e9c-3ec3cd6f292a", "type": "text/javascript", "exec": [ "" @@ -4132,7 +3981,6 @@ { "listen": "test", "script": { - "id": "cc0cbbf1-54d1-481f-b8fa-a6dc4c80e993", "type": "text/javascript", "exec": [ "" @@ -4143,7 +3991,6 @@ }, { "name": "WorkManagementForTemplate", - "description": "Requests for all things projects.", "item": [ { "name": "Create workstream with valid values", @@ -4477,11 +4324,11 @@ }, "response": [] } - ] + ], + "description": "Requests for all things projects." }, { "name": "EventHandling and Integration with Direct Project API", - "description": null, "item": [ { "name": "mock direct projects", @@ -4497,10 +4344,6 @@ "value": "application/json" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "https://api.topcoder-dev.com/v3/direct/projects", "protocol": "https", @@ -4714,7 +4557,6 @@ { "listen": "prerequest", "script": { - "id": "ef96ac6a-0fc0-4a64-a4fe-5390e17afe67", "type": "text/javascript", "exec": [ "" @@ -4724,7 +4566,6 @@ { "listen": "test", "script": { - "id": "12f9d794-0872-4058-aafa-77b89e72025b", "type": "text/javascript", "exec": [ "" @@ -4735,7 +4576,6 @@ }, { "name": "Project Phase", - "description": null, "item": [ { "name": "Create Phase", @@ -4743,7 +4583,6 @@ { "listen": "test", "script": { - "id": "7050133a-b934-4faf-8101-d2e80b5c0710", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -4790,7 +4629,6 @@ { "listen": "test", "script": { - "id": "2f771afe-7b4e-4260-b04d-324e880eb61b", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -4837,7 +4675,6 @@ { "listen": "test", "script": { - "id": "8415ad98-b3f6-4330-88b6-e1830da2e4f9", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -4892,10 +4729,6 @@ "value": "application/json" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/phases", "host": [ @@ -4924,10 +4757,6 @@ "value": "application/json" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/phases?fields=status,name,budget", "host": [ @@ -4948,6 +4777,40 @@ }, "response": [] }, + { + "name": "List Phase with member field", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{api-url}}/projects/{{projectId}}/phases?fields=status,name,budget,members,products", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "{{projectId}}", + "phases" + ], + "query": [ + { + "key": "fields", + "value": "status,name,budget,members,products" + } + ] + } + }, + "response": [] + }, { "name": "List Phase with sort", "request": { @@ -4962,10 +4825,6 @@ "value": "application/json" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/phases?sort=status desc", "host": [ @@ -5000,10 +4859,6 @@ "value": "application/json" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/phases?sort=order desc", "host": [ @@ -5024,6 +4879,40 @@ }, "response": [] }, + { + "name": "List Phase with memberOnly", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{api-url}}/projects/{{projectId}}/phases?memberOnly=true", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "{{projectId}}", + "phases" + ], + "query": [ + { + "key": "memberOnly", + "value": "true" + } + ] + } + }, + "response": [] + }, { "name": "Get Phase", "request": { @@ -5038,10 +4927,6 @@ "value": "application/json" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}", "host": [ @@ -5160,7 +5045,6 @@ }, { "name": "Phase Products", - "description": null, "item": [ { "name": "Create Phase Product", @@ -5168,7 +5052,6 @@ { "listen": "test", "script": { - "id": "77f089b3-cbe6-4fb4-b54f-2a52d138a050", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -5221,12 +5104,100 @@ "value": "Bearer {{jwt-token}}" } ], + "url": { + "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/products", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "{{projectId}}", + "phases", + "{{phaseId}}", + "products" + ] + } + }, + "response": [] + }, + { + "name": "Get Phase Product", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + } + ], + "url": { + "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/products/{{phaseProductId}}", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "{{projectId}}", + "phases", + "{{phaseId}}", + "products", + "{{phaseProductId}}" + ] + } + }, + "response": [] + }, + { + "name": "Update Phase Product", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"name\": \"test phase product xxx\",\n\t\"type\": \"type 2\",\n\t\"templateId\": 10,\n\t\"estimatedPrice\": 1.234567,\n\t\"actualPrice\": 2.34567,\n\t\"details\": {\n\t\t\"message\": \"this is a JSON type. You can use any json\"\n\t}\n}" + }, + "url": { + "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/products/{{phaseProductId}}", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "{{projectId}}", + "phases", + "{{phaseId}}", + "products", + "{{phaseProductId}}" + ] + } + }, + "response": [] + }, + { + "name": "Delete Phase Product", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + } + ], "body": { "mode": "raw", "raw": "" }, "url": { - "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/products", + "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/products/{{phaseProductId}}", "host": [ "{{api-url}}" ], @@ -5235,28 +5206,302 @@ "{{projectId}}", "phases", "{{phaseId}}", - "products" + "products", + "{{phaseProductId}}" ] } - }, - "response": [] + }, + "response": [] + } + ] + }, + { + "name": "Phase Members", + "item": [ + { + "name": "Before Start", + "item": [ + { + "name": "Create project type", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 201\", function () {", + " pm.response.to.have.status(201);", + " if(pm.response.status === \"Created\") {", + " const response = pm.response.json()", + " pm.environment.set(\"projectTypeId\", response.key);", + " }", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"key\": \"new key\",\r\n \"displayName\": \"new displayName\",\r\n \"icon\": \"http://example.com/icon4.ico\",\r\n \t\"question\": \"question 4\",\r\n \t\"info\": \"info 4\",\r\n \t\"aliases\": [\"key-41\", \"key_42\"],\r\n \t\"metadata\": {}\r\n }" + }, + "url": { + "raw": "{{api-url}}/projects/metadata/projectTypes", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "metadata", + "projectTypes" + ] + } + }, + "response": [] + }, + { + "name": "Create project", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 201\", function () {", + " pm.response.to.have.status(201);", + " if(pm.response.status === \"Created\") {", + " const response = pm.response.json()", + " pm.environment.set(\"projectId\", response.id);", + " }", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"name\": \"test project\",\n\t\"description\": \"Hello I am a test project\",\n\t\"type\": \"{{projectTypeId}}\"\n}" + }, + "url": { + "raw": "{{api-url}}/projects", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects" + ] + }, + "description": "Valid request body. Project should be created successfully." + }, + "response": [] + }, + { + "name": "Create project member - 1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 201\", function () {", + " pm.response.to.have.status(201);", + " if(pm.response.status === \"Created\") {", + " const response = pm.response.json()", + " pm.environment.set(\"phaseMemberId-1\", response.userId);", + " }", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"userId\": \"40158994\",\n \"role\": \"copilot\"\n}" + }, + "url": { + "raw": "{{api-url}}/projects/{{projectId}}/members", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "{{projectId}}", + "members" + ] + }, + "description": "If the request payload is valid, than project member should be created." + }, + "response": [] + }, + { + "name": "Create project member - 2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 201\", function () {", + " pm.response.to.have.status(201);", + " if(pm.response.status === \"Created\") {", + " const response = pm.response.json()", + " pm.environment.set(\"phaseMemberId-2\", response.userId);", + " }", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"userId\": \"40153800\",\n \"role\": \"copilot\"\n}" + }, + "url": { + "raw": "{{api-url}}/projects/{{projectId}}/members", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "{{projectId}}", + "members" + ] + }, + "description": "If the request payload is valid, than project member should be created." + }, + "response": [] + }, + { + "name": "Create Phase", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 201\", function () {", + " pm.response.to.have.status(201);", + " if(pm.response.status === \"Created\") {", + " const response = pm.response.json()", + " pm.environment.set(\"phaseId\", response.id);", + " }", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"name\": \"test project phase\",\n\t\"status\": \"active\",\n\t\"startDate\": \"2018-05-15T00:00:00\",\n\t\"endDate\": \"2018-05-16T00:00:00\",\n\t\"budget\": 20,\n\t\"details\": {\n\t\t\"aDetails\": \"a details\"\n\t}\n}" + }, + "url": { + "raw": "{{api-url}}/projects/{{projectId}}/phases", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "{{projectId}}", + "phases" + ] + } + }, + "response": [] + } + ] }, { - "name": "Get Phase Product", + "name": "Update Phase Members", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], "request": { - "method": "GET", + "method": "POST", "header": [ { "key": "Authorization", "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" } ], "body": { "mode": "raw", - "raw": "" + "raw": "{\n\t\"userIds\": [{{phaseMemberId-1}},{{phaseMemberId-2}}]\n}" }, "url": { - "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/products/{{phaseProductId}}", + "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/members", "host": [ "{{api-url}}" ], @@ -5265,17 +5510,29 @@ "{{projectId}}", "phases", "{{phaseId}}", - "products", - "{{phaseProductId}}" + "members" ] } }, "response": [] }, { - "name": "Update Phase Product", + "name": "List Phase Members", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], "request": { - "method": "PATCH", + "method": "GET", "header": [ { "key": "Authorization", @@ -5286,12 +5543,8 @@ "value": "application/json" } ], - "body": { - "mode": "raw", - "raw": "{\n\t\"name\": \"test phase product xxx\",\n\t\"type\": \"type 2\",\n\t\"templateId\": 10,\n\t\"estimatedPrice\": 1.234567,\n\t\"actualPrice\": 2.34567,\n\t\"details\": {\n\t\t\"message\": \"this is a JSON type. You can use any json\"\n\t}\n}" - }, "url": { - "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/products/{{phaseProductId}}", + "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/members", "host": [ "{{api-url}}" ], @@ -5300,21 +5553,37 @@ "{{projectId}}", "phases", "{{phaseId}}", - "products", - "{{phaseProductId}}" + "members" ] } }, "response": [] }, { - "name": "Delete Phase Product", + "name": "Delete Phase Member", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 204\", function () {\r", + " pm.response.to.have.status(204);\r", + "});" + ], + "type": "text/javascript" + } + } + ], "request": { "method": "DELETE", "header": [ { "key": "Authorization", "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" } ], "body": { @@ -5322,7 +5591,7 @@ "raw": "" }, "url": { - "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/products/{{phaseProductId}}", + "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/members/{{phaseMemberId-2}}", "host": [ "{{api-url}}" ], @@ -5331,8 +5600,8 @@ "{{projectId}}", "phases", "{{phaseId}}", - "products", - "{{phaseProductId}}" + "members", + "{{phaseMemberId-2}}" ] } }, @@ -5342,7 +5611,6 @@ }, { "name": "Project Templates", - "description": null, "item": [ { "name": "Create project template", @@ -5350,7 +5618,6 @@ { "listen": "test", "script": { - "id": "2f79c07b-8076-4715-abf7-1d6903df444f", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -5397,7 +5664,6 @@ { "listen": "test", "script": { - "id": "4c442ea3-0834-4a30-8044-a4e94fd4ea2d", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -5444,7 +5710,6 @@ { "listen": "test", "script": { - "id": "7d0ae3ca-fe2d-40eb-b5c8-9b03955babec", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -5563,10 +5828,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/projectTemplates", "host": [ @@ -5595,10 +5856,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/projectTemplates/{{projectTemplateId}}", "host": [ @@ -5852,7 +6109,6 @@ }, { "name": "Product Templates", - "description": null, "item": [ { "name": "Create product template", @@ -5860,7 +6116,6 @@ { "listen": "test", "script": { - "id": "b5aaf185-6026-4b58-b9b8-56616109cd5a", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -5907,7 +6162,6 @@ { "listen": "test", "script": { - "id": "d5a2af2e-97d2-415c-a533-1d52dd4003c7", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -6026,10 +6280,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/productTemplates", "host": [ @@ -6058,10 +6308,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/productTemplates/{{productTemplateId}}", "host": [ @@ -6255,7 +6501,6 @@ }, { "name": "Project Type", - "description": null, "item": [ { "name": "Create project type", @@ -6263,7 +6508,6 @@ { "listen": "test", "script": { - "id": "fbc45946-a3f2-433a-8ec5-0af82b69d2bd", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -6318,10 +6562,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/projectTypes", "host": [ @@ -6350,10 +6590,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/projectTypes/{{projectTypeId}}", "host": [ @@ -6439,7 +6675,6 @@ }, { "name": "Org Config", - "description": null, "item": [ { "name": "Create org config", @@ -6447,7 +6682,6 @@ { "listen": "test", "script": { - "id": "fbc45946-a3f2-433a-8ec5-0af82b69d2bd", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -6505,10 +6739,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/orgConfig", "host": [ @@ -6537,10 +6767,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/orgConfig?orgId={{orgStrId}}", "host": [ @@ -6576,10 +6802,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/orgConfig?orgId={{orgStrId}}&configName={{orgConfigName}}", "host": [ @@ -6676,7 +6898,6 @@ { "listen": "prerequest", "script": { - "id": "2e274cc9-22e6-4dd2-9eee-c4f1fd98253d", "type": "text/javascript", "exec": [ "" @@ -6686,7 +6907,6 @@ { "listen": "test", "script": { - "id": "9d171dbf-2a50-4483-b172-ce240ac09413", "type": "text/javascript", "exec": [ "" @@ -6697,7 +6917,6 @@ }, { "name": "Product Category", - "description": null, "item": [ { "name": "Create product category", @@ -6705,7 +6924,6 @@ { "listen": "test", "script": { - "id": "06156797-ceb2-4f8c-9448-5c453adb7b7a", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -6760,10 +6978,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/productCategories", "host": [ @@ -6792,10 +7006,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/productCategories/{{productCategoryId}}", "host": [ @@ -6892,7 +7102,6 @@ { "listen": "prerequest", "script": { - "id": "f0092ef5-e624-4c25-87b2-b6a9e4c81ec8", "type": "text/javascript", "exec": [ "" @@ -6902,7 +7111,6 @@ { "listen": "test", "script": { - "id": "9183c429-a5e0-4bf9-96a2-89f4d66e9b0d", "type": "text/javascript", "exec": [ "" @@ -6913,7 +7121,6 @@ }, { "name": "Project upgrade", - "description": "Request to migrate projects.", "item": [ { "name": "Migrate project", @@ -7043,11 +7250,11 @@ }, "response": [] } - ] + ], + "description": "Request to migrate projects." }, { "name": "Timeline", - "description": null, "item": [ { "name": "Create timeline", @@ -7055,7 +7262,6 @@ { "listen": "test", "script": { - "id": "c066e7d4-537f-406e-a768-ec4bf73a2634", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -7100,7 +7306,6 @@ { "listen": "test", "script": { - "id": "ee729ed9-0072-4821-9141-3615ff66f728", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -7189,10 +7394,6 @@ "type": "text" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/timelines?reference=project&referenceId={{projectId}}", "host": [ @@ -7229,10 +7430,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/timelines/{{timelineId}}", "host": [ @@ -7260,10 +7457,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/timelines/{{timelineId}}", "host": [ @@ -7405,7 +7598,6 @@ }, { "name": "Milestone", - "description": null, "item": [ { "name": "Create milestone", @@ -7413,7 +7605,6 @@ { "listen": "test", "script": { - "id": "8fd1d5e9-8e6e-4cd7-9010-b855308be069", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -7501,10 +7692,6 @@ "type": "text" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/timelines/{{timelineId}}/milestones", "host": [ @@ -7533,10 +7720,6 @@ "value": "Bearer {{jwt-token-copilot-40051332}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/timelines/{{timelineId}}/milestones?sort=order desc", "host": [ @@ -7571,10 +7754,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/timelines/{{timelineId}}/milestones/{{milestoneId}}", "host": [ @@ -7596,7 +7775,6 @@ { "listen": "test", "script": { - "id": "8fd1d5e9-8e6e-4cd7-9010-b855308be069", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -7971,7 +8149,6 @@ }, { "name": "Milestone Template", - "description": null, "item": [ { "name": "Create milestone template", @@ -7979,7 +8156,6 @@ { "listen": "test", "script": { - "id": "3dbf8b29-2498-4b05-93de-14d809ccc285", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -8197,10 +8373,6 @@ "value": "Bearer {{jwt-token-copilot-40051332}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/timelines/metadata/milestoneTemplates", "host": [ @@ -8229,10 +8401,6 @@ "value": "Bearer {{jwt-token-copilot-40051332}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/timelines/metadata/milestoneTemplates?reference=productTemplate&referenceId={{productTemplateId}}", "host": [ @@ -8271,10 +8439,6 @@ "value": "Bearer {{jwt-token-copilot-40051332}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/timelines/metadata/milestoneTemplates?reference=productTemplate&referenceId={{productTemplateId}}&sort=order desc", "host": [ @@ -8317,10 +8481,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/timelines/metadata/milestoneTemplates/{{milestoneTemplateId}}", "host": [ @@ -8571,7 +8731,6 @@ }, { "name": "Metadata", - "description": null, "item": [ { "name": "Get all metadata", @@ -8594,10 +8753,6 @@ "type": "text" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata", "host": [ @@ -8626,10 +8781,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata?includeAllReferred=true", "host": [ @@ -8653,7 +8804,6 @@ }, { "name": "Form Version", - "description": null, "item": [ { "name": "List forms", @@ -8670,10 +8820,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/form/{{formKey}}/versions", "host": [ @@ -8705,10 +8851,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/form/{{formKey}}/versions/{{formVersion}}", "host": [ @@ -8741,10 +8883,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/form/{{formKey}}", "host": [ @@ -8766,7 +8904,6 @@ { "listen": "test", "script": { - "id": "94f6be66-34cc-40c8-80c2-b27dd93ed527", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -8908,7 +9045,6 @@ }, { "name": "Form Revision", - "description": null, "item": [ { "name": "List all revision for version", @@ -8925,10 +9061,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/form/{{formKey}}/versions/{{formVersion}}/revisions", "host": [ @@ -8962,10 +9094,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/form/{{formKey}}/versions/{{formVersion}}/revisions/{{formRevision}}", "host": [ @@ -8991,7 +9119,6 @@ { "listen": "test", "script": { - "id": "dbe5ec9f-022c-4ec5-b58c-d19c15430b61", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -9137,7 +9264,6 @@ }, { "name": "Price Config Version", - "description": null, "item": [ { "name": "List price configs", @@ -9154,10 +9280,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/priceConfig/dev/versions", "host": [ @@ -9189,10 +9311,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/priceConfig/{{priceKey}}/versions/{{priceVersion}}", "host": [ @@ -9225,10 +9343,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/priceConfig/{{priceKey}}", "host": [ @@ -9250,7 +9364,6 @@ { "listen": "test", "script": { - "id": "e440c87c-49ff-4443-b9bf-b44d4e9a480f", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -9393,7 +9506,6 @@ { "listen": "prerequest", "script": { - "id": "59182724-4332-4d76-90ea-f7520a7b1be9", "type": "text/javascript", "exec": [ "" @@ -9403,7 +9515,6 @@ { "listen": "test", "script": { - "id": "abc13dca-e8a4-4995-970f-00e5889a5f2d", "type": "text/javascript", "exec": [ "" @@ -9414,7 +9525,6 @@ }, { "name": "Price Config Revision", - "description": null, "item": [ { "name": "List all revision for version", @@ -9431,10 +9541,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/priceConfig/dev/versions/3/revisions", "host": [ @@ -9468,10 +9574,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/priceConfig/{{priceKey}}/versions/{{priceVersion}}/revisions/{{priceRevision}}", "host": [ @@ -9497,7 +9599,6 @@ { "listen": "test", "script": { - "id": "d53ed608-b21c-4d6f-bb68-c2beda1d631d", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -9643,7 +9744,6 @@ }, { "name": "Plan Config Version", - "description": null, "item": [ { "name": "List plan configs", @@ -9660,10 +9760,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/planConfig/dev/versions", "host": [ @@ -9695,10 +9791,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/planConfig/{{planKey}}/versions/{{planVersion}}", "host": [ @@ -9731,10 +9823,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/planConfig/{{planKey}}", "host": [ @@ -9756,7 +9844,6 @@ { "listen": "test", "script": { - "id": "97bc350a-0c4f-46a6-a315-a62b203b3ad2", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -9898,7 +9985,6 @@ }, { "name": "Plan Config Revision", - "description": null, "item": [ { "name": "List all revision for version", @@ -9915,10 +10001,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/planConfig/{{planKey}}/versions/{{planVersion}}/revisions", "host": [ @@ -9952,10 +10034,6 @@ }, "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/metadata/planConfig/{{planKey}}/versions/{{planVersion}}/revisions/{{planRevision}}", "host": [ @@ -9981,7 +10059,6 @@ { "listen": "test", "script": { - "id": "a5373f1f-4beb-46f9-8538-10c938c204ba", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -10127,11 +10204,9 @@ }, { "name": "Project Reports", - "description": null, "item": [ { "name": "summary", - "description": null, "item": [ { "name": "get report by admin", @@ -10144,10 +10219,6 @@ "type": "text" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/reports?reportName=summary", "host": [ @@ -10179,10 +10250,6 @@ "type": "text" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/reports?reportName=summary", "host": [ @@ -10214,10 +10281,6 @@ "value": "Bearer {{jwt-token-admin-40051333}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/123456/reports?reportName=summary", "host": [ @@ -10249,10 +10312,6 @@ "value": "Bearer {{jwt-token-admin-40051333}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/reports?reportName=summary123", "host": [ @@ -10273,12 +10332,10 @@ }, "response": [] } - ], - "_postman_isSubFolder": true + ] }, { "name": "projectBudget", - "description": null, "item": [ { "name": "get report by admin", @@ -10291,10 +10348,6 @@ "value": "Bearer {{jwt-token-admin-40051333}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/reports?reportName=projectBudget", "host": [ @@ -10315,14 +10368,12 @@ }, "response": [] } - ], - "_postman_isSubFolder": true + ] } ] }, { "name": "Project Setting", - "description": null, "item": [ { "name": "Create project setting - double", @@ -10330,7 +10381,6 @@ { "listen": "test", "script": { - "id": "7350de08-5111-44f8-8a4c-3d0c48bcd8d4", "exec": [ "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", @@ -10377,7 +10427,6 @@ { "listen": "test", "script": { - "id": "bf3aa19f-517c-4103-9250-82d7847e7477", "exec": [ "" ], @@ -10421,7 +10470,6 @@ { "listen": "test", "script": { - "id": "7350de08-5111-44f8-8a4c-3d0c48bcd8d4", "exec": [ "" ], @@ -10465,7 +10513,6 @@ { "listen": "test", "script": { - "id": "7350de08-5111-44f8-8a4c-3d0c48bcd8d4", "exec": [ "" ], @@ -10541,7 +10588,6 @@ { "listen": "test", "script": { - "id": "7350de08-5111-44f8-8a4c-3d0c48bcd8d4", "exec": [ "" ], @@ -10713,7 +10759,6 @@ { "listen": "test", "script": { - "id": "7350de08-5111-44f8-8a4c-3d0c48bcd8d4", "exec": [ "" ], @@ -10765,10 +10810,6 @@ "value": "Bearer {{jwt-token}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/settings", "host": [ @@ -10797,10 +10838,6 @@ "value": "Bearer {{jwt-token-copilot-40051332}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/settings", "host": [ @@ -10829,10 +10866,6 @@ "value": "Bearer {{jwt-token-manager-40051334}}" } ], - "body": { - "mode": "raw", - "raw": "" - }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/settings", "host": [ @@ -11048,4 +11081,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index d8c6a839..125c87be 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -620,6 +620,7 @@ paths: startDate asc in: query type: string + - $ref: '#/parameters/memberOnlyQueryParam' responses: '200': description: A list of project phases @@ -1710,6 +1711,122 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/ErrorModel' + '/projects/{projectId}/phases/{phaseId}/members': + parameters: + - $ref: '#/parameters/projectIdParam' + - $ref: '#/parameters/phaseIdParam' + get: + tags: + - phase members + description: >- + Retrieve phase members by id. All users who can see project members can access + this endpoint. + security: + - Bearer: [] + responses: + '200': + description: phase members + schema: + type: array + items: + $ref: '#/definitions/PhaseMember' + '400': + description: Bad request + schema: + $ref: '#/definitions/ErrorModel' + '401': + description: Unauthorized + schema: + $ref: '#/definitions/ErrorModel' + '403': + description: Forbidden + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: Not found + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Internal Server Error + schema: + $ref: '#/definitions/ErrorModel' + post: + tags: + - phase members + security: + - Bearer: [] + description: >- + Update phase members. Only admin roles can access this + endpoint. + parameters: + - in: body + name: body + required: true + schema: + $ref: '#/definitions/NewPhaseMember' + responses: + '200': + description: Successfully updated phase members. + schema: + type: array + items: + $ref: '#/definitions/PhaseMember' + '401': + description: Unauthorized + schema: + $ref: '#/definitions/ErrorModel' + '403': + description: Forbidden + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: Not found + schema: + $ref: '#/definitions/ErrorModel' + '400': + description: Bad request + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Internal Server Error + schema: + $ref: '#/definitions/ErrorModel' + '/projects/{projectId}/phases/{phaseId}/members/{userId}': + parameters: + - $ref: '#/parameters/projectIdParam' + - $ref: '#/parameters/phaseIdParam' + - $ref: '#/parameters/userIdParam' + delete: + tags: + - phase members + description: >- + Remove an existing phase member. Only admin roles can + access this endpoint. + security: + - Bearer: [] + responses: + '204': + description: Phase member successfully removed + '400': + description: Bad request + schema: + $ref: '#/definitions/ErrorModel' + '401': + description: Unauthorized + schema: + $ref: '#/definitions/ErrorModel' + '403': + description: Forbidden + schema: + $ref: '#/definitions/ErrorModel' + '404': + description: Not found + schema: + $ref: '#/definitions/ErrorModel' + '500': + description: Internal Server Error + schema: + $ref: '#/definitions/ErrorModel' '/projects/{projectId}/upgrade': post: tags: @@ -4693,6 +4810,14 @@ parameters: type: integer format: int64 minimum: 1 + userIdParam: + name: userId + in: path + description: project phase user id param + required: true + type: integer + format: int64 + minimum: 1 workStreamIdParam: name: workStreamId in: path @@ -5789,6 +5914,28 @@ definitions: description: READ-ONLY. User that last updated this object readOnly: true - $ref: '#/definitions/ProductCategoryCreateRequest' + PhaseMember: + title: Phase member object + type: object + required: + - phaseId + - userId + properties: + phaseId: + type: integer + format: int64 + description: references to project phase id + userId: + type: integer + format: int64 + description: references to member's userId + NewPhaseMember: + title: New Phase members to Update + type: array + items: + type: integer + format: int64 + description: "The user id." ProjectTypeRequest: title: Project type request object type: object diff --git a/migrations/20210718_project_phase_member_table.sql b/migrations/20210718_project_phase_member_table.sql new file mode 100644 index 00000000..ac27ad5f --- /dev/null +++ b/migrations/20210718_project_phase_member_table.sql @@ -0,0 +1,13 @@ +CREATE TABLE "public"."project_phase_member" ( + "id" int8 NOT NULL DEFAULT nextval('project_phase_member_id_seq'::regclass), + "userId" int8 NOT NULL, + "deletedAt" timestamptz, + "createdAt" timestamptz, + "updatedAt" timestamptz, + "deletedBy" int4, + "createdBy" int4 NOT NULL, + "updatedBy" int4 NOT NULL, + "phaseId" int8, + CONSTRAINT "project_phase_member_phaseId_fkey" FOREIGN KEY ("phaseId") REFERENCES "public"."project_phases"("id") ON DELETE SET NULL ON UPDATE CASCADE, + PRIMARY KEY ("id") +); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2c8bc171..55a9bf14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -82,15 +82,20 @@ "join-component": "^1.1.0" } }, + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" + }, "@types/bluebird": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.0.tgz", "integrity": "sha1-JjNHCk6r6aR82aRf2yDtX5NAe8o=" }, "@types/body-parser": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz", + "integrity": "sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg==", "requires": { "@types/connect": "*", "@types/node": "*" @@ -103,20 +108,20 @@ "dev": true }, "@types/connect": { - "version": "3.4.33", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", - "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==", + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", "requires": { "@types/node": "*" } }, "@types/express": { - "version": "4.17.6", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.6.tgz", - "integrity": "sha512-n/mr9tZI83kd4azlPG5y997C/M4DNABK9yErhFM6hKdym4kkmd9j0vtsJyjFIwfRBxtrxZtAfGZCNRIBMFLK5w==", + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", "requires": { "@types/body-parser": "*", - "@types/express-serve-static-core": "*", + "@types/express-serve-static-core": "^4.17.18", "@types/qs": "*", "@types/serve-static": "*" } @@ -131,18 +136,19 @@ } }, "@types/express-serve-static-core": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.4.tgz", - "integrity": "sha512-dPs6CaRWxsfHbYDVU51VjEJaUJEcli4UI0fFMT4oWmgCvHj+j7oIxz5MLHVL0Rv++N004c21ylJNdWQvPkkb5w==", + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz", + "integrity": "sha512-3UJuW+Qxhzwjq3xhwXm2onQcFHn76frIYVbTu+kn24LFxI+dEhdfISDFovPB8VpEgW8oQCTpRuCe+0zJxB7NEA==", "requires": { "@types/node": "*", + "@types/qs": "*", "@types/range-parser": "*" } }, "@types/express-unless": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.1.tgz", - "integrity": "sha512-5fuvg7C69lemNgl0+v+CUxDYWVPSfXHhJPst4yTLcqi4zKJpORCxnDrnnilk3k0DTq/WrAUdvXFs01+vUqUZHw==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.2.tgz", + "integrity": "sha512-Q74UyYRX/zIgl1HSp9tUX2PlG8glkVm+59r7aK4KGKzC5jqKIOX6rrVLRQrzpZUQ84VukHtRoeAuon2nIssHPQ==", "requires": { "@types/express": "*" } @@ -158,9 +164,9 @@ "integrity": "sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==" }, "@types/mime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", - "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==" + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" }, "@types/node": { "version": "13.7.0", @@ -168,22 +174,22 @@ "integrity": "sha512-GnZbirvmqZUzMgkFn70c74OQpTTUcCzlhQliTzYjQMqg+hVKcDnxdL19Ne3UdYzdMA/+W3eb646FWn/ZaT1NfQ==" }, "@types/qs": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.1.tgz", - "integrity": "sha512-lhbQXx9HKZAPgBkISrBcmAcMpZsmpe/Cd/hY7LGZS5OfkySUBItnPZHgQPssWYUET8elF+yCFBbP1Q0RZPTdaw==" + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" }, "@types/range-parser": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", - "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" }, "@types/serve-static": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz", - "integrity": "sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g==", + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", "requires": { - "@types/express-serve-static-core": "*", - "@types/mime": "*" + "@types/mime": "^1", + "@types/node": "*" } }, "abbrev": { @@ -213,6 +219,29 @@ "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", "dev": true }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "agentkeepalive": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-3.5.2.tgz", @@ -225,6 +254,7 @@ "version": "6.11.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz", "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==", + "dev": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -479,40 +509,6 @@ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true }, - "auth0-js": { - "version": "9.13.2", - "resolved": "https://registry.npmjs.org/auth0-js/-/auth0-js-9.13.2.tgz", - "integrity": "sha512-gWlf+X3XhCT9JboYpGviflv0pHcaHFPGtkLXiebyJohHDKddiu2rZkezp9kZHEoXqxhtNqgWuuaXkcla5JtnXg==", - "requires": { - "base64-js": "^1.3.0", - "idtoken-verifier": "^2.0.2", - "js-cookie": "^2.2.0", - "qs": "^6.7.0", - "superagent": "^3.8.3", - "url-join": "^4.0.1", - "winchan": "^0.2.2" - }, - "dependencies": { - "crypto-js": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz", - "integrity": "sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==" - }, - "idtoken-verifier": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/idtoken-verifier/-/idtoken-verifier-2.0.2.tgz", - "integrity": "sha512-9UN83SKT9dtN3d7vNz3EMTqoaJi3D02Zg5XMqF6+bLrGL+Akbx4oj4SEWsgXtLF6cy46XrUcVzokFY+SWO+/MA==", - "requires": { - "base64-js": "^1.3.0", - "crypto-js": "^3.2.1", - "es6-promise": "^4.2.8", - "jsbn": "^1.1.0", - "unfetch": "^4.1.0", - "url-join": "^4.0.1" - } - } - } - }, "aws-sdk": { "version": "2.610.0", "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.610.0.tgz", @@ -535,9 +531,9 @@ "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", - "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" }, "axios": { "version": "0.19.2", @@ -2797,13 +2793,6 @@ "requires": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" - }, - "dependencies": { - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" - } } }, "ecdsa-sig-formatter": { @@ -2992,11 +2981,6 @@ "es6-symbol": "^3.1.1" } }, - "es6-promise": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" - }, "es6-symbol": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", @@ -4543,12 +4527,25 @@ "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" }, "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", "requires": { - "ajv": "^6.5.5", + "ajv": "^6.12.3", "har-schema": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + } } }, "has": { @@ -4700,6 +4697,31 @@ } } }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -4710,6 +4732,30 @@ "sshpk": "^1.7.0" } }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "requires": { + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -5473,11 +5519,6 @@ } } }, - "js-cookie": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", - "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" - }, "js-tokens": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", @@ -5503,9 +5544,9 @@ } }, "jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha1-sBMHyym2GKHtJux56RH4A8TaAEA=" + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, "jsesc": { "version": "1.3.0", @@ -5656,31 +5697,54 @@ } }, "jwks-rsa": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-1.7.0.tgz", - "integrity": "sha512-tq7DVJt9J6wTvl9+AQfwZIiPSuY2Vf0F+MovfRTFuBqLB1xgDVhegD33ChEAQ6yBv9zFvUIyj4aiwrSA5VehUw==", + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-1.12.3.tgz", + "integrity": "sha512-cFipFDeYYaO9FhhYJcZWX/IyZgc0+g316rcHnDpT2dNRNIE/lMOmWKKqp09TkJoYlNFzrEVODsR4GgXJMgWhnA==", "requires": { "@types/express-jwt": "0.0.42", + "axios": "^0.21.1", "debug": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", "jsonwebtoken": "^8.5.1", - "limiter": "^1.1.4", - "lru-memoizer": "^2.0.1", + "limiter": "^1.1.5", + "lru-memoizer": "^2.1.2", "ms": "^2.1.2", - "request": "^2.88.0" + "proxy-from-env": "^1.1.0" }, "dependencies": { + "axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } } }, + "follow-redirects": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz", + "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==" + }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" } } }, @@ -5717,31 +5781,6 @@ "integrity": "sha1-fQ0U7vPslwLG8wxg6oHxqNP5APs=", "dev": true }, - "le_node": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/le_node/-/le_node-1.8.0.tgz", - "integrity": "sha512-NXzjxBskZ4QawTNwlGdRG05jYU0LhV2nxxmP3x7sRMHyROV0jPdyyikO9at+uYrWX3VFt0Y/am11oKITedx0iw==", - "requires": { - "babel-runtime": "6.6.1", - "codependency": "0.1.4", - "json-stringify-safe": "5.0.1", - "lodash": "4.17.11", - "reconnect-core": "1.3.0", - "semver": "5.1.0" - }, - "dependencies": { - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" - }, - "semver": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.1.0.tgz", - "integrity": "sha1-hfLPhVBGXE3wAM99hvawVBBqueU=" - } - } - }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -5914,9 +5953,9 @@ } }, "lru-memoizer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.2.tgz", - "integrity": "sha512-N5L5xlnVcbIinNn/TJ17vHBZwBMt9t7aJDz2n97moWubjNl6VO9Ao2XuAGBBddkYdjrwR9HfzXbT6NfMZXAZ/A==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.4.tgz", + "integrity": "sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==", "requires": { "lodash.clonedeep": "^4.5.0", "lru-cache": "~4.0.0" @@ -7437,6 +7476,11 @@ "ipaddr.js": "1.9.0" } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -7468,6 +7512,26 @@ "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" }, + "r7insight_node": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/r7insight_node/-/r7insight_node-1.8.4.tgz", + "integrity": "sha512-6cQrzLkaOxdv/SRFXWRJjgFr8a3nXUOT/4IMFuBv+mWzBnu5DJl+HzONAsWYvclrlZnvfa54PaIPqPuPRSlbrQ==", + "requires": { + "babel-runtime": "6.6.1", + "codependency": "0.1.4", + "json-stringify-safe": "5.0.1", + "lodash": "4.17.15", + "reconnect-core": "1.3.0", + "semver": "5.1.0" + }, + "dependencies": { + "semver": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.1.0.tgz", + "integrity": "sha1-hfLPhVBGXE3wAM99hvawVBBqueU=" + } + } + }, "randomatic": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", @@ -8661,13 +8725,6 @@ "jsbn": "~0.1.0", "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" - }, - "dependencies": { - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" - } } }, "static-eval": { @@ -8905,17 +8962,16 @@ } }, "tc-core-library-js": { - "version": "github:appirio-tech/tc-core-library-js#f45352974dafe5a10c86fc50bdd59ef399b50c65", - "from": "github:appirio-tech/tc-core-library-js#v2.6.3", + "version": "github:appirio-tech/tc-core-library-js#c4ab01f468a98dc7e22f188a176794b5ea4f2f9d", + "from": "github:appirio-tech/tc-core-library-js#v2.6.6", "requires": { - "auth0-js": "^9.4.2", "axios": "^0.19.0", "bunyan": "^1.8.12", - "jsonwebtoken": "^8.3.0", - "jwks-rsa": "^1.3.0", - "le_node": "^1.3.1", - "lodash": "^4.17.10", + "jsonwebtoken": "^8.5.1", + "jwks-rsa": "^1.6.0", + "lodash": "^4.17.15", "millisecond": "^0.1.2", + "r7insight_node": "^1.8.4", "request": "^2.88.0" } }, @@ -9177,11 +9233,6 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" }, - "unfetch": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.1.0.tgz", - "integrity": "sha512-crP/n3eAPUJxZXM9T80/yv0YhkTEx2K1D3h7D1AJM6fzsWZrxdyRuLN0JH/dkZh1LNH8LxCnBzoPFCPbb2iGpg==" - }, "union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", @@ -9363,11 +9414,6 @@ "querystring": "0.2.0" } }, - "url-join": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", - "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" - }, "url-parse-lax": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", @@ -9536,11 +9582,6 @@ } } }, - "winchan": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/winchan/-/winchan-0.2.2.tgz", - "integrity": "sha512-pvN+IFAbRP74n/6mc6phNyCH8oVkzXsto4KCHPJ2AScniAnA1AmeLI03I2BzjePpaClGSI4GUMowzsD3qz5PRQ==" - }, "wkx": { "version": "0.4.8", "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.4.8.tgz", diff --git a/src/events/projects/index.js b/src/events/projects/index.js index b3401098..ea9a36e1 100644 --- a/src/events/projects/index.js +++ b/src/events/projects/index.js @@ -56,7 +56,7 @@ async function projectUpdatedKafkaHandler(app, topic, payload) { const doc = await eClient.get({ index: ES_PROJECT_INDEX, type: ES_PROJECT_TYPE, id: previousValue.id }); // console.log(doc._source, 'Received project from ES');// eslint-disable-line no-underscore-dangle const merged = _.merge(doc._source, project.get({ plain: true })); // eslint-disable-line no-underscore-dangle - console.log(merged, 'Merged project'); + app.logger.debug(merged, 'Merged project'); // update the merged document await eClient.update({ index: ES_PROJECT_INDEX, @@ -66,7 +66,7 @@ async function projectUpdatedKafkaHandler(app, topic, payload) { doc: merged, }, }); - console.log(`Succesfully updated project document in ES (projectId: ${previousValue.id})`); + app.logger.debug(`Succesfully updated project document in ES (projectId: ${previousValue.id})`); } catch (error) { throw Error(`failed to updated project document in elasitcsearch index (projectId: ${previousValue.id})` + `. Details: '${error}'.`); diff --git a/src/models/projectPhase.js b/src/models/projectPhase.js index 5c260e1d..7c22c7fe 100644 --- a/src/models/projectPhase.js +++ b/src/models/projectPhase.js @@ -42,6 +42,7 @@ module.exports = function defineProjectPhase(sequelize, DataTypes) { ProjectPhase.associate = (models) => { ProjectPhase.hasMany(models.PhaseProduct, { as: 'products', foreignKey: 'phaseId' }); + ProjectPhase.hasMany(models.ProjectPhaseMember, { as: 'members', foreignKey: 'phaseId' }); ProjectPhase.belongsToMany(models.WorkStream, { through: models.PhaseWorkStream, foreignKey: 'phaseId' }); }; diff --git a/src/models/projectPhaseMember.js b/src/models/projectPhaseMember.js new file mode 100644 index 00000000..91dbd22f --- /dev/null +++ b/src/models/projectPhaseMember.js @@ -0,0 +1,62 @@ +module.exports = function defineProjectPhaseMember(sequelize, DataTypes) { + const ProjectPhaseMember = sequelize.define('ProjectPhaseMember', { + id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, + userId: { type: DataTypes.BIGINT, allowNull: false }, + + deletedAt: { type: DataTypes.DATE, allowNull: true }, + createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + deletedBy: { type: DataTypes.INTEGER, allowNull: true }, + createdBy: { type: DataTypes.INTEGER, allowNull: false }, + updatedBy: { type: DataTypes.INTEGER, allowNull: false }, + }, + { + tableName: 'project_phase_member', + paranoid: true, + timestamps: true, + updatedAt: 'updatedAt', + createdAt: 'createdAt', + deletedAt: 'deletedAt', + defaultScope: { + attributes: { + exclude: ['deletedAt', 'deletedBy'], + }, + }, + hooks: { + afterCreate: (projectPhaseMember) => { + // eslint-disable-next-line no-param-reassign + delete projectPhaseMember.dataValues.deletedAt; + // eslint-disable-next-line no-param-reassign + delete projectPhaseMember.dataValues.deletedBy; + }, + }, + indexes: [ + { + unique: true, + fields: ['phaseId', 'userId'], + where: { + deletedAt: null, + }, + }, + ], + }); + + ProjectPhaseMember.getPhaseMembers = (phaseId, raw = true) => ProjectPhaseMember.findAll({ + where: { + phaseId, + }, + raw, + }); + + ProjectPhaseMember.getMemberPhases = (userId, raw = true) => ProjectPhaseMember.findAll({ + where: { + userId, + }, + raw, + }); + + ProjectPhaseMember.associate = (models) => { + ProjectPhaseMember.belongsTo(models.ProjectPhase, { foreignKey: 'phaseId' }); + }; + return ProjectPhaseMember; +}; diff --git a/src/permissions/index.js b/src/permissions/index.js index 9e4bc1e4..0abd4c7b 100644 --- a/src/permissions/index.js +++ b/src/permissions/index.js @@ -98,6 +98,10 @@ module.exports = () => { Authorizer.setPolicy('project.updatePhaseProduct', copilotAndAbove); Authorizer.setPolicy('project.deletePhaseProduct', copilotAndAbove); + Authorizer.setPolicy('phaseMember.update', projectAdmin); + Authorizer.setPolicy('phaseMember.delete', projectAdmin); + Authorizer.setPolicy('phaseMember.view', generalPermission(PERMISSION.READ_PROJECT_MEMBER)); + Authorizer.setPolicy('milestoneTemplate.clone', projectAdmin); Authorizer.setPolicy('milestoneTemplate.create', projectAdmin); Authorizer.setPolicy('milestoneTemplate.edit', projectAdmin); diff --git a/src/routes/index.js b/src/routes/index.js index 6b725728..a20e1a04 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -183,6 +183,13 @@ router.route('/v5/projects/:projectId(\\d+)/phases/:phaseId(\\d+)/products/:prod .patch(require('./phaseProducts/update')) .delete(require('./phaseProducts/delete')); +router.route('/v5/projects/:projectId(\\d+)/phases/:phaseId(\\d+)/members') + .get(require('./phaseMembers/list')) + .post(require('./phaseMembers/update')); + +router.route('/v5/projects/:projectId(\\d+)/phases/:phaseId(\\d+)/members/:userId(\\d+)') + .delete(require('./phaseMembers/delete')); + router.route('/v5/projects/metadata/productCategories') .post(require('./productCategories/create')); diff --git a/src/routes/phaseMembers/delete.js b/src/routes/phaseMembers/delete.js new file mode 100644 index 00000000..032cf6d7 --- /dev/null +++ b/src/routes/phaseMembers/delete.js @@ -0,0 +1,80 @@ +import _ from 'lodash'; +import Joi from 'joi'; +import validate from 'express-validation'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import models from '../../models'; +import util from '../../util'; +import { EVENT, RESOURCES, ROUTES } from '../../constants'; + +/** + * API to update a project phase members. + */ +const permissions = tcMiddleware.permissions; + +const deletePhaseMemberValidations = { + params: { + projectId: Joi.number().integer().positive().required(), + phaseId: Joi.number().integer().positive().required(), + userId: Joi.number().integer().positive().required(), + }, +}; + +module.exports = [ + // handles request validations + validate(deletePhaseMemberValidations), + permissions('phaseMember.delete'), + async (req, res, next) => { + const projectId = _.parseInt(req.params.projectId); + const phaseId = _.parseInt(req.params.phaseId); + const userId = _.parseInt(req.params.userId); + let transaction; + try { + // check if project and phase exist + const phase = await models.ProjectPhase.findOne({ + where: { + id: phaseId, + projectId, + deletedAt: { $eq: null }, + }, + raw: true, + }); + if (!phase) { + const err = new Error('No active project phase found for project id ' + + `${projectId} and phase id ${phaseId}`); + err.status = 404; + throw (err); + } + // get current phase members. + const phaseMembers = await models.ProjectPhaseMember.getPhaseMembers(phaseId); + // find out which is to be deleted + const memberToDelete = _.find(phaseMembers, ['userId', userId]); + if (memberToDelete) { + transaction = await models.sequelize.transaction(); + const phaseMember = await models.ProjectPhaseMember.findOne({ + where: { + phaseId, + userId, + deletedAt: { $eq: null }, + }, + }); + await phaseMember.update({ deletedBy: req.authUser.userId }, { transaction }); + await phaseMember.destroy({ transaction }); + const updatedPhase = _.cloneDeep(phase); + util.sendResourceToKafkaBus( + req, + EVENT.ROUTING_KEY.PROJECT_PHASE_UPDATED, + RESOURCES.PHASE, + _.assign(updatedPhase, { members: _.filter(phaseMembers, member => member.userId !== userId) }), + _.assign(phase, { members: phaseMembers }), + ROUTES.PHASES.UPDATE); + await transaction.commit(); + } + res.status(204).end(); + } catch (err) { + if (transaction) { + await transaction.rollback(); + } + next(err); + } + }, +]; diff --git a/src/routes/phaseMembers/delete.spec.js b/src/routes/phaseMembers/delete.spec.js new file mode 100644 index 00000000..9d48b0ed --- /dev/null +++ b/src/routes/phaseMembers/delete.spec.js @@ -0,0 +1,158 @@ +/** + * Tests for delete.js + */ +import _ from 'lodash'; +import config from 'config'; +import request from 'supertest'; +import util from '../../util'; +import models from '../../models'; +import server from '../../app'; +import testUtil from '../../tests/util'; + +const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName'); +const ES_PROJECT_TYPE = config.get('elasticsearchConfig.docType'); +const eClient = util.getElasticSearchClient(); + +describe('Delete phase member', () => { + let id; + let project; + let phaseId; + const copilotUser = { + handle: testUtil.getDecodedToken(testUtil.jwts.copilot).handle, + userId: testUtil.getDecodedToken(testUtil.jwts.copilot).userId, + firstName: 'fname', + lastName: 'lName', + email: 'some@abc.com', + }; + const memberUser = { + handle: testUtil.getDecodedToken(testUtil.jwts.member).handle, + userId: testUtil.getDecodedToken(testUtil.jwts.member).userId, + firstName: 'fname', + lastName: 'lName', + email: 'some@abc.com', + }; + before(function beforeHook(done) { + this.timeout(20000); + // mocks + testUtil.clearDb() + .then(() => testUtil.clearES()) + .then(() => { + models.Project.create({ + type: 'generic', + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', + }).then((p) => { + id = p.id; + project = p.toJSON(); + // create members + models.ProjectMember.bulkCreate([{ + id: 1, + userId: copilotUser.userId, + projectId: id, + role: 'copilot', + isPrimary: false, + createdBy: 1, + updatedBy: 1, + }, { + id: 2, + userId: memberUser.userId, + projectId: id, + role: 'customer', + isPrimary: true, + createdBy: 1, + updatedBy: 1, + }]).then(() => { + models.ProjectPhase.create({ + name: 'test project phase', + projectId: id, + 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', + }, + createdBy: 1, + updatedBy: 1, + }).then((ph) => { + const phase = ph.toJSON(); + phaseId = phase.id; + models.ProjectPhaseMember.create({ + phaseId, + userId: copilotUser.userId, + }).then((phaseMember) => { + _.assign(phase, { members: [phaseMember.toJSON()] }); + // Index to ES + // Overwrite lastActivityAt as otherwise ES fill not be able to parse it + project.lastActivityAt = 1; + project.phases = [phase]; + return eClient.index({ + index: ES_PROJECT_INDEX, + type: ES_PROJECT_TYPE, + id, + body: project, + }).then(() => { + done(); + }); + }); + }); + }); + }); + }); + }); + + after((done) => { + testUtil.clearDb(done); + }); + describe('DELETE /projects/{projectId}/phases/{phaseId}/members/{memberId}', () => { + it('should return 403 for anonymous user', (done) => { + request(server) + .delete(`/v5/projects/${id}/phases/${phaseId}/members/${copilotUser.userId}`) + .expect(403, done); + }); + + it('should return 403 for regular user', (done) => { + request(server) + .delete(`/v5/projects/${id}/phases/${phaseId}/members/${copilotUser.userId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(403, done); + }); + + it('should return 200 for connect admin', (done) => { + request(server) + .delete(`/v5/projects/${id}/phases/${phaseId}/members/${copilotUser.userId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(204, done); + }); + + it('should return 204 for project admin', (done) => { + request(server) + .delete(`/v5/projects/${id}/phases/${phaseId}/members/${copilotUser.userId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(204, done); + }); + + it('should return 403 for copilot', (done) => { + request(server) + .delete(`/v5/projects/${id}/phases/${phaseId}/members/${copilotUser.userId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(403, done); + }); + }); +}); diff --git a/src/routes/phaseMembers/list.js b/src/routes/phaseMembers/list.js new file mode 100644 index 00000000..92e99a0a --- /dev/null +++ b/src/routes/phaseMembers/list.js @@ -0,0 +1,66 @@ +import _ from 'lodash'; +import config from 'config'; +import Joi from 'joi'; +import validate from 'express-validation'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import models from '../../models'; +import util from '../../util'; + +const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName'); +const ES_PROJECT_TYPE = config.get('elasticsearchConfig.docType'); + +/** + * API to list a project phase members. + */ +const permissions = tcMiddleware.permissions; + +const listPhaseMemberValidations = { + params: { + projectId: Joi.number().integer().positive().required(), + phaseId: Joi.number().integer().positive().required(), + }, +}; + +module.exports = [ + // handles request validations + validate(listPhaseMemberValidations), + permissions('phaseMember.view'), + async (req, res, next) => { + const projectId = _.parseInt(req.params.projectId); + const phaseId = _.parseInt(req.params.phaseId); + try { + const esClient = util.getElasticSearchClient(); + const project = await esClient.get({ index: ES_PROJECT_INDEX, type: ES_PROJECT_TYPE, id: projectId }); + // eslint-disable-next-line no-underscore-dangle + const phases = _.isArray(project._source.phases) ? project._source.phases : []; // eslint-disable-line no-underscore-dangle + const phase = _.find(phases, ['id', phaseId]); + const phaseMembers = phase.members || []; + res.json(phaseMembers); + return; + } catch (err) { + req.log.debug('No active project phase found in ES for project id ' + + `${projectId} and phase id ${phaseId}`); + } + try { + req.log.debug('Fall back to DB'); + const phase = await models.ProjectPhase.findOne({ + where: { + id: phaseId, + projectId, + deletedAt: { $eq: null }, + }, + raw: true, + }); + if (!phase) { + const err = new Error('No active project phase found for project id ' + + `${projectId} and phase id ${phaseId}`); + err.status = 404; + throw (err); + } + const phaseMembers = await models.ProjectPhaseMember.getPhaseMembers(phaseId); + res.json(phaseMembers); + } catch (err) { + next(err); + } + }, +]; diff --git a/src/routes/phaseMembers/list.spec.js b/src/routes/phaseMembers/list.spec.js new file mode 100644 index 00000000..e9b0a8c0 --- /dev/null +++ b/src/routes/phaseMembers/list.spec.js @@ -0,0 +1,160 @@ +/** + * Tests for list.js + */ +import _ from 'lodash'; +import config from 'config'; +import request from 'supertest'; +import chai from 'chai'; +import util from '../../util'; +import models from '../../models'; +import server from '../../app'; +import testUtil from '../../tests/util'; + +const should = chai.should(); + +const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName'); +const ES_PROJECT_TYPE = config.get('elasticsearchConfig.docType'); +const eClient = util.getElasticSearchClient(); + +describe('List phase members', () => { + let id; + let project; + let phaseId; + let memberId; + const copilotUser = { + handle: testUtil.getDecodedToken(testUtil.jwts.copilot).handle, + userId: testUtil.getDecodedToken(testUtil.jwts.copilot).userId, + firstName: 'fname', + lastName: 'lName', + email: 'some@abc.com', + }; + before(function beforeHook(done) { + this.timeout(20000); + // mocks + testUtil.clearDb() + .then(() => testUtil.clearES()) + .then(() => { + models.Project.create({ + type: 'generic', + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', + }).then((p) => { + id = p.id; + project = p.toJSON(); + // create members + models.ProjectMember.create({ + id: 1, + userId: copilotUser.userId, + projectId: id, + role: 'copilot', + isPrimary: false, + createdBy: 1, + updatedBy: 1, + }).then((member) => { + memberId = member.id; + models.ProjectPhase.create({ + name: 'test project phase', + projectId: id, + 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', + }, + createdBy: 1, + updatedBy: 1, + }).then((ph) => { + const phase = ph.toJSON(); + phaseId = phase.id; + models.ProjectPhaseMember.create({ + phaseId, + memberId, + userId: copilotUser.userId, + }).then((phaseMember) => { + _.assign(phase, { members: [phaseMember.toJSON()] }); + // Index to ES + // Overwrite lastActivityAt as otherwise ES fill not be able to parse it + project.lastActivityAt = 1; + project.phases = [phase]; + return eClient.index({ + index: ES_PROJECT_INDEX, + type: ES_PROJECT_TYPE, + id, + body: project, + }).then(() => { + done(); + }); + }); + }); + }); + }); + }); + }); + + after((done) => { + testUtil.clearDb(done); + }); + describe('GET /projects/{projectId}/phases/{phaseId}/members', () => { + it('should return 403 for anonymous user', (done) => { + request(server) + .get(`/v5/projects/${id}/phases/${phaseId}/members`) + .expect(403, done); + }); + + it('should return 403 for non project member user', (done) => { + request(server) + .get(`/v5/projects/${id}/phases/${phaseId}/members`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member2}`, + }) + .expect(403, done); + }); + + it('should return 200 for connect admin', (done) => { + request(server) + .get(`/v5/projects/${id}/phases/${phaseId}/members`) + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for project admin', (done) => { + request(server) + .get(`/v5/projects/${id}/phases/${phaseId}/members`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end(done); + }); + + it('should return 200 for copilot', (done) => { + request(server) + .get(`/v5/projects/${id}/phases/${phaseId}/members`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(200) + .end((err, res) => { + const resJson = res.body; + should.exist(resJson); + resJson.should.have.length(1); + resJson[0].userId.should.be.eql(copilotUser.userId); + resJson[0].memberId.should.be.eql(1); + resJson[0].phaseId.should.be.eql(phaseId); + done(); + }); + }); + }); +}); diff --git a/src/routes/phaseMembers/update.js b/src/routes/phaseMembers/update.js new file mode 100644 index 00000000..b05aef57 --- /dev/null +++ b/src/routes/phaseMembers/update.js @@ -0,0 +1,91 @@ +import _ from 'lodash'; +import Joi from 'joi'; +import validate from 'express-validation'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import models from '../../models'; +import util from '../../util'; +import { EVENT, RESOURCES, ROUTES } from '../../constants'; + +/** + * API to update a project phase members. + */ +const permissions = tcMiddleware.permissions; + +const updatePhaseMemberValidations = { + body: Joi.object().keys({ + userIds: Joi.array().items(Joi.number().integer()).required(), + }), + params: { + projectId: Joi.number().integer().positive().required(), + phaseId: Joi.number().integer().positive().required(), + }, +}; + +module.exports = [ + // handles request validations + validate(updatePhaseMemberValidations), + permissions('phaseMember.update'), + async (req, res, next) => { + const projectId = _.parseInt(req.params.projectId); + const phaseId = _.parseInt(req.params.phaseId); + const createdBy = _.parseInt(req.authUser.userId); + const updatedBy = _.parseInt(req.authUser.userId); + const newPhaseMembers = req.body.userIds; + const transaction = await models.sequelize.transaction(); + try { + // chekc if project and phase exist + const phase = await models.ProjectPhase.findOne({ + where: { + id: phaseId, + projectId, + deletedAt: { $eq: null }, + }, + raw: true, + }); + if (!phase) { + const err = new Error('No active project phase found for project id ' + + `${projectId} and phase id ${phaseId}`); + err.status = 404; + throw (err); + } + const projectMembers = _.map(await models.ProjectMember.getActiveProjectMembers(projectId), 'userId'); + const notProjectMembers = _.difference(newPhaseMembers, projectMembers); + if (notProjectMembers.length > 0) { + const err = new Error(`Members with id: ${notProjectMembers} are not members of project ${projectId}`); + err.status = 404; + throw (err); + } + const phaseMembers = await models.ProjectPhaseMember.getPhaseMembers(phaseId); + const existentPhaseMembers = _.map(phaseMembers, 'userId'); + let updatedPhaseMembers = _.cloneDeep(phaseMembers); + const updatedPhase = _.cloneDeep(phase); + const membersToAdd = _.difference(newPhaseMembers, existentPhaseMembers); + const membersToRemove = _.differenceBy(existentPhaseMembers, newPhaseMembers); + if (membersToRemove.length > 0) { + await models.ProjectPhaseMember.destroy({ where: { phaseId, userId: membersToRemove }, transaction }); + updatedPhaseMembers = _.filter(updatedPhaseMembers, row => !_.includes(membersToRemove, row.userId)); + } + if (membersToAdd.length > 0) { + const createData = _.map(membersToAdd, userId => ({ phaseId, userId, createdBy, updatedBy })); + const result = await models.ProjectPhaseMember.bulkCreate(createData, { transaction }); + updatedPhaseMembers.push(..._.map(result, item => item.toJSON())); + } + req.log.debug('updated phase members', JSON.stringify(newPhaseMembers, null, 2)); + // emit event + if (membersToRemove.length > 0 || membersToAdd.length > 0) { + util.sendResourceToKafkaBus( + req, + EVENT.ROUTING_KEY.PROJECT_PHASE_UPDATED, + RESOURCES.PHASE, + _.assign(updatedPhase, { members: updatedPhaseMembers }), + _.assign(phase, { members: phaseMembers }), + ROUTES.PHASES.UPDATE); + } + await transaction.commit(); + res.json(updatedPhaseMembers); + } catch (err) { + await transaction.rollback(); + next(err); + } + }, +]; diff --git a/src/routes/phaseMembers/update.spec.js b/src/routes/phaseMembers/update.spec.js new file mode 100644 index 00000000..88bb462c --- /dev/null +++ b/src/routes/phaseMembers/update.spec.js @@ -0,0 +1,178 @@ +/** + * Tests for update.js + */ +import _ from 'lodash'; +import config from 'config'; +import request from 'supertest'; +import chai from 'chai'; +import util from '../../util'; +import models from '../../models'; +import server from '../../app'; +import testUtil from '../../tests/util'; + +const should = chai.should(); + +const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName'); +const ES_PROJECT_TYPE = config.get('elasticsearchConfig.docType'); +const eClient = util.getElasticSearchClient(); + +describe('Update phase members', () => { + let id; + let project; + let phaseId; + const copilotUser = { + handle: testUtil.getDecodedToken(testUtil.jwts.copilot).handle, + userId: testUtil.getDecodedToken(testUtil.jwts.copilot).userId, + firstName: 'fname', + lastName: 'lName', + email: 'some@abc.com', + }; + const memberUser = { + handle: testUtil.getDecodedToken(testUtil.jwts.member).handle, + userId: testUtil.getDecodedToken(testUtil.jwts.member).userId, + firstName: 'fname', + lastName: 'lName', + email: 'some@abc.com', + }; + before(function beforeHook(done) { + this.timeout(20000); + // mocks + testUtil.clearDb() + .then(() => testUtil.clearES()) + .then(() => { + models.Project.create({ + type: 'generic', + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', + }).then((p) => { + id = p.id; + project = p.toJSON(); + // create members + models.ProjectMember.bulkCreate([{ + id: 1, + userId: copilotUser.userId, + projectId: id, + role: 'copilot', + isPrimary: false, + createdBy: 1, + updatedBy: 1, + }, { + id: 2, + userId: memberUser.userId, + projectId: id, + role: 'customer', + isPrimary: true, + createdBy: 1, + updatedBy: 1, + }]).then(() => { + models.ProjectPhase.create({ + name: 'test project phase', + projectId: id, + 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', + }, + createdBy: 1, + updatedBy: 1, + }).then((ph) => { + const phase = ph.toJSON(); + phaseId = phase.id; + models.ProjectPhaseMember.create({ + phaseId, + userId: copilotUser.userId, + }).then((phaseMember) => { + _.assign(phase, { members: [phaseMember.toJSON()] }); + // Index to ES + // Overwrite lastActivityAt as otherwise ES fill not be able to parse it + project.lastActivityAt = 1; + project.phases = [phase]; + return eClient.index({ + index: ES_PROJECT_INDEX, + type: ES_PROJECT_TYPE, + id, + body: project, + }).then(() => { + done(); + }); + }); + }); + }); + }); + }); + }); + + after((done) => { + testUtil.clearDb(done); + }); + describe('POST /projects/{projectId}/phases/{phaseId}/members', () => { + it('should return 403 for anonymous user', (done) => { + request(server) + .post(`/v5/projects/${id}/phases/${phaseId}/members`) + .send({ memberIds: [copilotUser.userId, memberUser.userId] }) + .expect(403, done); + }); + + it('should return 403 for regular user', (done) => { + request(server) + .post(`/v5/projects/${id}/phases/${phaseId}/members`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send({ memberIds: [copilotUser.userId, memberUser.userId] }) + .expect(403, done); + }); + + it('should return 200 for connect admin', (done) => { + request(server) + .post(`/v5/projects/${id}/phases/${phaseId}/members`) + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .send({ memberIds: [copilotUser.userId, memberUser.userId] }) + .expect(200) + .end((err, res) => { + const resJson = res.body; + should.exist(resJson); + resJson.should.have.length(2); + done(); + }); + }); + + it('should return 200 for project admin', (done) => { + request(server) + .post(`/v5/projects/${id}/phases/${phaseId}/members`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send({ memberIds: [] }) + .expect(200) + .end((err, res) => { + const resJson = res.body; + should.exist(resJson); + resJson.should.have.length(0); + done(); + }); + }); + + it('should return 403 for copilot', (done) => { + request(server) + .post(`/v5/projects/${id}/phases/${phaseId}/members`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ memberIds: [copilotUser.userId, memberUser.userId] }) + .expect(403, done); + }); + }); +}); diff --git a/src/routes/phases/create.spec.js b/src/routes/phases/create.spec.js index 983ced88..68200ff1 100644 --- a/src/routes/phases/create.spec.js +++ b/src/routes/phases/create.spec.js @@ -293,7 +293,6 @@ describe('Project Phases', () => { done(err); } else { const resJson = res.body; - console.log(resJson); validatePhase(resJson, body); resJson.products.should.have.length(1); diff --git a/src/routes/phases/get.js b/src/routes/phases/get.js index 12c1bd3e..64c34983 100644 --- a/src/routes/phases/get.js +++ b/src/routes/phases/get.js @@ -5,7 +5,15 @@ import util from '../../util'; import models from '../../models'; const permissions = tcMiddleware.permissions; - +const populateMemberDetails = async (phase, logger, id) => { + if (phase.members && phase.members.length > 0) { + const details = await util.getMemberDetailsByUserIds(_.map(phase.members, 'userId'), logger, id); + _.forEach(phase.members, (member) => { + _.assign(member, _.find(details, detail => detail.userId === member.userId)); + }); + } + return phase; +}; module.exports = [ permissions('project.view'), (req, res, next) => { @@ -39,7 +47,10 @@ module.exports = [ return models.ProjectPhase .findOne({ where: { id: phaseId, projectId }, - raw: true, + include: [{ + model: models.ProjectPhaseMember, + as: 'members', + }], }) .then((phase) => { if (!phase) { @@ -49,12 +60,15 @@ module.exports = [ err.status = 404; throw err; } - res.json(phase); + return populateMemberDetails(phase.toJSON(), req.log, req.id) + .then(result => res.json(result)); }) .catch(err => next(err)); } req.log.debug('phase found in ES'); - return res.json(data[0].inner_hits.phases.hits.hits[0]._source); // eslint-disable-line no-underscore-dangle + // eslint-disable-next-line no-underscore-dangle + return populateMemberDetails(data[0].inner_hits.phases.hits.hits[0]._source, req.log, req.id) + .then(phase => res.json(phase)); }) .catch(next); }, diff --git a/src/routes/phases/list.js b/src/routes/phases/list.js index cd6aaabf..c7c80d75 100644 --- a/src/routes/phases/list.js +++ b/src/routes/phases/list.js @@ -4,6 +4,7 @@ import config from 'config'; import { middleware as tcMiddleware } from 'tc-core-library-js'; import util from '../../util'; import models from '../../models'; +import { ADMIN_ROLES } from '../../constants'; const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName'); const ES_PROJECT_TYPE = config.get('elasticsearchConfig.docType'); @@ -14,6 +15,18 @@ const PHASE_ATTRIBUTES = _.keys(models.ProjectPhase.rawAttributes); const permissions = tcMiddleware.permissions; +const populateMemberDetails = async (phases, logger, id) => { + const userIds = _.reduce(phases, (acc, phase) => _.concat(acc, _.map(phase.members, 'userId')), []); + if (userIds.length > 0) { + const details = await util.getMemberDetailsByUserIds(userIds, logger, id); + _.forEach(phases, (phase) => { + _.forEach(phase.members, (member) => { + _.assign(member, _.find(details, detail => detail.userId === member.userId)); + }); + }); + } + return phases; +}; module.exports = [ permissions('project.view'), (req, res, next) => { @@ -22,6 +35,8 @@ module.exports = [ // Parse the fields string to determine what fields are to be returned let fields = req.query.fields ? decodeURIComponent(req.query.fields).split(',') : PHASE_ATTRIBUTES; let sort = req.query.sort ? decodeURIComponent(req.query.sort) : 'startDate'; + const memberOnly = req.query.memberOnly ? req.query.memberOnly.toLowerCase() === 'true' : false; + const isAdmin = util.hasRoles(req, ADMIN_ROLES); if (sort && sort.indexOf(' ') === -1) { sort += ' asc'; } @@ -43,32 +58,45 @@ module.exports = [ // Get the phases let phases = _.isArray(doc._source.phases) ? doc._source.phases : []; // eslint-disable-line no-underscore-dangle + if (memberOnly && !isAdmin) { + phases = _.filter(phases, phase => _.includes(_.map(_.get(phase, 'members'), 'userId')), req.authUser.userId); + } // Sort phases = _.orderBy(phases, [sortColumnAndOrder[0]], [sortColumnAndOrder[1]]); - fields = _.intersection(fields, [...PHASE_ATTRIBUTES, 'products']); + fields = _.intersection(fields, [...PHASE_ATTRIBUTES, 'products', 'members']); if (_.indexOf(fields, 'id') < 0) { fields.push('id'); } phases = _.map(phases, phase => _.pick(phase, fields)); - - res.json(phases); + return populateMemberDetails(phases, req.log, req.id) + .then(result => res.json(result)); }) .catch((err) => { if (err.status === 404) { req.log.debug('No phases found in ES'); + const include = { + model: models.ProjectPhase, + as: 'phases', + order: [['startDate', 'asc']], + include: [], + }; + if (_.indexOf(fields, 'products') >= 0) { + include.include.push({ + model: models.PhaseProduct, + as: 'products', + }); + } + if (_.indexOf(fields, 'members') >= 0) { + include.include.push({ + model: models.ProjectPhaseMember, + as: 'members', + }); + } // Load the phases return models.Project.findByPk(projectId, { - include: [{ - model: models.ProjectPhase, - as: 'phases', - order: [['startDate', 'asc']], - include: [{ - model: models.PhaseProduct, - as: 'products', - }], - }], + include: [include], }) .then((project) => { if (!project) { @@ -79,17 +107,22 @@ module.exports = [ // Get the phases let phases = _.isArray(project.phases) ? project.phases : []; - + phases = _.map(phases, phase => phase.toJSON()); + if (memberOnly && !isAdmin) { + phases = _.filter(phases, phase => + _.includes(_.map(_.get(phase, 'members'), 'userId')), req.authUser.userId); + } // Sort phases = _.orderBy(phases, [sortColumnAndOrder[0]], [sortColumnAndOrder[1]]); - - fields = _.intersection(fields, [...PHASE_ATTRIBUTES, 'products']); + _.remove(PHASE_ATTRIBUTES, attribute => _.includes(['deletedAt', 'deletedBy'], attribute)); + fields = _.intersection(fields, [...PHASE_ATTRIBUTES, 'products', 'members']); if (_.indexOf(fields, 'id') < 0) { fields.push('id'); } - + phases = _.map(phases, phase => _.pick(phase, fields)); // Write to response - return res.json(_.map(phases, p => _.omit(p.toJSON(), ['deletedAt', 'deletedBy']))); + return populateMemberDetails(phases, req.log, req.id) + .then(result => res.json(result)); }); } return next(err); diff --git a/src/routes/phases/list.spec.js b/src/routes/phases/list.spec.js index 6058643b..80ad7637 100644 --- a/src/routes/phases/list.spec.js +++ b/src/routes/phases/list.spec.js @@ -46,7 +46,7 @@ describe('Project Phases', () => { email: 'some@abc.com', }; before(function beforeHook(done) { - this.timeout(10000); + this.timeout(20000); // mocks testUtil.clearDb() .then(() => testUtil.clearES()) @@ -85,18 +85,26 @@ describe('Project Phases', () => { }]).then(() => { _.assign(body, { projectId }); return models.ProjectPhase.create(body); - }).then((phase) => { - // Index to ES - // Overwrite lastActivityAt as otherwise ES fill not be able to parse it - project.lastActivityAt = 1; - project.phases = [phase]; - return eClient.index({ - index: ES_PROJECT_INDEX, - type: ES_PROJECT_TYPE, - id: projectId, - body: project, - }).then(() => { - done(); + }).then((ph) => { + const phase = ph.toJSON(); + models.ProjectPhaseMember.create({ + phaseId: phase.id, + memberId: 1, + userId: copilotUser.userId, + }).then((phaseMember) => { + _.assign(phase, { members: [phaseMember.toJSON()] }); + // Index to ES + // Overwrite lastActivityAt as otherwise ES fill not be able to parse it + project.lastActivityAt = 1; + project.phases = [phase]; + return eClient.index({ + index: ES_PROJECT_INDEX, + type: ES_PROJECT_TYPE, + id: projectId, + body: project, + }).then(() => { + done(); + }); }); }); }); @@ -171,5 +179,47 @@ describe('Project Phases', () => { } }); }); + + it('should return 1 phase when user have project permission (copilot) with memberOnly', (done) => { + request(server) + .get(`/v5/projects/${projectId}/phases/`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .query({ memberOnly: true }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body; + should.exist(resJson); + resJson.should.have.lengthOf(1); + done(); + } + }); + }); + + it('should return 0 phase when user have project permission (customer) with memberOnly', (done) => { + request(server) + .get(`/v5/projects/${projectId}/phases/`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .query({ memberOnly: true }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body; + should.exist(resJson); + resJson.should.have.lengthOf(0); + done(); + } + }); + }); }); }); From 61363cd7ef77fa65873717a0832b7d34fe58a5a5 Mon Sep 17 00:00:00 2001 From: Ahmad Alkhawaja Date: Sun, 25 Jul 2021 14:10:04 +0300 Subject: [PATCH 03/17] trigger CI Build --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 016e6d83..c48b1a79 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -152,7 +152,7 @@ workflows: - UnitTests filters: branches: - only: ['develop', 'connect-performance-testing', 'feature/shapeup_billing_accounts_protections'] + only: ['develop', 'connect-performance-testing', 'feature/new-milestone-concept'] - deployProd: context : org-global requires: From 3c6f384f4004f044137ab5ce2f75bcde28891b5b Mon Sep 17 00:00:00 2001 From: eisbilir Date: Sun, 25 Jul 2021 20:36:18 +0300 Subject: [PATCH 04/17] fix: phase member test spec --- src/events/projects/index.spec.js | 2 +- src/routes/phaseMembers/delete.spec.js | 4 +++- src/routes/phaseMembers/list.spec.js | 8 +++----- src/routes/phaseMembers/update.spec.js | 12 +++++++----- src/routes/phases/list.js | 4 ++-- src/routes/phases/list.spec.js | 3 ++- 6 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/events/projects/index.spec.js b/src/events/projects/index.spec.js index 0d75ba82..942f549d 100644 --- a/src/events/projects/index.spec.js +++ b/src/events/projects/index.spec.js @@ -25,7 +25,7 @@ describe('projectUpdatedKafkaHandler', () => { initiatorUserId: 2, }; - const mockedApp = {}; + const mockedApp = { logger: console, models }; it('should throw validation exception when payload is empty', async () => { await expect(projectUpdatedKafkaHandler(mockedApp, topic, {})).to.be.rejectedWith(Error); diff --git a/src/routes/phaseMembers/delete.spec.js b/src/routes/phaseMembers/delete.spec.js index 9d48b0ed..46a9ed00 100644 --- a/src/routes/phaseMembers/delete.spec.js +++ b/src/routes/phaseMembers/delete.spec.js @@ -88,6 +88,8 @@ describe('Delete phase member', () => { models.ProjectPhaseMember.create({ phaseId, userId: copilotUser.userId, + createdBy: 1, + updatedBy: 1, }).then((phaseMember) => { _.assign(phase, { members: [phaseMember.toJSON()] }); // Index to ES @@ -112,7 +114,7 @@ describe('Delete phase member', () => { after((done) => { testUtil.clearDb(done); }); - describe('DELETE /projects/{projectId}/phases/{phaseId}/members/{memberId}', () => { + describe('DELETE /projects/{projectId}/phases/{phaseId}/members/{userId}', () => { it('should return 403 for anonymous user', (done) => { request(server) .delete(`/v5/projects/${id}/phases/${phaseId}/members/${copilotUser.userId}`) diff --git a/src/routes/phaseMembers/list.spec.js b/src/routes/phaseMembers/list.spec.js index e9b0a8c0..34e9436c 100644 --- a/src/routes/phaseMembers/list.spec.js +++ b/src/routes/phaseMembers/list.spec.js @@ -20,7 +20,6 @@ describe('List phase members', () => { let id; let project; let phaseId; - let memberId; const copilotUser = { handle: testUtil.getDecodedToken(testUtil.jwts.copilot).handle, userId: testUtil.getDecodedToken(testUtil.jwts.copilot).userId, @@ -57,8 +56,7 @@ describe('List phase members', () => { isPrimary: false, createdBy: 1, updatedBy: 1, - }).then((member) => { - memberId = member.id; + }).then(() => { models.ProjectPhase.create({ name: 'test project phase', projectId: id, @@ -77,8 +75,9 @@ describe('List phase members', () => { phaseId = phase.id; models.ProjectPhaseMember.create({ phaseId, - memberId, userId: copilotUser.userId, + createdBy: 1, + updatedBy: 1, }).then((phaseMember) => { _.assign(phase, { members: [phaseMember.toJSON()] }); // Index to ES @@ -151,7 +150,6 @@ describe('List phase members', () => { should.exist(resJson); resJson.should.have.length(1); resJson[0].userId.should.be.eql(copilotUser.userId); - resJson[0].memberId.should.be.eql(1); resJson[0].phaseId.should.be.eql(phaseId); done(); }); diff --git a/src/routes/phaseMembers/update.spec.js b/src/routes/phaseMembers/update.spec.js index 88bb462c..a8f21c6f 100644 --- a/src/routes/phaseMembers/update.spec.js +++ b/src/routes/phaseMembers/update.spec.js @@ -91,6 +91,8 @@ describe('Update phase members', () => { models.ProjectPhaseMember.create({ phaseId, userId: copilotUser.userId, + createdBy: 1, + updatedBy: 1, }).then((phaseMember) => { _.assign(phase, { members: [phaseMember.toJSON()] }); // Index to ES @@ -119,7 +121,7 @@ describe('Update phase members', () => { it('should return 403 for anonymous user', (done) => { request(server) .post(`/v5/projects/${id}/phases/${phaseId}/members`) - .send({ memberIds: [copilotUser.userId, memberUser.userId] }) + .send({ userIds: [copilotUser.userId, memberUser.userId] }) .expect(403, done); }); @@ -129,7 +131,7 @@ describe('Update phase members', () => { .set({ Authorization: `Bearer ${testUtil.jwts.member}`, }) - .send({ memberIds: [copilotUser.userId, memberUser.userId] }) + .send({ userIds: [copilotUser.userId, memberUser.userId] }) .expect(403, done); }); @@ -139,7 +141,7 @@ describe('Update phase members', () => { .set({ Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, }) - .send({ memberIds: [copilotUser.userId, memberUser.userId] }) + .send({ userIds: [copilotUser.userId, memberUser.userId] }) .expect(200) .end((err, res) => { const resJson = res.body; @@ -155,7 +157,7 @@ describe('Update phase members', () => { .set({ Authorization: `Bearer ${testUtil.jwts.admin}`, }) - .send({ memberIds: [] }) + .send({ userIds: [] }) .expect(200) .end((err, res) => { const resJson = res.body; @@ -171,7 +173,7 @@ describe('Update phase members', () => { .set({ Authorization: `Bearer ${testUtil.jwts.copilot}`, }) - .send({ memberIds: [copilotUser.userId, memberUser.userId] }) + .send({ userIds: [copilotUser.userId, memberUser.userId] }) .expect(403, done); }); }); diff --git a/src/routes/phases/list.js b/src/routes/phases/list.js index c7c80d75..7fec101c 100644 --- a/src/routes/phases/list.js +++ b/src/routes/phases/list.js @@ -59,7 +59,7 @@ module.exports = [ let phases = _.isArray(doc._source.phases) ? doc._source.phases : []; // eslint-disable-line no-underscore-dangle if (memberOnly && !isAdmin) { - phases = _.filter(phases, phase => _.includes(_.map(_.get(phase, 'members'), 'userId')), req.authUser.userId); + phases = _.filter(phases, phase => _.includes(_.map(_.get(phase, 'members'), 'userId'), req.authUser.userId)); } // Sort phases = _.orderBy(phases, [sortColumnAndOrder[0]], [sortColumnAndOrder[1]]); @@ -110,7 +110,7 @@ module.exports = [ phases = _.map(phases, phase => phase.toJSON()); if (memberOnly && !isAdmin) { phases = _.filter(phases, phase => - _.includes(_.map(_.get(phase, 'members'), 'userId')), req.authUser.userId); + _.includes(_.map(_.get(phase, 'members'), 'userId'), req.authUser.userId)); } // Sort phases = _.orderBy(phases, [sortColumnAndOrder[0]], [sortColumnAndOrder[1]]); diff --git a/src/routes/phases/list.spec.js b/src/routes/phases/list.spec.js index 80ad7637..83e739cb 100644 --- a/src/routes/phases/list.spec.js +++ b/src/routes/phases/list.spec.js @@ -89,8 +89,9 @@ describe('Project Phases', () => { const phase = ph.toJSON(); models.ProjectPhaseMember.create({ phaseId: phase.id, - memberId: 1, userId: copilotUser.userId, + createdBy: 1, + updatedBy: 1, }).then((phaseMember) => { _.assign(phase, { members: [phaseMember.toJSON()] }); // Index to ES From 4f5152cb474fa3d892b53979943e3f760240c6e7 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Wed, 28 Jul 2021 00:34:17 +0300 Subject: [PATCH 05/17] update: phase member details --- src/routes/phases/get.js | 18 +++++++++--------- src/routes/phases/list.js | 25 +++++++++++++------------ 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/routes/phases/get.js b/src/routes/phases/get.js index 64c34983..deb439b8 100644 --- a/src/routes/phases/get.js +++ b/src/routes/phases/get.js @@ -5,14 +5,14 @@ import util from '../../util'; import models from '../../models'; const permissions = tcMiddleware.permissions; -const populateMemberDetails = async (phase, logger, id) => { - if (phase.members && phase.members.length > 0) { - const details = await util.getMemberDetailsByUserIds(_.map(phase.members, 'userId'), logger, id); - _.forEach(phase.members, (member) => { - _.assign(member, _.find(details, detail => detail.userId === member.userId)); - }); +const populateMemberDetails = async (phase, req) => { + const members = _.map(phase.members, member => _.pick(member, 'userId')); + try { + const detailedMembers = await util.getObjectsWithMemberDetails(members, ['userId', 'handle', 'photoURL'], req); + return _.assign(phase, { members: detailedMembers }); + } catch (err) { + return _.assign(phase, { members }); } - return phase; }; module.exports = [ permissions('project.view'), @@ -60,14 +60,14 @@ module.exports = [ err.status = 404; throw err; } - return populateMemberDetails(phase.toJSON(), req.log, req.id) + return populateMemberDetails(phase.toJSON(), req) .then(result => res.json(result)); }) .catch(err => next(err)); } req.log.debug('phase found in ES'); // eslint-disable-next-line no-underscore-dangle - return populateMemberDetails(data[0].inner_hits.phases.hits.hits[0]._source, req.log, req.id) + return populateMemberDetails(data[0].inner_hits.phases.hits.hits[0]._source, req) .then(phase => res.json(phase)); }) .catch(next); diff --git a/src/routes/phases/list.js b/src/routes/phases/list.js index 7fec101c..7b76922e 100644 --- a/src/routes/phases/list.js +++ b/src/routes/phases/list.js @@ -15,17 +15,18 @@ const PHASE_ATTRIBUTES = _.keys(models.ProjectPhase.rawAttributes); const permissions = tcMiddleware.permissions; -const populateMemberDetails = async (phases, logger, id) => { - const userIds = _.reduce(phases, (acc, phase) => _.concat(acc, _.map(phase.members, 'userId')), []); - if (userIds.length > 0) { - const details = await util.getMemberDetailsByUserIds(userIds, logger, id); - _.forEach(phases, (phase) => { - _.forEach(phase.members, (member) => { - _.assign(member, _.find(details, detail => detail.userId === member.userId)); - }); - }); +const populateMemberDetails = async (phases, req) => { + let members = _.reduce(phases, (acc, phase) => + _.concat(acc, _.map(phase.members, member => _.pick(member, 'userId'))), []); + members = _.uniqBy(members, 'userId'); + try { + const detailedMembers = await util.getObjectsWithMemberDetails(members, ['userId', 'handle', 'photoURL'], req); + return _.map(phases, phase => + _.assign(phase, { members: _.intersectionBy(detailedMembers, phase.members, 'userId') })); + } catch (err) { + return _.map(phases, phase => + _.assign(phase, { members: _.map(phase.members, member => _.pick(member, 'userId')) })); } - return phases; }; module.exports = [ permissions('project.view'), @@ -70,7 +71,7 @@ module.exports = [ } phases = _.map(phases, phase => _.pick(phase, fields)); - return populateMemberDetails(phases, req.log, req.id) + return populateMemberDetails(phases, req) .then(result => res.json(result)); }) .catch((err) => { @@ -121,7 +122,7 @@ module.exports = [ } phases = _.map(phases, phase => _.pick(phase, fields)); // Write to response - return populateMemberDetails(phases, req.log, req.id) + return populateMemberDetails(phases, req) .then(result => res.json(result)); }); } From 7f02ec1de91c3f9baa9249796f18e698f82ceca9 Mon Sep 17 00:00:00 2001 From: Ahmad Alkhawaja Date: Wed, 28 Jul 2021 12:01:06 +0300 Subject: [PATCH 06/17] updated script to include sequence creation and removed 'public' schema --- migrations/20210718_project_phase_member_table.sql | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/migrations/20210718_project_phase_member_table.sql b/migrations/20210718_project_phase_member_table.sql index ac27ad5f..6b422a59 100644 --- a/migrations/20210718_project_phase_member_table.sql +++ b/migrations/20210718_project_phase_member_table.sql @@ -1,4 +1,11 @@ -CREATE TABLE "public"."project_phase_member" ( +CREATE SEQUENCE project_phase_member_id_seq + INCREMENT 1 + START 1 + MINVALUE 1 + MAXVALUE 9223372036854775807 + CACHE 1; + +CREATE TABLE "project_phase_member" ( "id" int8 NOT NULL DEFAULT nextval('project_phase_member_id_seq'::regclass), "userId" int8 NOT NULL, "deletedAt" timestamptz, @@ -8,6 +15,6 @@ CREATE TABLE "public"."project_phase_member" ( "createdBy" int4 NOT NULL, "updatedBy" int4 NOT NULL, "phaseId" int8, - CONSTRAINT "project_phase_member_phaseId_fkey" FOREIGN KEY ("phaseId") REFERENCES "public"."project_phases"("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "project_phase_member_phaseId_fkey" FOREIGN KEY ("phaseId") REFERENCES "project_phases"("id") ON DELETE SET NULL ON UPDATE CASCADE, PRIMARY KEY ("id") ); \ No newline at end of file From 72d79bde791b5c89d1042ad800238dce5b740d7a Mon Sep 17 00:00:00 2001 From: eisbilir Date: Thu, 29 Jul 2021 15:59:23 +0300 Subject: [PATCH 07/17] update: phase update will return members --- src/routes/phases/update.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/routes/phases/update.js b/src/routes/phases/update.js index 9f4c417a..698f2701 100644 --- a/src/routes/phases/update.js +++ b/src/routes/phases/update.js @@ -26,7 +26,15 @@ const updateProjectPhaseValidation = { order: Joi.number().integer().optional(), }).required(), }; - +const populateMemberDetails = async (phase, req) => { + const members = _.map(phase.members, member => _.pick(member, 'userId')); + try { + const detailedMembers = await util.getObjectsWithMemberDetails(members, ['userId', 'handle', 'photoURL'], req); + return _.assign(phase, { members: detailedMembers }); + } catch (err) { + return _.assign(phase, { members }); + } +}; module.exports = [ // validate request payload @@ -102,8 +110,14 @@ module.exports = [ updatedValue, previousValue, ROUTES.PHASES.UPDATE); - - res.json(updated); + return models.ProjectPhase.findOne({ + where: { id: phaseId, projectId }, + include: [{ + model: models.ProjectPhaseMember, + as: 'members', + }], + }).then(phaseWithMembers => populateMemberDetails(phaseWithMembers.toJSON(), req) + .then(result => res.json(result))); }) .catch(err => next(err)); }, From fc7c959b37fcb903871e025328ff19d7882a2b59 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Thu, 29 Jul 2021 16:46:36 +0300 Subject: [PATCH 08/17] update: use 100 for maxPhaseProductCount --- config/default.json | 2 +- src/routes/projects/create.spec.js | 32 ++++++++++++++---------------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/config/default.json b/config/default.json index 9cf176ae..3fdbaa81 100644 --- a/config/default.json +++ b/config/default.json @@ -45,7 +45,7 @@ "messageApiUrl": "http://api.topcoder-dev.com/v5", "busApiToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoicHJvamVjdC1zZXJ2aWNlIiwiaWF0IjoxNTEyNzQ3MDgyLCJleHAiOjE1MjEzODcwODJ9.PHuNcFDaotGAL8RhQXQMdpL8yOKXxjB5DbBIodmt7RE", "HEALTH_CHECK_URL": "_health", - "maxPhaseProductCount": 1, + "maxPhaseProductCount": 100, "TOKEN_CACHE_TIME": "86000", "whitelistedOriginsForUserIdAuth": "[\"https:\/\/topcoder-newauth.auth0.com\/\",\"https:\/\/api.topcoder-dev.com\"]", "EMAIL_INVITE_FROM_NAME": "Topcoder", diff --git a/src/routes/projects/create.spec.js b/src/routes/projects/create.spec.js index 7dc8d551..d09acfcd 100644 --- a/src/routes/projects/create.spec.js +++ b/src/routes/projects/create.spec.js @@ -16,7 +16,8 @@ const should = chai.should(); const expect = chai.expect; describe('Project create', () => { - before((done) => { + before(function beforeHook(done) { + this.timeout(20000); testUtil.clearDb() .then(() => testUtil.clearES()) .then(() => models.ProjectType.bulkCreate([ @@ -76,8 +77,16 @@ describe('Project create', () => { updatedBy: 4, }, ])) - .then(() => models.ProjectTemplate.bulkCreate([ - { + .then(() => { + const exceededProducts = []; + for (let i = 1; i <= _.parseInt(config.get('maxPhaseProductCount')) + 1; i += 1) { + exceededProducts.push({ + id: i, + name: `product ${i}`, + productKey: `visual_design_prod${i}`, + }); + } + return models.ProjectTemplate.bulkCreate([{ id: 1, name: 'template 1', key: 'key 1', @@ -91,18 +100,7 @@ describe('Project create', () => { phase1: { name: 'phase 1', duration: 5, - products: [ - { - id: 21, - name: 'product 1', - productKey: 'visual_design_prod1', - }, - { - id: 22, - name: 'product 2', - productKey: 'visual_design_prod2', - }, - ], + products: exceededProducts, }, }, createdBy: 1, @@ -206,8 +204,8 @@ describe('Project create', () => { }, createdBy: 1, updatedBy: 2, - }, - ])) + }]); + }) .then(() => models.BuildingBlock.bulkCreate([ { id: 1, From 3fe176e8b146590924cffc1cd86520d500426546 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Fri, 30 Jul 2021 00:38:49 +0300 Subject: [PATCH 09/17] allow update phase members with create-update phase --- docs/Project API.postman_collection.json | 283 ++++++++++++++++++++++- src/permissions/index.js | 4 +- src/routes/phaseMembers/delete.spec.js | 2 +- src/routes/phaseMembers/update.js | 31 +-- src/routes/phaseMembers/updateService.js | 46 ++++ src/routes/phases/create.js | 21 +- src/routes/phases/create.spec.js | 38 +++ src/routes/phases/get.js | 14 +- src/routes/phases/list.js | 23 +- src/routes/phases/update.js | 33 +-- src/routes/phases/update.spec.js | 68 ++++++ src/util.js | 32 +++ 12 files changed, 520 insertions(+), 75 deletions(-) create mode 100644 src/routes/phaseMembers/updateService.js diff --git a/docs/Project API.postman_collection.json b/docs/Project API.postman_collection.json index 0146caa6..1b7583f4 100644 --- a/docs/Project API.postman_collection.json +++ b/docs/Project API.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "3eba12ae-a066-4d5a-bdd5-3121377e476b", + "_postman_id": "4c51e04b-42d3-4c9f-bf08-af02f51f7756", "name": "Project API", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -4577,6 +4577,208 @@ { "name": "Project Phase", "item": [ + { + "name": "Before Start", + "item": [ + { + "name": "Create project type", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 201\", function () {", + " pm.response.to.have.status(201);", + " if(pm.response.status === \"Created\") {", + " const response = pm.response.json()", + " pm.environment.set(\"projectTypeId\", response.key);", + " }", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"key\": \"new key\",\r\n \"displayName\": \"new displayName\",\r\n \"icon\": \"http://example.com/icon4.ico\",\r\n \t\"question\": \"question 4\",\r\n \t\"info\": \"info 4\",\r\n \t\"aliases\": [\"key-41\", \"key_42\"],\r\n \t\"metadata\": {}\r\n }" + }, + "url": { + "raw": "{{api-url}}/projects/metadata/projectTypes", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "metadata", + "projectTypes" + ] + } + }, + "response": [] + }, + { + "name": "Create project", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 201\", function () {", + " pm.response.to.have.status(201);", + " if(pm.response.status === \"Created\") {", + " const response = pm.response.json()", + " pm.environment.set(\"projectId\", response.id);", + " }", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"name\": \"test project\",\n\t\"description\": \"Hello I am a test project\",\n\t\"type\": \"{{projectTypeId}}\"\n}" + }, + "url": { + "raw": "{{api-url}}/projects", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects" + ] + }, + "description": "Valid request body. Project should be created successfully." + }, + "response": [] + }, + { + "name": "Create project member - 1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 201\", function () {", + " pm.response.to.have.status(201);", + " if(pm.response.status === \"Created\") {", + " const response = pm.response.json()", + " pm.environment.set(\"phaseMemberId-1\", response.userId);", + " }", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"userId\": \"40158994\",\n \"role\": \"copilot\"\n}" + }, + "url": { + "raw": "{{api-url}}/projects/{{projectId}}/members", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "{{projectId}}", + "members" + ] + }, + "description": "If the request payload is valid, than project member should be created." + }, + "response": [] + }, + { + "name": "Create project member - 2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 201\", function () {", + " pm.response.to.have.status(201);", + " if(pm.response.status === \"Created\") {", + " const response = pm.response.json()", + " pm.environment.set(\"phaseMemberId-2\", response.userId);", + " }", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"userId\": \"40153800\",\n \"role\": \"copilot\"\n}" + }, + "url": { + "raw": "{{api-url}}/projects/{{projectId}}/members", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "{{projectId}}", + "members" + ] + }, + "description": "If the request payload is valid, than project member should be created." + }, + "response": [] + } + ] + }, { "name": "Create Phase", "event": [ @@ -4715,6 +4917,52 @@ }, "response": [] }, + { + "name": "Create Phase with members", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 201\", function () {", + " pm.response.to.have.status(201);", + " pm.environment.set(\"phaseId\", pm.response.json().id);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"name\": \"test project phase\",\n\t\"status\": \"active\",\n\t\"startDate\": \"2018-05-15T00:00:00\",\n\t\"endDate\": \"2018-05-16T00:00:00\",\n\t\"budget\": 20,\n\t\"details\": {\n\t\t\"aDetails\": \"a details\"\n\t},\n \"members\": [{{phaseMemberId-1}},{{phaseMemberId-2}}]\n}" + }, + "url": { + "raw": "{{api-url}}/projects/{{projectId}}/phases", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "{{projectId}}", + "phases" + ] + } + }, + "response": [] + }, { "name": "List Phase", "request": { @@ -5008,6 +5256,39 @@ }, "response": [] }, + { + "name": "Update Phase with members", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"name\": \"test project phase xxx\",\n\t\"status\": \"inactive\",\n\t\"startDate\": \"2018-05-14T00:00:00\",\n\t\"endDate\": \"2018-05-15T00:00:00\",\n\t\"budget\": 30,\n\t\"progress\": 15,\n\t\"details\": {\n\t\t\"message\": \"phase details\"\n\t},\n \"members\": [{{phaseMemberId-1}},{{phaseMemberId-2}}]\n}" + }, + "url": { + "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "{{projectId}}", + "phases", + "{{phaseId}}" + ] + } + }, + "response": [] + }, { "name": "Delete Phase", "request": { diff --git a/src/permissions/index.js b/src/permissions/index.js index 0abd4c7b..8dc1f2ff 100644 --- a/src/permissions/index.js +++ b/src/permissions/index.js @@ -98,8 +98,8 @@ module.exports = () => { Authorizer.setPolicy('project.updatePhaseProduct', copilotAndAbove); Authorizer.setPolicy('project.deletePhaseProduct', copilotAndAbove); - Authorizer.setPolicy('phaseMember.update', projectAdmin); - Authorizer.setPolicy('phaseMember.delete', projectAdmin); + Authorizer.setPolicy('phaseMember.update', copilotAndAbove); + Authorizer.setPolicy('phaseMember.delete', copilotAndAbove); Authorizer.setPolicy('phaseMember.view', generalPermission(PERMISSION.READ_PROJECT_MEMBER)); Authorizer.setPolicy('milestoneTemplate.clone', projectAdmin); diff --git a/src/routes/phaseMembers/delete.spec.js b/src/routes/phaseMembers/delete.spec.js index 46a9ed00..72401df8 100644 --- a/src/routes/phaseMembers/delete.spec.js +++ b/src/routes/phaseMembers/delete.spec.js @@ -130,7 +130,7 @@ describe('Delete phase member', () => { .expect(403, done); }); - it('should return 200 for connect admin', (done) => { + it('should return 204 for connect admin', (done) => { request(server) .delete(`/v5/projects/${id}/phases/${phaseId}/members/${copilotUser.userId}`) .set({ diff --git a/src/routes/phaseMembers/update.js b/src/routes/phaseMembers/update.js index b05aef57..64c55e54 100644 --- a/src/routes/phaseMembers/update.js +++ b/src/routes/phaseMembers/update.js @@ -5,6 +5,7 @@ import { middleware as tcMiddleware } from 'tc-core-library-js'; import models from '../../models'; import util from '../../util'; import { EVENT, RESOURCES, ROUTES } from '../../constants'; +import updateService from './updateService'; /** * API to update a project phase members. @@ -28,10 +29,7 @@ module.exports = [ async (req, res, next) => { const projectId = _.parseInt(req.params.projectId); const phaseId = _.parseInt(req.params.phaseId); - const createdBy = _.parseInt(req.authUser.userId); - const updatedBy = _.parseInt(req.authUser.userId); const newPhaseMembers = req.body.userIds; - const transaction = await models.sequelize.transaction(); try { // chekc if project and phase exist const phase = await models.ProjectPhase.findOne({ @@ -48,31 +46,12 @@ module.exports = [ err.status = 404; throw (err); } - const projectMembers = _.map(await models.ProjectMember.getActiveProjectMembers(projectId), 'userId'); - const notProjectMembers = _.difference(newPhaseMembers, projectMembers); - if (notProjectMembers.length > 0) { - const err = new Error(`Members with id: ${notProjectMembers} are not members of project ${projectId}`); - err.status = 404; - throw (err); - } const phaseMembers = await models.ProjectPhaseMember.getPhaseMembers(phaseId); - const existentPhaseMembers = _.map(phaseMembers, 'userId'); - let updatedPhaseMembers = _.cloneDeep(phaseMembers); - const updatedPhase = _.cloneDeep(phase); - const membersToAdd = _.difference(newPhaseMembers, existentPhaseMembers); - const membersToRemove = _.differenceBy(existentPhaseMembers, newPhaseMembers); - if (membersToRemove.length > 0) { - await models.ProjectPhaseMember.destroy({ where: { phaseId, userId: membersToRemove }, transaction }); - updatedPhaseMembers = _.filter(updatedPhaseMembers, row => !_.includes(membersToRemove, row.userId)); - } - if (membersToAdd.length > 0) { - const createData = _.map(membersToAdd, userId => ({ phaseId, userId, createdBy, updatedBy })); - const result = await models.ProjectPhaseMember.bulkCreate(createData, { transaction }); - updatedPhaseMembers.push(..._.map(result, item => item.toJSON())); - } + const updatedPhaseMembers = await updateService(req.authUser, projectId, phaseId, newPhaseMembers); req.log.debug('updated phase members', JSON.stringify(newPhaseMembers, null, 2)); + const updatedPhase = _.cloneDeep(phase); // emit event - if (membersToRemove.length > 0 || membersToAdd.length > 0) { + if (_.intersectionBy(phaseMembers, updatedPhaseMembers, 'id').length !== updatedPhaseMembers.length) { util.sendResourceToKafkaBus( req, EVENT.ROUTING_KEY.PROJECT_PHASE_UPDATED, @@ -81,10 +60,8 @@ module.exports = [ _.assign(phase, { members: phaseMembers }), ROUTES.PHASES.UPDATE); } - await transaction.commit(); res.json(updatedPhaseMembers); } catch (err) { - await transaction.rollback(); next(err); } }, diff --git a/src/routes/phaseMembers/updateService.js b/src/routes/phaseMembers/updateService.js new file mode 100644 index 00000000..8a474ae0 --- /dev/null +++ b/src/routes/phaseMembers/updateService.js @@ -0,0 +1,46 @@ +import _ from 'lodash'; +import models from '../../models'; + +/** + * Update phase members + * @param {Object} currentUser the user who perform this operation + * @param {String} projectId the project id + * @param {String} phaseId the phase id + * @param {Array} newPhaseMembers the array of userIds + * @returns {Array} the array of updated phase member objects + */ +async function update(currentUser, projectId, phaseId, newPhaseMembers) { + const createdBy = _.parseInt(currentUser.userId); + const updatedBy = _.parseInt(currentUser.userId); + const newMembers = _.uniq(newPhaseMembers); + const transaction = await models.sequelize.transaction(); + try { + const projectMembers = _.map(await models.ProjectMember.getActiveProjectMembers(projectId), 'userId'); + const notProjectMembers = _.difference(newMembers, projectMembers); + if (notProjectMembers.length > 0) { + const err = new Error(`Members with id: ${notProjectMembers} are not members of project ${projectId}`); + err.status = 400; + throw (err); + } + let phaseMembers = await models.ProjectPhaseMember.getPhaseMembers(phaseId); + const existentPhaseMembers = _.map(phaseMembers, 'userId'); + const membersToAdd = _.difference(newMembers, existentPhaseMembers); + const membersToRemove = _.differenceBy(existentPhaseMembers, newMembers); + if (membersToRemove.length > 0) { + await models.ProjectPhaseMember.destroy({ where: { phaseId, userId: membersToRemove }, transaction }); + phaseMembers = _.filter(phaseMembers, row => !_.includes(membersToRemove, row.userId)); + } + if (membersToAdd.length > 0) { + const createData = _.map(membersToAdd, userId => ({ phaseId, userId, createdBy, updatedBy })); + const result = await models.ProjectPhaseMember.bulkCreate(createData, { transaction }); + phaseMembers.push(..._.map(result, item => item.toJSON())); + } + await transaction.commit(); + return phaseMembers; + } catch (err) { + await transaction.rollback(); + throw err; + } +} + +export default update; diff --git a/src/routes/phases/create.js b/src/routes/phases/create.js index a6ff8bef..7a0636c6 100644 --- a/src/routes/phases/create.js +++ b/src/routes/phases/create.js @@ -6,6 +6,8 @@ import models from '../../models'; import util from '../../util'; import { EVENT, RESOURCES } from '../../constants'; +import updatePhaseMemberService from '../phaseMembers/updateService'; + const permissions = require('tc-core-library-js').middleware.permissions; @@ -24,6 +26,7 @@ const addProjectPhaseValidations = { details: Joi.any().optional(), order: Joi.number().integer().optional(), productTemplateId: Joi.number().integer().positive().optional(), + members: Joi.array().items(Joi.number().integer()).optional(), }).required(), }; @@ -61,7 +64,7 @@ module.exports = [ throw err; } return models.ProjectPhase - .create(data) + .create(_.omit(data, 'members')) .then((_newProjectPhase) => { newProjectPhase = _.cloneDeep(_newProjectPhase); req.log.debug('new project phase created (id# %d, name: %s)', @@ -102,6 +105,15 @@ module.exports = [ ]; }); }); + }) + // create phase members if `members` is defined + .then(() => { + if (_.isNil(data.members) || _.isEmpty(data.members)) { + return Promise.resolve(); + } + + return updatePhaseMemberService(req.authUser, projectId, newProjectPhase.id, data.members) + .then(members => _.assign(newProjectPhase, { members })); }); }) .then(() => { @@ -110,10 +122,13 @@ module.exports = [ EVENT.ROUTING_KEY.PROJECT_PHASE_ADDED, RESOURCES.PHASE, newProjectPhase); - - res.status(201).json(newProjectPhase); + return util.populatePhasesWithMemberDetails(newProjectPhase, req) + .then(phase => res.status(201).json(phase)); }) .catch((err) => { + if (err.message) { + _.assign(err, { details: err.message }); + } util.handleError('Error creating project phase', err, req, next); }); }, diff --git a/src/routes/phases/create.spec.js b/src/routes/phases/create.spec.js index 68200ff1..40d9650b 100644 --- a/src/routes/phases/create.spec.js +++ b/src/routes/phases/create.spec.js @@ -369,6 +369,44 @@ describe('Project Phases', () => { }); }); + it('should return 201 with member details if payload has members property', (done) => { + const bodyWithMembers = _.cloneDeep(body); + _.assign(bodyWithMembers, { members: [copilotUser.userId] }); + request(server) + .post(`/v5/projects/${projectId}/phases/`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(bodyWithMembers) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body; + validatePhase(resJson, bodyWithMembers); + resJson.members.should.have.length(1); + resJson.members[0].userId.should.eql(copilotUser.userId); + done(); + } + }); + }); + + it('should return 400 if members property includes userId who is not a member of project', (done) => { + const bodyWithMembers = _.cloneDeep(body); + _.assign(bodyWithMembers, { members: [999] }); + request(server) + .post(`/v5/projects/${projectId}/phases/`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(bodyWithMembers) + .expect('Content-Type', /json/) + .expect(400) + .end(done); + }); + describe('Bus api', () => { let createEventSpy; const sandbox = sinon.sandbox.create(); diff --git a/src/routes/phases/get.js b/src/routes/phases/get.js index deb439b8..5d329363 100644 --- a/src/routes/phases/get.js +++ b/src/routes/phases/get.js @@ -5,15 +5,7 @@ import util from '../../util'; import models from '../../models'; const permissions = tcMiddleware.permissions; -const populateMemberDetails = async (phase, req) => { - const members = _.map(phase.members, member => _.pick(member, 'userId')); - try { - const detailedMembers = await util.getObjectsWithMemberDetails(members, ['userId', 'handle', 'photoURL'], req); - return _.assign(phase, { members: detailedMembers }); - } catch (err) { - return _.assign(phase, { members }); - } -}; + module.exports = [ permissions('project.view'), (req, res, next) => { @@ -60,14 +52,14 @@ module.exports = [ err.status = 404; throw err; } - return populateMemberDetails(phase.toJSON(), req) + return util.populatePhasesWithMemberDetails(phase.toJSON(), req) .then(result => res.json(result)); }) .catch(err => next(err)); } req.log.debug('phase found in ES'); // eslint-disable-next-line no-underscore-dangle - return populateMemberDetails(data[0].inner_hits.phases.hits.hits[0]._source, req) + return util.populatePhasesWithMemberDetails(data[0].inner_hits.phases.hits.hits[0]._source, req) .then(phase => res.json(phase)); }) .catch(next); diff --git a/src/routes/phases/list.js b/src/routes/phases/list.js index 7b76922e..cb69e8ee 100644 --- a/src/routes/phases/list.js +++ b/src/routes/phases/list.js @@ -15,19 +15,6 @@ const PHASE_ATTRIBUTES = _.keys(models.ProjectPhase.rawAttributes); const permissions = tcMiddleware.permissions; -const populateMemberDetails = async (phases, req) => { - let members = _.reduce(phases, (acc, phase) => - _.concat(acc, _.map(phase.members, member => _.pick(member, 'userId'))), []); - members = _.uniqBy(members, 'userId'); - try { - const detailedMembers = await util.getObjectsWithMemberDetails(members, ['userId', 'handle', 'photoURL'], req); - return _.map(phases, phase => - _.assign(phase, { members: _.intersectionBy(detailedMembers, phase.members, 'userId') })); - } catch (err) { - return _.map(phases, phase => - _.assign(phase, { members: _.map(phase.members, member => _.pick(member, 'userId')) })); - } -}; module.exports = [ permissions('project.view'), (req, res, next) => { @@ -71,7 +58,10 @@ module.exports = [ } phases = _.map(phases, phase => _.pick(phase, fields)); - return populateMemberDetails(phases, req) + if (_.indexOf(fields, 'members') < 0) { + return res.json(phases); + } + return util.populatePhasesWithMemberDetails(phases, req) .then(result => res.json(result)); }) .catch((err) => { @@ -122,7 +112,10 @@ module.exports = [ } phases = _.map(phases, phase => _.pick(phase, fields)); // Write to response - return populateMemberDetails(phases, req) + if (_.indexOf(fields, 'members') < 0) { + return res.json(phases); + } + return util.populatePhasesWithMemberDetails(phases, req) .then(result => res.json(result)); }); } diff --git a/src/routes/phases/update.js b/src/routes/phases/update.js index 698f2701..4707ba9a 100644 --- a/src/routes/phases/update.js +++ b/src/routes/phases/update.js @@ -7,6 +7,7 @@ import models from '../../models'; import util from '../../util'; import { EVENT, RESOURCES, ROUTES } from '../../constants'; +import updatePhaseMemberService from '../phaseMembers/updateService'; const permissions = tcMiddleware.permissions; @@ -24,17 +25,9 @@ const updateProjectPhaseValidation = { progress: Joi.number().min(0).optional(), details: Joi.any().optional(), order: Joi.number().integer().optional(), + members: Joi.array().items(Joi.number().integer()).optional(), }).required(), }; -const populateMemberDetails = async (phase, req) => { - const members = _.map(phase.members, member => _.pick(member, 'userId')); - try { - const detailedMembers = await util.getObjectsWithMemberDetails(members, ['userId', 'handle', 'photoURL'], req); - return _.assign(phase, { members: detailedMembers }); - } catch (err) { - return _.assign(phase, { members }); - } -}; module.exports = [ // validate request payload @@ -88,35 +81,45 @@ module.exports = [ err.status = 400; reject(err); } else { - _.extend(existing, updatedProps); + _.extend(existing, _.omit(updatedProps, 'members')); existing.save().then(accept).catch(reject); } } })) .then((updatedPhase) => { - updated = updatedPhase; + updated = updatedPhase.get({ plain: true }); + }) + .then(() => { + if (_.isNil(updatedProps.members)) { + return Promise.resolve(); + } + + return updatePhaseMemberService(req.authUser, projectId, phaseId, updatedProps.members) + .then(members => _.assign(updated, { members })); }), ) .then(() => { req.log.debug('updated project phase', JSON.stringify(updated, null, 2)); - const updatedValue = updated.get({ plain: true }); - // emit event util.sendResourceToKafkaBus( req, EVENT.ROUTING_KEY.PROJECT_PHASE_UPDATED, RESOURCES.PHASE, - updatedValue, + updated, previousValue, ROUTES.PHASES.UPDATE); + if (updated.members) { + return util.populatePhasesWithMemberDetails(updated, req) + .then(result => res.json(result)); + } return models.ProjectPhase.findOne({ where: { id: phaseId, projectId }, include: [{ model: models.ProjectPhaseMember, as: 'members', }], - }).then(phaseWithMembers => populateMemberDetails(phaseWithMembers.toJSON(), req) + }).then(phaseWithMembers => util.populatePhasesWithMemberDetails(phaseWithMembers.toJSON(), req) .then(result => res.json(result))); }) .catch(err => next(err)); diff --git a/src/routes/phases/update.spec.js b/src/routes/phases/update.spec.js index cd5720ac..18554c86 100644 --- a/src/routes/phases/update.spec.js +++ b/src/routes/phases/update.spec.js @@ -284,6 +284,74 @@ describe('Project Phases', () => { }); }); + it('should return 200 with member details after updating members', (done) => { + const bodyWithMembers = _.cloneDeep(updateBody); + _.assign(bodyWithMembers, { members: [copilotUser.userId] }); + request(server) + .patch(`/v5/projects/${projectId}/phases/${phaseId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(bodyWithMembers) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body; + validatePhase(resJson, bodyWithMembers); + resJson.members.should.have.length(1); + resJson.members[0].userId.should.eql(copilotUser.userId); + done(); + } + }); + }); + + it('should return 200 with existent member details vith valid payload without members', (done) => { + models.ProjectPhaseMember.create({ + id: 1, + userId: copilotUser.userId, + phaseId, + createdBy: 1, + updatedBy: 1, + }).then(() => { + request(server) + .patch(`/v5/projects/${projectId}/phases/${phaseId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(updateBody) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body; + validatePhase(resJson, updateBody); + resJson.members.should.have.length(1); + resJson.members[0].userId.should.eql(copilotUser.userId); + done(); + } + }); + }); + }); + + it('should return 400 if members property includes userId who is not a member of project', (done) => { + const bodyWithMembers = _.cloneDeep(updateBody); + _.assign(bodyWithMembers, { members: [999] }); + request(server) + .patch(`/v5/projects/${projectId}/phases/${phaseId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(bodyWithMembers) + .expect('Content-Type', /json/) + .expect(400) + .end(done); + }); + it('should return 403 if requested by manager which is not a member', (done) => { request(server) .patch(`/v5/projects/${projectId}/phases/${phaseId}`) diff --git a/src/util.js b/src/util.js index a5ab739a..c3432ff4 100644 --- a/src/util.js +++ b/src/util.js @@ -791,6 +791,38 @@ const projectServiceUtils = { }); }, + /** + * Add member details to Project Phase objects + * + * @param {Array|Object} phases Array of phase object or single phase object + * @param {Object} req The request object + * + * @return {Array|Object} Phase(s) with member details + */ + populatePhasesWithMemberDetails: async (phases, req) => { + if (_.isArray(phases)) { + let members = _.reduce(phases, (acc, phase) => + _.concat(acc, _.map(phase.members, member => _.pick(member, 'userId'))), []); + members = _.uniqBy(members, 'userId'); + try { + const detailedMembers = await util.getObjectsWithMemberDetails(members, ['userId', 'handle', 'photoURL'], req); + return _.map(phases, phase => + _.assign(phase, { members: _.intersectionBy(detailedMembers, phase.members, 'userId') })); + } catch (err) { + return _.map(phases, phase => + _.assign(phase, { members: _.map(phase.members, member => _.pick(member, 'userId')) })); + } + } else { + const members = _.map(phases.members, member => _.pick(member, 'userId')); + try { + const detailedMembers = await util.getObjectsWithMemberDetails(members, ['userId', 'handle', 'photoURL'], req); + return _.assign(phases, { members: detailedMembers }); + } catch (err) { + return _.assign(phases, { members }); + } + } + }, + /** * Retrieve member details from userIds */ From 9237cb80c81d7e816a70e3a3d9aae14e930dc23e Mon Sep 17 00:00:00 2001 From: eisbilir Date: Fri, 30 Jul 2021 16:28:47 +0300 Subject: [PATCH 10/17] use transaction --- src/routes/phaseMembers/updateService.js | 18 ++++++++++++++---- src/routes/phases/create.js | 9 ++++----- src/routes/phases/delete.js | 6 +++--- src/routes/phases/update.js | 6 +++--- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/routes/phaseMembers/updateService.js b/src/routes/phaseMembers/updateService.js index 8a474ae0..b0781c34 100644 --- a/src/routes/phaseMembers/updateService.js +++ b/src/routes/phaseMembers/updateService.js @@ -7,13 +7,19 @@ import models from '../../models'; * @param {String} projectId the project id * @param {String} phaseId the phase id * @param {Array} newPhaseMembers the array of userIds + * @param {Object} _transaction the sequelize transaction (optional) * @returns {Array} the array of updated phase member objects */ -async function update(currentUser, projectId, phaseId, newPhaseMembers) { +async function update(currentUser, projectId, phaseId, newPhaseMembers, _transaction) { const createdBy = _.parseInt(currentUser.userId); const updatedBy = _.parseInt(currentUser.userId); const newMembers = _.uniq(newPhaseMembers); - const transaction = await models.sequelize.transaction(); + let transaction; + if (_.isUndefined(_transaction)) { + transaction = await models.sequelize.transaction(); + } else { + transaction = _transaction; + } try { const projectMembers = _.map(await models.ProjectMember.getActiveProjectMembers(projectId), 'userId'); const notProjectMembers = _.difference(newMembers, projectMembers); @@ -35,10 +41,14 @@ async function update(currentUser, projectId, phaseId, newPhaseMembers) { const result = await models.ProjectPhaseMember.bulkCreate(createData, { transaction }); phaseMembers.push(..._.map(result, item => item.toJSON())); } - await transaction.commit(); + if (_.isUndefined(_transaction)) { + await transaction.commit(); + } return phaseMembers; } catch (err) { - await transaction.rollback(); + if (_.isUndefined(_transaction)) { + await transaction.rollback(); + } throw err; } } diff --git a/src/routes/phases/create.js b/src/routes/phases/create.js index 7a0636c6..1463d898 100644 --- a/src/routes/phases/create.js +++ b/src/routes/phases/create.js @@ -47,7 +47,7 @@ module.exports = [ }); let newProjectPhase = null; - models.sequelize.transaction(() => { + models.sequelize.transaction((transaction) => { req.log.debug('Create Phase - Starting transaction'); return models.Project.findOne({ where: { id: projectId, deletedAt: { $eq: null } }, @@ -64,7 +64,7 @@ module.exports = [ throw err; } return models.ProjectPhase - .create(_.omit(data, 'members')) + .create(_.omit(data, 'members'), { transaction }) .then((_newProjectPhase) => { newProjectPhase = _.cloneDeep(_newProjectPhase); req.log.debug('new project phase created (id# %d, name: %s)', @@ -88,7 +88,6 @@ module.exports = [ err.status = 400; throw err; } - // Create the phase product return models.PhaseProduct.create({ name: productTemplate.name, @@ -98,7 +97,7 @@ module.exports = [ phaseId: newProjectPhase.id, createdBy: req.authUser.userId, updatedBy: req.authUser.userId, - }) + }, { transaction }) .then((phaseProduct) => { newProjectPhase.products = [ _.omit(phaseProduct.toJSON(), ['deletedAt', 'deletedBy']), @@ -112,7 +111,7 @@ module.exports = [ return Promise.resolve(); } - return updatePhaseMemberService(req.authUser, projectId, newProjectPhase.id, data.members) + return updatePhaseMemberService(req.authUser, projectId, newProjectPhase.id, data.members, transaction) .then(members => _.assign(newProjectPhase, { members })); }); }) diff --git a/src/routes/phases/delete.js b/src/routes/phases/delete.js index 3dcaa017..eccc2c02 100644 --- a/src/routes/phases/delete.js +++ b/src/routes/phases/delete.js @@ -16,7 +16,7 @@ module.exports = [ const projectId = _.parseInt(req.params.projectId); const phaseId = _.parseInt(req.params.phaseId); - models.sequelize.transaction(() => + models.sequelize.transaction(transaction => // soft delete the record models.ProjectPhase.findOne({ where: { @@ -32,9 +32,9 @@ module.exports = [ err.status = 404; return Promise.reject(err); } - return existing.update({ deletedBy: req.authUser.userId }); + return existing.update({ deletedBy: req.authUser.userId }, { transaction }); }) - .then(entity => entity.destroy())) + .then(entity => entity.destroy({ transaction }))) .then((deleted) => { req.log.debug('deleted project phase', JSON.stringify(deleted, null, 2)); diff --git a/src/routes/phases/update.js b/src/routes/phases/update.js index 4707ba9a..3008f583 100644 --- a/src/routes/phases/update.js +++ b/src/routes/phases/update.js @@ -45,7 +45,7 @@ module.exports = [ let previousValue; let updated; - models.sequelize.transaction(() => models.ProjectPhase.findOne({ + models.sequelize.transaction(transaction => models.ProjectPhase.findOne({ where: { id: phaseId, projectId, @@ -82,7 +82,7 @@ module.exports = [ reject(err); } else { _.extend(existing, _.omit(updatedProps, 'members')); - existing.save().then(accept).catch(reject); + existing.save({ transaction }).then(accept).catch(reject); } } })) @@ -94,7 +94,7 @@ module.exports = [ return Promise.resolve(); } - return updatePhaseMemberService(req.authUser, projectId, phaseId, updatedProps.members) + return updatePhaseMemberService(req.authUser, projectId, phaseId, updatedProps.members, transaction) .then(members => _.assign(updated, { members })); }), ) From d42095fe91177d91da63ae7e5ba7765a23e7d2a8 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Mon, 2 Aug 2021 18:43:38 +0300 Subject: [PATCH 11/17] fix: phase member tests --- src/routes/phaseMembers/delete.spec.js | 17 +++++++++++++++-- src/routes/phaseMembers/update.spec.js | 18 ++++++++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/routes/phaseMembers/delete.spec.js b/src/routes/phaseMembers/delete.spec.js index 72401df8..d6988d65 100644 --- a/src/routes/phaseMembers/delete.spec.js +++ b/src/routes/phaseMembers/delete.spec.js @@ -148,13 +148,26 @@ describe('Delete phase member', () => { .expect(204, done); }); - it('should return 403 for copilot', (done) => { + it('should return 204 for copilot which is member of project', (done) => { request(server) .delete(`/v5/projects/${id}/phases/${phaseId}/members/${copilotUser.userId}`) .set({ Authorization: `Bearer ${testUtil.jwts.copilot}`, }) - .expect(403, done); + .expect(204, done); + }); + + it('should return 403 for copilot which is not member of project', (done) => { + models.ProjectMember.destroy({ + where: { userId: testUtil.userIds.copilot, id }, + }).then(() => { + request(server) + .delete(`/v5/projects/${id}/phases/${phaseId}/members/${copilotUser.userId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(403, done); + }); }); }); }); diff --git a/src/routes/phaseMembers/update.spec.js b/src/routes/phaseMembers/update.spec.js index a8f21c6f..91251277 100644 --- a/src/routes/phaseMembers/update.spec.js +++ b/src/routes/phaseMembers/update.spec.js @@ -167,14 +167,28 @@ describe('Update phase members', () => { }); }); - it('should return 403 for copilot', (done) => { + it('should return 200 for copilot which is member of project', (done) => { request(server) .post(`/v5/projects/${id}/phases/${phaseId}/members`) .set({ Authorization: `Bearer ${testUtil.jwts.copilot}`, }) .send({ userIds: [copilotUser.userId, memberUser.userId] }) - .expect(403, done); + .expect(200, done); + }); + + it('should return 403 for copilot which is not member of project', (done) => { + models.ProjectMember.destroy({ + where: { userId: testUtil.userIds.copilot, id }, + }).then(() => { + request(server) + .post(`/v5/projects/${id}/phases/${phaseId}/members`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ userIds: [copilotUser.userId, memberUser.userId] }) + .expect(403, done); + }); }); }); }); From e289b5c4445d72446bcf75ffbdcf25f6237ef54d Mon Sep 17 00:00:00 2001 From: eisbilir Date: Tue, 3 Aug 2021 15:10:46 +0300 Subject: [PATCH 12/17] adding phase approval api --- docs/Project API.postman_collection.json | 379 +- docs/Project API.postman_environment.json | 20 +- docs/swagger.yaml | 3611 +++++++++-------- .../20210802_project_phase_approval_table.sql | 27 + src/models/projectPhase.js | 1 + src/models/projectPhaseApproval.js | 50 + src/permissions/constants.js | 12 + src/permissions/index.js | 3 + src/routes/index.js | 4 + src/routes/phaseApprovals/create.js | 76 + src/routes/phaseApprovals/create.spec.js | 294 ++ src/routes/phaseApprovals/list.js | 86 + src/routes/phaseApprovals/list.spec.js | 218 + src/routes/phaseMembers/delete.spec.js | 8 +- src/routes/phaseMembers/list.spec.js | 5 +- src/routes/phaseMembers/update.js | 2 +- src/routes/phaseMembers/update.spec.js | 8 +- src/routes/phaseMembers/updateService.js | 2 +- src/routes/phases/get.js | 4 + src/routes/phases/list.js | 10 +- 20 files changed, 3085 insertions(+), 1735 deletions(-) create mode 100644 migrations/20210802_project_phase_approval_table.sql create mode 100644 src/models/projectPhaseApproval.js create mode 100644 src/routes/phaseApprovals/create.js create mode 100644 src/routes/phaseApprovals/create.spec.js create mode 100644 src/routes/phaseApprovals/list.js create mode 100644 src/routes/phaseApprovals/list.spec.js diff --git a/docs/Project API.postman_collection.json b/docs/Project API.postman_collection.json index 1b7583f4..612452d2 100644 --- a/docs/Project API.postman_collection.json +++ b/docs/Project API.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "4c51e04b-42d3-4c9f-bf08-af02f51f7756", + "_postman_id": "34327e29-e237-4bca-9101-5d2b6a1e25ff", "name": "Project API", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -5026,7 +5026,7 @@ "response": [] }, { - "name": "List Phase with member field", + "name": "List Phase with members field", "request": { "method": "GET", "header": [ @@ -5059,6 +5059,40 @@ }, "response": [] }, + { + "name": "List Phase with approvals field", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{api-url}}/projects/{{projectId}}/phases?fields=status,name,budget,members,products,approvals", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "{{projectId}}", + "phases" + ], + "query": [ + { + "key": "fields", + "value": "status,name,budget,members,products,approvals" + } + ] + } + }, + "response": [] + }, { "name": "List Phase with sort", "request": { @@ -5357,7 +5391,7 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"name\": \"test phase product\",\n\t\"type\": \"type 1\",\n\t\"estimatedPrice\": 10\n}" + "raw": "{\n\t\"name\": \"test phase product\",\n\t\"type\": \"type 1\",\n\t\"estimatedPrice\": 10,\n \"templateId\": 2,\n \"actualPrice\": \"10\"\n}" }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/products", @@ -5890,6 +5924,345 @@ } ] }, + { + "name": "Phase Approvals", + "item": [ + { + "name": "Before Start", + "item": [ + { + "name": "Create project type", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 201\", function () {", + " pm.response.to.have.status(201);", + " if(pm.response.status === \"Created\") {", + " const response = pm.response.json()", + " pm.environment.set(\"projectTypeId\", response.key);", + " }", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"key\": \"new key\",\r\n \"displayName\": \"new displayName\",\r\n \"icon\": \"http://example.com/icon4.ico\",\r\n \t\"question\": \"question 4\",\r\n \t\"info\": \"info 4\",\r\n \t\"aliases\": [\"key-41\", \"key_42\"],\r\n \t\"metadata\": {}\r\n }" + }, + "url": { + "raw": "{{api-url}}/projects/metadata/projectTypes", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "metadata", + "projectTypes" + ] + } + }, + "response": [] + }, + { + "name": "Create project", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 201\", function () {", + " pm.response.to.have.status(201);", + " if(pm.response.status === \"Created\") {", + " const response = pm.response.json()", + " pm.environment.set(\"projectId\", response.id);", + " }", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"name\": \"test project\",\n\t\"description\": \"Hello I am a test project\",\n\t\"type\": \"{{projectTypeId}}\"\n}" + }, + "url": { + "raw": "{{api-url}}/projects", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects" + ] + }, + "description": "Valid request body. Project should be created successfully." + }, + "response": [] + }, + { + "name": "Create project customer", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 201\", function () {", + " pm.response.to.have.status(201);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"userId\": \"88774634\",\n \"role\": \"customer\"\n}" + }, + "url": { + "raw": "{{api-url}}/projects/{{projectId}}/members", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "{{projectId}}", + "members" + ] + }, + "description": "If the request payload is valid, than project member should be created." + }, + "response": [] + }, + { + "name": "Create Phase", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 201\", function () {", + " pm.response.to.have.status(201);", + " if(pm.response.status === \"Created\") {", + " const response = pm.response.json()", + " pm.environment.set(\"phaseId\", response.id);", + " }", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"name\": \"test project phase\",\n\t\"status\": \"active\",\n\t\"startDate\": \"2018-05-15T00:00:00\",\n\t\"endDate\": \"2018-05-16T00:00:00\",\n\t\"budget\": 20,\n\t\"details\": {\n\t\t\"aDetails\": \"a details\"\n\t}\n}" + }, + "url": { + "raw": "{{api-url}}/projects/{{projectId}}/phases", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "{{projectId}}", + "phases" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Create Phase Approval - approve", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token-topcoder-user-88774634}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"decision\": \"approve\",\n \"comment\": \"good\",\n \"startDate\": \"2021-08-02\",\n \"endDate\": \"2021-08-03\",\n \"expectedEndDate\": \"2021-08-03\"\n}" + }, + "url": { + "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/approvals", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "{{projectId}}", + "phases", + "{{phaseId}}", + "approvals" + ] + } + }, + "response": [] + }, + { + "name": "Create Phase Approval - approve Copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token-topcoder-user-88774634}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"decision\": \"reject\",\n \"comment\": \"bad\",\n \"startDate\": \"2021-08-03\",\n \"endDate\": \"2021-08-04\",\n \"expectedEndDate\": \"2021-08-05\"\n}" + }, + "url": { + "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/approvals", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "{{projectId}}", + "phases", + "{{phaseId}}", + "approvals" + ] + } + }, + "response": [] + }, + { + "name": "List Phase Approvals", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/approvals", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "{{projectId}}", + "phases", + "{{phaseId}}", + "approvals" + ] + } + }, + "response": [] + } + ] + }, { "name": "Project Templates", "item": [ diff --git a/docs/Project API.postman_environment.json b/docs/Project API.postman_environment.json index e153507d..6a43c0db 100644 --- a/docs/Project API.postman_environment.json +++ b/docs/Project API.postman_environment.json @@ -1,63 +1,59 @@ { - "id": "9408797f-cb90-43c1-b08b-375e30edb5bb", + "id": "01590787-fe89-4fb8-b48f-0a2a2c8d4096", "name": "Project API", "values": [ { "key": "api-url", "value": "http://localhost:8001/v5", - "description": "", "enabled": true }, { "key": "jwt-token", "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", - "description": "", "enabled": true }, { "key": "jwt-token-admin-40051333", "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw", - "description": "", "enabled": true }, { "key": "jwt-token-member-40051331", "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzEiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.pDtRzcGQjgCBD6aLsW-1OFhzmrv5mXhb8YLDWbGAnKo", - "description": "", "enabled": true }, { "key": "jwt-token-copilot-40051332", "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBDb3BpbG90Il0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjo0MDA1MTMzMiwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImlhdCI6MTQ3MDYyMDA0NH0.DnX17gBaVF2JTuRai-C2BDSdEjij9da_s4eYcMIjP0c", - "description": "", "enabled": true }, { "key": "jwt-token-manager-40051334", "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzQiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.J5VtOEQVph5jfe2Ji-NH7txEDcx_5gthhFeD-MzX9ck", - "description": "", "enabled": true }, { "key": "jwt-token-member2-40051335", "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJtZW1iZXIyIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzNSIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.Mh4bw3wm-cn5Kcf96gLFVlD0kySOqqk4xN3qnreAKL4", - "description": "", "enabled": true }, { "key": "jwt-token-connectAdmin-40051336", "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJDb25uZWN0IEFkbWluIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJjb25uZWN0X2FkbWluMSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzYiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoiY29ubmVjdF9hZG1pbjFAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.nSGfXMl02NZ90ZKLiEKPg75iAjU92mfteaY6xgqkM30", - "description": "", + "enabled": true + }, + { + "key": "jwt-token-topcoder-user-88774634", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiODg3NzQ2MzQiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.ruzw_HVO0c1Aui-7Jqv5qLrUk1d1N8BFd2ZyJJjlQBQ", "enabled": true }, { "key": "inactive-userId", "value": "1800075", - "description": "", "enabled": true } ], "_postman_variable_scope": "environment", - "_postman_exported_at": "2019-06-07T11:02:18.794Z", - "_postman_exported_using": "Postman/6.5.3" + "_postman_exported_at": "2021-08-03T11:54:06.275Z", + "_postman_exported_using": "Postman/8.9.1" } \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 125c87be..68fcf6ed 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,4 +1,4 @@ -swagger: '2.0' +swagger: "2.0" info: version: v5 title: Projects API @@ -11,7 +11,7 @@ info: You can also set a custom page size up to 100 with the `perPage` parameter. Pagination response data is included in http headers. By Default, the response header contains links with `next`, `last`, `first`, `prev` resource links. -host: 'localhost:3000' +host: "localhost:3000" basePath: /v5 schemes: - http @@ -25,7 +25,7 @@ securityDefinitions: name: Authorization in: header paths: - '/projects': + "/projects": get: tags: - project @@ -34,12 +34,12 @@ paths: - Bearer: [] description: Retrieve projects that match the filter responses: - '200': + "200": description: A list of projects schema: type: array items: - $ref: '#/definitions/Project' + $ref: "#/definitions/Project" headers: X-Next-Page: type: integer @@ -62,31 +62,31 @@ paths: Link: type: string description: Pagination link header. - '401': + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/pageParam' - - $ref: '#/parameters/perPageParam' - - $ref: '#/parameters/idQueryParam' - - $ref: '#/parameters/statusQueryParam' - - $ref: '#/parameters/typeQueryParam' - - $ref: '#/parameters/memberOnlyQueryParam' - - $ref: '#/parameters/keywordQueryParam' - - $ref: '#/parameters/nameQueryParam' - - $ref: '#/parameters/codeQueryParam' - - $ref: '#/parameters/customerQueryParam' - - $ref: '#/parameters/managerQueryParam' - - $ref: '#/parameters/directProjectIdQueryParam' + - $ref: "#/parameters/pageParam" + - $ref: "#/parameters/perPageParam" + - $ref: "#/parameters/idQueryParam" + - $ref: "#/parameters/statusQueryParam" + - $ref: "#/parameters/typeQueryParam" + - $ref: "#/parameters/memberOnlyQueryParam" + - $ref: "#/parameters/keywordQueryParam" + - $ref: "#/parameters/nameQueryParam" + - $ref: "#/parameters/codeQueryParam" + - $ref: "#/parameters/customerQueryParam" + - $ref: "#/parameters/managerQueryParam" + - $ref: "#/parameters/directProjectIdQueryParam" - name: sort required: false description: > @@ -106,25 +106,25 @@ paths: name: body required: true schema: - $ref: '#/definitions/NewProject' + $ref: "#/definitions/NewProject" responses: - '200': + "200": description: Returns the newly created project schema: - $ref: '#/definitions/Project' - '401': + $ref: "#/definitions/Project" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/{projectId}': + $ref: "#/definitions/ErrorModel" + "/projects/{projectId}": get: tags: - project @@ -132,28 +132,28 @@ paths: security: - Bearer: [] responses: - '200': + "200": description: a project schema: - $ref: '#/definitions/Project' - '400': + $ref: "#/definitions/Project" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/projectIdParam' + - $ref: "#/parameters/projectIdParam" - name: fields required: false type: string @@ -177,34 +177,34 @@ paths: Update a project that user has access to. Managers and admin are able to pull out a project from cancelled state. responses: - '200': + "200": description: >- Successfully updated project. Returns original and updated project object schema: - $ref: '#/definitions/Project' - '401': + $ref: "#/definitions/Project" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/projectIdParam' + - $ref: "#/parameters/projectIdParam" - name: body in: body required: true @@ -212,7 +212,7 @@ paths: Only specify those properties that needs to be updated. `cancelReason` is mandatory if status is cancelled schema: - $ref: '#/definitions/NewProject' + $ref: "#/definitions/NewProject" delete: tags: - project @@ -220,31 +220,31 @@ paths: security: - Bearer: [] parameters: - - $ref: '#/parameters/projectIdParam' + - $ref: "#/parameters/projectIdParam" responses: - '204': + "204": description: Project successfully removed - '400': + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: If project is not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/{projectId}/attachments': + $ref: "#/definitions/ErrorModel" + "/projects/{projectId}/attachments": get: tags: - project attachments @@ -252,22 +252,22 @@ paths: security: - Bearer: [] responses: - '200': + "200": description: list of project attachments schema: type: array items: - $ref: '#/definitions/ProjectAttachment' - '401': + $ref: "#/definitions/ProjectAttachment" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/projectIdParam' + - $ref: "#/parameters/projectIdParam" operationId: listProjectAttachment post: tags: @@ -276,34 +276,34 @@ paths: security: - Bearer: [] parameters: - - $ref: '#/parameters/projectIdParam' + - $ref: "#/parameters/projectIdParam" - in: body name: body required: true schema: - $ref: '#/definitions/NewProjectAttachment' + $ref: "#/definitions/NewProjectAttachment" responses: - '200': + "200": description: Returns the newly created project attachment schema: - $ref: '#/definitions/ProjectAttachment' - '400': + $ref: "#/definitions/ProjectAttachment" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/{projectId}/attachments/{id}': + $ref: "#/definitions/ErrorModel" + "/projects/{projectId}/attachments/{id}": get: tags: - project attachments @@ -311,24 +311,24 @@ paths: security: - Bearer: [] responses: - '200': + "200": description: a project attachment schema: - $ref: '#/definitions/ProjectAttachment' - '401': + $ref: "#/definitions/ProjectAttachment" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/projectIdParam' + - $ref: "#/parameters/projectIdParam" - in: path name: id required: true @@ -342,7 +342,7 @@ paths: security: - Bearer: [] parameters: - - $ref: '#/parameters/projectIdParam' + - $ref: "#/parameters/projectIdParam" - in: path name: id required: true @@ -353,32 +353,32 @@ paths: required: true description: Specify only those properties that needs to be updated schema: - $ref: '#/definitions/UpdateProjectAttachment' + $ref: "#/definitions/UpdateProjectAttachment" responses: - '200': + "200": description: Returns the newly created project schema: - $ref: '#/definitions/ProjectAttachment' - '400': + $ref: "#/definitions/ProjectAttachment" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: If project attachment is not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" delete: tags: - project attachments @@ -386,36 +386,36 @@ paths: security: - Bearer: [] parameters: - - $ref: '#/parameters/projectIdParam' + - $ref: "#/parameters/projectIdParam" - in: path name: id required: true description: The id of attachment to delete type: integer responses: - '204': + "204": description: Attachment successfully removed - '400': + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: If attachment is not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/{projectId}/members': + $ref: "#/definitions/ErrorModel" + "/projects/{projectId}/members": get: tags: - project members @@ -423,26 +423,26 @@ paths: security: - Bearer: [] responses: - '200': + "200": description: list of project members schema: type: array items: - $ref: '#/definitions/ProjectMember' - '400': + $ref: "#/definitions/ProjectMember" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/projectIdParam' + - $ref: "#/parameters/projectIdParam" - name: role required: false description: > @@ -457,34 +457,34 @@ paths: security: - Bearer: [] parameters: - - $ref: '#/parameters/projectIdParam' + - $ref: "#/parameters/projectIdParam" - in: body name: body required: true schema: - $ref: '#/definitions/NewProjectMember' + $ref: "#/definitions/NewProjectMember" responses: - '200': + "200": description: Returns the newly created project schema: - $ref: '#/definitions/ProjectMember' - '401': + $ref: "#/definitions/ProjectMember" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/{projectId}/members/{id}': + $ref: "#/definitions/ErrorModel" + "/projects/{projectId}/members/{id}": get: tags: - project members @@ -492,24 +492,24 @@ paths: security: - Bearer: [] responses: - '200': + "200": description: a project member schema: - $ref: '#/definitions/ProjectMember' - '401': + $ref: "#/definitions/ProjectMember" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/projectIdParam' + - $ref: "#/parameters/projectIdParam" - in: path name: id required: true @@ -522,34 +522,34 @@ paths: security: - Bearer: [] parameters: - - $ref: '#/parameters/projectIdParam' + - $ref: "#/parameters/projectIdParam" - in: path name: id required: true type: integer responses: - '204': + "204": description: Member successfully removed - '400': + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: If Project doesn't contain such Member schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" patch: tags: - project members @@ -557,34 +557,34 @@ paths: - Bearer: [] description: Support editing project member roles & primary option. responses: - '200': + "200": description: >- Successfully updated project member. Returns entire project member object schema: - $ref: '#/definitions/ProjectMember' - '401': + $ref: "#/definitions/ProjectMember" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/projectIdParam' + - $ref: "#/parameters/projectIdParam" - in: path name: id required: true @@ -593,10 +593,10 @@ paths: in: body required: true schema: - $ref: '#/definitions/UpdateProjectMember' - '/projects/{projectId}/phases': + $ref: "#/definitions/UpdateProjectMember" + "/projects/{projectId}/phases": parameters: - - $ref: '#/parameters/projectIdParam' + - $ref: "#/parameters/projectIdParam" get: tags: - phase @@ -620,30 +620,30 @@ paths: startDate asc in: query type: string - - $ref: '#/parameters/memberOnlyQueryParam' + - $ref: "#/parameters/memberOnlyQueryParam" responses: - '200': + "200": description: A list of project phases schema: type: array items: - $ref: '#/definitions/ProjectPhase' - '400': + $ref: "#/definitions/ProjectPhase" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" post: tags: - phase @@ -661,7 +661,7 @@ paths: schema: type: object allOf: - - $ref: '#/definitions/ProjectPhaseRequest' + - $ref: "#/definitions/ProjectPhaseRequest" properties: productTemplateId: type: number @@ -670,30 +670,30 @@ paths: the optional productTemplateId used to populate a new phase product for the created phase responses: - '200': + "200": description: Returns the newly created project phase schema: - $ref: '#/definitions/ProjectPhase' - '401': + $ref: "#/definitions/ProjectPhase" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/{projectId}/phases/{phaseId}': + $ref: "#/definitions/ErrorModel" + "/projects/{projectId}/phases/{phaseId}": parameters: - - $ref: '#/parameters/projectIdParam' - - $ref: '#/parameters/phaseIdParam' + - $ref: "#/parameters/projectIdParam" + - $ref: "#/parameters/phaseIdParam" get: tags: - phase @@ -703,32 +703,32 @@ paths: security: - Bearer: [] responses: - '200': + "200": description: a project phase schema: - $ref: '#/definitions/ProjectPhase' - '400': + $ref: "#/definitions/ProjectPhase" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/phaseIdParam' + - $ref: "#/parameters/phaseIdParam" operationId: getProjectPhase patch: tags: @@ -742,37 +742,37 @@ paths: same project which have `order` greater than or equal to the `order` specified in the POST body. responses: - '200': + "200": description: Successfully updated project phase. schema: - $ref: '#/definitions/ProjectPhase' - '401': + $ref: "#/definitions/ProjectPhase" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/phaseIdParam' + - $ref: "#/parameters/phaseIdParam" - name: body in: body required: true schema: - $ref: '#/definitions/ProjectPhaseRequest' + $ref: "#/definitions/ProjectPhaseRequest" delete: tags: - phase @@ -782,33 +782,33 @@ paths: security: - Bearer: [] parameters: - - $ref: '#/parameters/phaseIdParam' + - $ref: "#/parameters/phaseIdParam" responses: - '204': + "204": description: Project phase successfully removed - '400': + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: If project is not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/{projectId}/workstreams': + $ref: "#/definitions/ErrorModel" + "/projects/{projectId}/workstreams": parameters: - - $ref: '#/parameters/projectIdParam' + - $ref: "#/parameters/projectIdParam" get: tags: - workstream @@ -818,28 +818,28 @@ paths: description: >- Retrieve all project workstreams. responses: - '200': + "200": description: A list of project work streams schema: type: array items: - $ref: '#/definitions/WorkStream' - '401': + $ref: "#/definitions/WorkStream" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: If project is not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" post: tags: - workstream @@ -855,36 +855,36 @@ paths: schema: type: object allOf: - - $ref: '#/definitions/WorkStreamRequest' + - $ref: "#/definitions/WorkStreamRequest" responses: - '200': + "200": description: Returns the newly created project work stream schema: - $ref: '#/definitions/WorkStream' - '400': + $ref: "#/definitions/WorkStream" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: If project is not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/{projectId}/workstreams/{workStreamId}': + $ref: "#/definitions/ErrorModel" + "/projects/{projectId}/workstreams/{workStreamId}": parameters: - - $ref: '#/parameters/projectIdParam' - - $ref: '#/parameters/workStreamIdParam' + - $ref: "#/parameters/projectIdParam" + - $ref: "#/parameters/workStreamIdParam" get: tags: - workstream @@ -893,28 +893,28 @@ paths: security: - Bearer: [] responses: - '200': + "200": description: a project work stream schema: - $ref: '#/definitions/WorkStream' - '401': + $ref: "#/definitions/WorkStream" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/workStreamIdParam' + - $ref: "#/parameters/workStreamIdParam" operationId: getWorkStream patch: tags: @@ -925,36 +925,36 @@ paths: description: >- Update a project work stream. responses: - '200': + "200": description: Successfully updated project work stream. schema: - $ref: '#/definitions/WorkStream' - '400': + $ref: "#/definitions/WorkStream" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - name: body in: body required: true schema: - $ref: '#/definitions/WorkStreamRequest' + $ref: "#/definitions/WorkStreamRequest" delete: tags: - workstream @@ -963,28 +963,28 @@ paths: security: - Bearer: [] responses: - '204': + "204": description: Project work stream successfully removed - '401': + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: If project is not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/{projectId}/workstreams/{workStreamId}/works': + $ref: "#/definitions/ErrorModel" + "/projects/{projectId}/workstreams/{workStreamId}/works": parameters: - - $ref: '#/parameters/projectIdParam' - - $ref: '#/parameters/workStreamIdParam' + - $ref: "#/parameters/projectIdParam" + - $ref: "#/parameters/workStreamIdParam" get: tags: - work @@ -1008,28 +1008,28 @@ paths: in: query type: string responses: - '200': + "200": description: A list of project works schema: type: array items: - $ref: '#/definitions/ProjectPhase' - '401': + $ref: "#/definitions/ProjectPhase" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: If project or workstream is not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" post: tags: - work @@ -1045,37 +1045,37 @@ paths: schema: type: object allOf: - - $ref: '#/definitions/ProjectPhaseRequest' + - $ref: "#/definitions/ProjectPhaseRequest" responses: - '200': + "200": description: Returns the newly created project work schema: - $ref: '#/definitions/ProjectPhase' - '400': + $ref: "#/definitions/ProjectPhase" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: If project or workstream is not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/{projectId}/workstreams/{workStreamId}/works/{phaseId}': + $ref: "#/definitions/ErrorModel" + "/projects/{projectId}/workstreams/{workStreamId}/works/{phaseId}": parameters: - - $ref: '#/parameters/projectIdParam' - - $ref: '#/parameters/phaseIdParam' - - $ref: '#/parameters/workStreamIdParam' + - $ref: "#/parameters/projectIdParam" + - $ref: "#/parameters/phaseIdParam" + - $ref: "#/parameters/workStreamIdParam" get: tags: - work @@ -1084,28 +1084,28 @@ paths: security: - Bearer: [] responses: - '200': + "200": description: a project work schema: - $ref: '#/definitions/ProjectPhase' - '401': + $ref: "#/definitions/ProjectPhase" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/phaseIdParam' + - $ref: "#/parameters/phaseIdParam" operationId: getWork patch: tags: @@ -1116,37 +1116,37 @@ paths: description: >- Update work for given project and workstream. responses: - '200': + "200": description: Successfully updated project work. schema: - $ref: '#/definitions/ProjectPhase' - '400': + $ref: "#/definitions/ProjectPhase" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/phaseIdParam' + - $ref: "#/parameters/phaseIdParam" - name: body in: body required: true schema: - $ref: '#/definitions/ProjectPhaseRequest' + $ref: "#/definitions/ProjectPhaseRequest" delete: tags: - work @@ -1155,31 +1155,31 @@ paths: security: - Bearer: [] parameters: - - $ref: '#/parameters/phaseIdParam' + - $ref: "#/parameters/phaseIdParam" responses: - '204': + "204": description: Work successfully removed - '401': + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: If project or workstream is not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/{projectId}/workstreams/{workStreamId}/works/{phaseId}/workitems': + $ref: "#/definitions/ErrorModel" + "/projects/{projectId}/workstreams/{workStreamId}/works/{phaseId}/workitems": parameters: - - $ref: '#/parameters/projectIdParam' - - $ref: '#/parameters/phaseIdParam' - - $ref: '#/parameters/workStreamIdParam' + - $ref: "#/parameters/projectIdParam" + - $ref: "#/parameters/phaseIdParam" + - $ref: "#/parameters/workStreamIdParam" get: tags: - work item @@ -1189,28 +1189,28 @@ paths: description: >- Retrieve all work items for given project, workstream and phase. responses: - '200': + "200": description: A list of work items schema: type: array items: - $ref: '#/definitions/PhaseProduct' - '401': + $ref: "#/definitions/PhaseProduct" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: If project, workstream or phase is not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" post: tags: - work item @@ -1223,38 +1223,38 @@ paths: name: body required: true schema: - $ref: '#/definitions/PhaseProductRequest' + $ref: "#/definitions/PhaseProductRequest" responses: - '200': + "200": description: Returns the newly created work item schema: - $ref: '#/definitions/PhaseProduct' - '400': + $ref: "#/definitions/PhaseProduct" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: If project, workstream or phase is not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/{projectId}/workstreams/{workStreamId}/works/{phaseId}/workitems/{productId}': + $ref: "#/definitions/ErrorModel" + "/projects/{projectId}/workstreams/{workStreamId}/works/{phaseId}/workitems/{productId}": parameters: - - $ref: '#/parameters/projectIdParam' - - $ref: '#/parameters/phaseIdParam' - - $ref: '#/parameters/workStreamIdParam' - - $ref: '#/parameters/productIdParam' + - $ref: "#/parameters/projectIdParam" + - $ref: "#/parameters/phaseIdParam" + - $ref: "#/parameters/workStreamIdParam" + - $ref: "#/parameters/productIdParam" get: tags: - work item @@ -1263,28 +1263,28 @@ paths: security: - Bearer: [] responses: - '200': + "200": description: a work item schema: - $ref: '#/definitions/PhaseProduct' - '401': + $ref: "#/definitions/PhaseProduct" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/phaseIdParam' + - $ref: "#/parameters/phaseIdParam" operationId: getWorkItem patch: tags: @@ -1295,37 +1295,37 @@ paths: description: >- Update a work item for given project, workstream and phase. responses: - '200': + "200": description: Successfully updated work item. schema: - $ref: '#/definitions/PhaseProduct' - '400': + $ref: "#/definitions/PhaseProduct" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/phaseIdParam' + - $ref: "#/parameters/phaseIdParam" - name: body in: body required: true schema: - $ref: '#/definitions/PhaseProductRequest' + $ref: "#/definitions/PhaseProductRequest" delete: tags: - work item @@ -1334,29 +1334,29 @@ paths: security: - Bearer: [] parameters: - - $ref: '#/parameters/phaseIdParam' + - $ref: "#/parameters/phaseIdParam" responses: - '204': + "204": description: Work item successfully removed - '401': + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: If project, workstream or phase is not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/{projectId}/settings': + $ref: "#/definitions/ErrorModel" + "/projects/{projectId}/settings": parameters: - - $ref: '#/parameters/projectIdParam' + - $ref: "#/parameters/projectIdParam" get: tags: - project settings @@ -1366,24 +1366,24 @@ paths: description: >- Retrieve all project settings. Only users with readPermission can get the setting responses: - '200': + "200": description: A list of project phases schema: type: array items: - $ref: '#/definitions/ProjectSetting' - '401': + $ref: "#/definitions/ProjectSetting" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" post: tags: - project settings @@ -1399,32 +1399,32 @@ paths: schema: type: object allOf: - - $ref: '#/definitions/ProjectSettingRequest' + - $ref: "#/definitions/ProjectSettingRequest" responses: - '200': + "200": description: Returns the newly created project phase schema: - $ref: '#/definitions/ProjectPhase' - '400': + $ref: "#/definitions/ProjectPhase" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/{projectId}/settings/{settingId}': + $ref: "#/definitions/ErrorModel" + "/projects/{projectId}/settings/{settingId}": parameters: - - $ref: '#/parameters/projectIdParam' - - $ref: '#/parameters/settingIdParam' + - $ref: "#/parameters/projectIdParam" + - $ref: "#/parameters/settingIdParam" patch: tags: - project settings @@ -1434,36 +1434,36 @@ paths: description: >- Update a project setting. All user with write permission can edit the setting. responses: - '200': + "200": description: Successfully updated project setting. schema: - $ref: '#/definitions/ProjectSetting' - '400': + $ref: "#/definitions/ProjectSetting" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - name: body in: body required: true schema: - $ref: '#/definitions/ProjectSettingRequest' + $ref: "#/definitions/ProjectSettingRequest" delete: tags: - project settings @@ -1473,27 +1473,27 @@ paths: security: - Bearer: [] parameters: - - $ref: '#/parameters/settingIdParam' + - $ref: "#/parameters/settingIdParam" responses: - '204': + "204": description: Project setting successfully removed - '401': + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: If project is not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/{projectId}/estimations/{estimationId}/items': + $ref: "#/definitions/ErrorModel" + "/projects/{projectId}/estimations/{estimationId}/items": get: tags: - Project Estimation Items @@ -1501,35 +1501,35 @@ paths: - Bearer: [] description: get project estimation items parameters: - - $ref: '#/parameters/projectIdParam' - - $ref: '#/parameters/projectEstimationIdParam' + - $ref: "#/parameters/projectIdParam" + - $ref: "#/parameters/projectEstimationIdParam" responses: - '200': + "200": description: List of project estimation items schema: type: array items: - $ref: '#/definitions/ProjectEstimationItem' - '401': + $ref: "#/definitions/ProjectEstimationItem" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Model not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Invalid server state or unknown error schema: - $ref: '#/definitions/ErrorModel' - '/projects/{projectId}/phases/{phaseId}/products': + $ref: "#/definitions/ErrorModel" + "/projects/{projectId}/phases/{phaseId}/products": parameters: - - $ref: '#/parameters/projectIdParam' - - $ref: '#/parameters/phaseIdParam' + - $ref: "#/parameters/projectIdParam" + - $ref: "#/parameters/phaseIdParam" get: tags: - phase product @@ -1540,28 +1540,28 @@ paths: Retrieve all phase products. All users who can edit project can access this endpoint. responses: - '200': + "200": description: A list of phase products schema: type: array items: - $ref: '#/definitions/PhaseProduct' - '400': + $ref: "#/definitions/PhaseProduct" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" post: tags: - phase product @@ -1574,33 +1574,33 @@ paths: name: body required: true schema: - $ref: '#/definitions/PhaseProductRequest' + $ref: "#/definitions/PhaseProductRequest" responses: - '200': + "200": description: Returns the newly created phase product schema: - $ref: '#/definitions/PhaseProduct' - '401': + $ref: "#/definitions/PhaseProduct" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/{projectId}/phases/{phaseId}/products/{productId}': + $ref: "#/definitions/ErrorModel" + "/projects/{projectId}/phases/{phaseId}/products/{productId}": parameters: - - $ref: '#/parameters/projectIdParam' - - $ref: '#/parameters/phaseIdParam' - - $ref: '#/parameters/productIdParam' + - $ref: "#/parameters/projectIdParam" + - $ref: "#/parameters/phaseIdParam" + - $ref: "#/parameters/productIdParam" get: tags: - phase product @@ -1610,32 +1610,32 @@ paths: security: - Bearer: [] responses: - '200': + "200": description: a phase product schema: - $ref: '#/definitions/PhaseProduct' - '400': + $ref: "#/definitions/PhaseProduct" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/phaseIdParam' + - $ref: "#/parameters/phaseIdParam" operationId: getPhaseProduct patch: tags: @@ -1647,37 +1647,37 @@ paths: Update a phase product. All users who can edit project can access this endpoint. responses: - '200': + "200": description: Successfully updated phase product. schema: - $ref: '#/definitions/PhaseProduct' - '401': + $ref: "#/definitions/PhaseProduct" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/phaseIdParam' + - $ref: "#/parameters/phaseIdParam" - name: body in: body required: true schema: - $ref: '#/definitions/PhaseProductRequest' + $ref: "#/definitions/PhaseProductRequest" delete: tags: - phase product @@ -1687,147 +1687,225 @@ paths: security: - Bearer: [] parameters: - - $ref: '#/parameters/phaseIdParam' + - $ref: "#/parameters/phaseIdParam" responses: - '204': + "204": description: Project phase successfully removed - '400': + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: If project is not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/{projectId}/phases/{phaseId}/members': + $ref: "#/definitions/ErrorModel" + "/projects/{projectId}/phases/{phaseId}/members": parameters: - - $ref: '#/parameters/projectIdParam' - - $ref: '#/parameters/phaseIdParam' + - $ref: "#/parameters/projectIdParam" + - $ref: "#/parameters/phaseIdParam" get: tags: - phase members description: >- - Retrieve phase members by id. All users who can see project members can access + Retrieve phase members. All users who can see project members can access this endpoint. security: - Bearer: [] responses: - '200': + "200": description: phase members schema: type: array items: - $ref: '#/definitions/PhaseMember' - '400': + $ref: "#/definitions/PhaseMember" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" post: tags: - phase members security: - Bearer: [] description: >- - Update phase members. Only admin roles can access this + Update phase members. Admin/manager and project copilot roles can access this endpoint. parameters: - in: body name: body required: true schema: - $ref: '#/definitions/NewPhaseMember' + $ref: "#/definitions/NewPhaseMember" responses: - '200': + "200": description: Successfully updated phase members. schema: type: array items: - $ref: '#/definitions/PhaseMember' - '401': + $ref: "#/definitions/PhaseMember" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/{projectId}/phases/{phaseId}/members/{userId}': + $ref: "#/definitions/ErrorModel" + "/projects/{projectId}/phases/{phaseId}/members/{userId}": parameters: - - $ref: '#/parameters/projectIdParam' - - $ref: '#/parameters/phaseIdParam' - - $ref: '#/parameters/userIdParam' + - $ref: "#/parameters/projectIdParam" + - $ref: "#/parameters/phaseIdParam" + - $ref: "#/parameters/userIdParam" delete: tags: - phase members description: >- - Remove an existing phase member. Only admin roles can + Remove an existing phase member. Admin/manager and project copilot roles can access this endpoint. security: - Bearer: [] responses: - '204': + "204": description: Phase member successfully removed - '400': + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/{projectId}/upgrade': + $ref: "#/definitions/ErrorModel" + "/projects/{projectId}/phases/{phaseId}/approvals": + parameters: + - $ref: "#/parameters/projectIdParam" + - $ref: "#/parameters/phaseIdParam" + get: + tags: + - phase approvals + description: >- + Retrieve phase approvals. All users who can view project can access + this endpoint. + security: + - Bearer: [] + responses: + "200": + description: phase approvals + schema: + type: array + items: + $ref: "#/definitions/PhaseApproval" + "400": + description: Bad request + schema: + $ref: "#/definitions/ErrorModel" + "401": + description: Unauthorized + schema: + $ref: "#/definitions/ErrorModel" + "403": + description: Forbidden + schema: + $ref: "#/definitions/ErrorModel" + "404": + description: Not found + schema: + $ref: "#/definitions/ErrorModel" + "500": + description: Internal Server Error + schema: + $ref: "#/definitions/ErrorModel" + post: + tags: + - phase approvals + security: + - Bearer: [] + description: >- + Create phase approval. Only project customer can access this + endpoint. + parameters: + - in: body + name: body + required: true + schema: + $ref: "#/definitions/NewPhaseApproval" + responses: + "200": + description: Successfully create phase approval. + schema: + $ref: "#/definitions/PhaseApproval" + "401": + description: Unauthorized + schema: + $ref: "#/definitions/ErrorModel" + "403": + description: Forbidden + schema: + $ref: "#/definitions/ErrorModel" + "404": + description: Not found + schema: + $ref: "#/definitions/ErrorModel" + "400": + description: Bad request + schema: + $ref: "#/definitions/ErrorModel" + "500": + description: Internal Server Error + schema: + $ref: "#/definitions/ErrorModel" + "/projects/{projectId}/upgrade": post: tags: - project @@ -1838,38 +1916,38 @@ paths: Migrates a project to a target version. Only users with "administrator" or "Connect admin" roles can access to this endpoint parameters: - - $ref: '#/parameters/projectIdParam' + - $ref: "#/parameters/projectIdParam" - name: body in: body required: true description: Project upgrade body schema: - $ref: '#/definitions/ProjectUpgrade' + $ref: "#/definitions/ProjectUpgrade" responses: - '200': + "200": description: Project migrated successfully schema: - $ref: '#/definitions/ProjectUpgrade' - '401': + $ref: "#/definitions/ProjectUpgrade" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Project not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" /projects/metadata: get: tags: @@ -1882,18 +1960,18 @@ paths: milestoneTemplates, projectTypes, productCategories. All user roles can access this endpoint. responses: - '200': + "200": description: The metadata schema: - $ref: '#/definitions/AllMetadataResponse' - '401': + $ref: "#/definitions/AllMetadataResponse" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" /projects/metadata/projectTemplates: get: tags: @@ -1903,20 +1981,20 @@ paths: - Bearer: [] description: Retrieve all project templates. All user roles can access this endpoint. responses: - '200': + "200": description: A list of project templates schema: type: array items: - $ref: '#/definitions/ProjectTemplate' - '401': + $ref: "#/definitions/ProjectTemplate" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" post: tags: - projectTemplate @@ -1929,29 +2007,29 @@ paths: name: body required: true schema: - $ref: '#/definitions/ProjectTemplateRequest' + $ref: "#/definitions/ProjectTemplateRequest" responses: - '200': + "200": description: Returns the newly created project template schema: - $ref: '#/definitions/ProjectTemplate' - '401': + $ref: "#/definitions/ProjectTemplate" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/metadata/projectTemplates/{templateId}': + $ref: "#/definitions/ErrorModel" + "/projects/metadata/projectTemplates/{templateId}": get: tags: - projectTemplate @@ -1961,28 +2039,28 @@ paths: security: - Bearer: [] responses: - '200': + "200": description: a project template schema: - $ref: '#/definitions/ProjectTemplate' - '400': + $ref: "#/definitions/ProjectTemplate" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/templateIdParam' + - $ref: "#/parameters/templateIdParam" operationId: getProjectTemplate patch: tags: @@ -1996,37 +2074,37 @@ paths: would overwrite the existing fields, or add new if the fields don't exist in the JSON object. responses: - '200': + "200": description: Successfully updated project template. schema: - $ref: '#/definitions/ProjectTemplate' - '401': + $ref: "#/definitions/ProjectTemplate" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/templateIdParam' + - $ref: "#/parameters/templateIdParam" - name: body in: body required: true schema: - $ref: '#/definitions/ProjectTemplateRequest' + $ref: "#/definitions/ProjectTemplateRequest" delete: tags: - projectTemplate @@ -2036,31 +2114,31 @@ paths: security: - Bearer: [] parameters: - - $ref: '#/parameters/templateIdParam' + - $ref: "#/parameters/templateIdParam" responses: - '204': + "204": description: Project template successfully removed - '400': + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: If project is not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/metadata/projectTemplates/{templateId}/upgrade': + $ref: "#/definitions/ErrorModel" + "/projects/metadata/projectTemplates/{templateId}/upgrade": post: tags: - projectTemplate @@ -2069,35 +2147,35 @@ paths: security: - Bearer: [] parameters: - - $ref: '#/parameters/templateIdParam' + - $ref: "#/parameters/templateIdParam" - in: body name: body required: true schema: - $ref: '#/definitions/ProjectTemplateUpgradeBody' + $ref: "#/definitions/ProjectTemplateUpgradeBody" responses: - '200': + "200": description: Project template successfully upgrade - '401': + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: If project template is not found schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" /projects/metadata/productTemplates: get: tags: @@ -2107,20 +2185,20 @@ paths: - Bearer: [] description: Retrieve all product templates. All user roles can access this endpoint. responses: - '200': + "200": description: A list of product templates schema: type: array items: - $ref: '#/definitions/ProductTemplate' - '401': + $ref: "#/definitions/ProductTemplate" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" post: tags: - productTemplate @@ -2133,29 +2211,29 @@ paths: name: body required: true schema: - $ref: '#/definitions/ProductTemplateRequest' + $ref: "#/definitions/ProductTemplateRequest" responses: - '200': + "200": description: Returns the newly created product template schema: - $ref: '#/definitions/ProductTemplate' - '401': + $ref: "#/definitions/ProductTemplate" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/metadata/productTemplates/{templateId}': + $ref: "#/definitions/ErrorModel" + "/projects/metadata/productTemplates/{templateId}": get: tags: - productTemplate @@ -2165,28 +2243,28 @@ paths: security: - Bearer: [] responses: - '200': + "200": description: a product template schema: - $ref: '#/definitions/ProductTemplate' - '400': + $ref: "#/definitions/ProductTemplate" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/templateIdParam' + - $ref: "#/parameters/templateIdParam" operationId: getProductTemplate patch: tags: @@ -2200,37 +2278,37 @@ paths: would overwrite the existing fields, or add new if the fields don't exist in the JSON object. responses: - '200': + "200": description: Successfully updated product template. schema: - $ref: '#/definitions/ProductTemplate' - '401': + $ref: "#/definitions/ProductTemplate" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/templateIdParam' + - $ref: "#/parameters/templateIdParam" - name: body in: body required: true schema: - $ref: '#/definitions/ProductTemplateRequest' + $ref: "#/definitions/ProductTemplateRequest" delete: tags: - productTemplate @@ -2240,31 +2318,31 @@ paths: security: - Bearer: [] parameters: - - $ref: '#/parameters/templateIdParam' + - $ref: "#/parameters/templateIdParam" responses: - '204': + "204": description: Product template successfully removed - '400': + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: If product is not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/metadata/productTemplates/{templateId}/upgrade': + $ref: "#/definitions/ErrorModel" + "/projects/metadata/productTemplates/{templateId}/upgrade": post: tags: - productTemplate @@ -2273,35 +2351,35 @@ paths: security: - Bearer: [] parameters: - - $ref: '#/parameters/templateIdParam' + - $ref: "#/parameters/templateIdParam" - in: body name: body required: true schema: - $ref: '#/definitions/ProductTemplateUpgradeBody' + $ref: "#/definitions/ProductTemplateUpgradeBody" responses: - '200': + "200": description: Product template successfully upgraded - '401': + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: If product template is not found schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" /projects/metadata/productCategories: get: tags: @@ -2313,20 +2391,20 @@ paths: Retrieve all product categories. All user roles can access this endpoint. responses: - '200': + "200": description: A list of product categories schema: type: array items: - $ref: '#/definitions/ProductCategory' - '401': + $ref: "#/definitions/ProductCategory" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" post: tags: - productCategory @@ -2341,29 +2419,29 @@ paths: name: body required: true schema: - $ref: '#/definitions/ProductCategoryCreateRequest' + $ref: "#/definitions/ProductCategoryCreateRequest" responses: - '200': + "200": description: Returns the newly created product category schema: - $ref: '#/definitions/ProductCategory' - '401': + $ref: "#/definitions/ProductCategory" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/metadata/productCategories/{key}': + $ref: "#/definitions/ErrorModel" + "/projects/metadata/productCategories/{key}": get: tags: - productCategory @@ -2373,28 +2451,28 @@ paths: security: - Bearer: [] responses: - '200': + "200": description: a product category schema: - $ref: '#/definitions/ProductCategory' - '400': + $ref: "#/definitions/ProductCategory" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/keyParam' + - $ref: "#/parameters/keyParam" operationId: getProductCategory patch: tags: @@ -2406,37 +2484,37 @@ paths: Update a product category. Only admin or connect admin can access this endpoint. responses: - '200': + "200": description: Successfully updated product category. schema: - $ref: '#/definitions/ProductCategory' - '401': + $ref: "#/definitions/ProductCategory" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/keyParam' + - $ref: "#/parameters/keyParam" - name: body in: body required: true schema: - $ref: '#/definitions/ProductCategoryRequest' + $ref: "#/definitions/ProductCategoryRequest" delete: tags: - productCategory @@ -2446,30 +2524,30 @@ paths: security: - Bearer: [] parameters: - - $ref: '#/parameters/keyParam' + - $ref: "#/parameters/keyParam" responses: - '204': + "204": description: Product category successfully removed - '400': + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: If product category is not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" /projects/metadata/projectTypes: get: tags: @@ -2479,20 +2557,20 @@ paths: - Bearer: [] description: Retrieve all project types. All user roles can access this endpoint. responses: - '200': + "200": description: A list of project types schema: type: array items: - $ref: '#/definitions/ProjectType' - '401': + $ref: "#/definitions/ProjectType" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" post: tags: - projectType @@ -2507,29 +2585,29 @@ paths: name: body required: true schema: - $ref: '#/definitions/ProjectTypeCreateRequest' + $ref: "#/definitions/ProjectTypeCreateRequest" responses: - '200': + "200": description: Returns the newly created project type schema: - $ref: '#/definitions/ProjectType' - '401': + $ref: "#/definitions/ProjectType" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/metadata/projectTypes/{key}': + $ref: "#/definitions/ErrorModel" + "/projects/metadata/projectTypes/{key}": get: tags: - projectType @@ -2537,28 +2615,28 @@ paths: security: - Bearer: [] responses: - '200': + "200": description: a project type schema: - $ref: '#/definitions/ProjectType' - '400': + $ref: "#/definitions/ProjectType" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/keyParam' + - $ref: "#/parameters/keyParam" operationId: getProjectType patch: tags: @@ -2570,37 +2648,37 @@ paths: Update a project type. Only admin or connect admin can access this endpoint. responses: - '200': + "200": description: Successfully updated project type. schema: - $ref: '#/definitions/ProjectType' - '401': + $ref: "#/definitions/ProjectType" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/keyParam' + - $ref: "#/parameters/keyParam" - name: body in: body required: true schema: - $ref: '#/definitions/ProjectTypeRequest' + $ref: "#/definitions/ProjectTypeRequest" delete: tags: - projectType @@ -2610,30 +2688,30 @@ paths: security: - Bearer: [] parameters: - - $ref: '#/parameters/keyParam' + - $ref: "#/parameters/keyParam" responses: - '204': + "204": description: Project type successfully removed - '400': + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: If project is not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" /projects/metadata/orgConfig: get: tags: @@ -2656,24 +2734,24 @@ paths: in: query description: configuration name responses: - '200': + "200": description: A list of organization configs schema: type: array items: - $ref: '#/definitions/OrgConfig' - '400': + $ref: "#/definitions/OrgConfig" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" post: tags: - orgConfig @@ -2688,29 +2766,29 @@ paths: name: body required: true schema: - $ref: '#/definitions/OrgConfigCreateRequest' + $ref: "#/definitions/OrgConfigCreateRequest" responses: - '200': + "200": description: Returns the newly created organization config schema: - $ref: '#/definitions/OrgConfig' - '401': + $ref: "#/definitions/OrgConfig" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/metadata/orgConfig/{id}': + $ref: "#/definitions/ErrorModel" + "/projects/metadata/orgConfig/{id}": get: tags: - orgConfig @@ -2718,28 +2796,28 @@ paths: security: - Bearer: [] responses: - '200': + "200": description: a project type schema: - $ref: '#/definitions/OrgConfig' - '400': + $ref: "#/definitions/OrgConfig" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/idParam' + - $ref: "#/parameters/idParam" operationId: getOrgConfig patch: tags: @@ -2751,37 +2829,37 @@ paths: Update a organization config. Only admin or connect admin can access this endpoint. responses: - '200': + "200": description: Successfully updated organization config. schema: - $ref: '#/definitions/OrgConfig' - '401': + $ref: "#/definitions/OrgConfig" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/idParam' + - $ref: "#/parameters/idParam" - name: body in: body required: true schema: - $ref: '#/definitions/OrgConfigCreateRequest' + $ref: "#/definitions/OrgConfigCreateRequest" delete: tags: - orgConfig @@ -2791,30 +2869,30 @@ paths: security: - Bearer: [] parameters: - - $ref: '#/parameters/idParam' + - $ref: "#/parameters/idParam" responses: - '204': + "204": description: Organization config successfully removed - '400': + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: If organization config is not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" /projects/metadata/workManagementPermission: get: tags: @@ -2834,24 +2912,24 @@ paths: Url encoded list of Supported filters - projectTemplateId (required) responses: - '200': + "200": description: A list of work management permissions schema: type: array items: - $ref: '#/definitions/WorkManagementPermission' - '401': + $ref: "#/definitions/WorkManagementPermission" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" post: tags: - workManagementPermission @@ -2866,59 +2944,60 @@ paths: name: body required: true schema: - $ref: '#/definitions/WorkManagementPermissionCreateRequest' + $ref: "#/definitions/WorkManagementPermissionCreateRequest" responses: - '200': + "200": description: Returns the newly created work management permission schema: - $ref: '#/definitions/WorkManagementPermission' - '400': + $ref: "#/definitions/WorkManagementPermission" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/metadata/workManagementPermission/{id}': + $ref: "#/definitions/ErrorModel" + "/projects/metadata/workManagementPermission/{id}": get: tags: - workManagementPermission - description: Retrieve work management permission by id. Only admin or connect admin can access + description: >- + Retrieve work management permission by id. Only admin or connect admin can access this endpoint. security: - Bearer: [] responses: - '200': + "200": description: a project type schema: - $ref: '#/definitions/WorkManagementPermission' - '401': + $ref: "#/definitions/WorkManagementPermission" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/permissionIdParam' + - $ref: "#/parameters/permissionIdParam" operationId: getWorkManagementPermission patch: tags: @@ -2930,37 +3009,37 @@ paths: Update a work management permission. Only admin or connect admin can access this endpoint. responses: - '200': + "200": description: Successfully updated work management permission. schema: - $ref: '#/definitions/WorkManagementPermission' - '400': + $ref: "#/definitions/WorkManagementPermission" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/permissionIdParam' + - $ref: "#/parameters/permissionIdParam" - name: body in: body required: true schema: - $ref: '#/definitions/WorkManagementPermissionCreateRequest' + $ref: "#/definitions/WorkManagementPermissionCreateRequest" delete: tags: - workManagementPermission @@ -2970,28 +3049,28 @@ paths: security: - Bearer: [] parameters: - - $ref: '#/parameters/permissionIdParam' + - $ref: "#/parameters/permissionIdParam" responses: - '204': + "204": description: Work management permission successfully removed - '401': + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: If work management permission is not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" - '/projects/{projectId}/permissions': + "/projects/{projectId}/permissions": get: tags: - permissions @@ -2999,33 +3078,33 @@ paths: security: - Bearer: [] responses: - '200': + "200": description: permissions schema: title: Single work management permission response object type: object example: - 'work.create': true - 'workItem.edit': true + "work.create": true + "workItem.edit": true - '401': + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/projectIdParam' + - $ref: "#/parameters/projectIdParam" operationId: getPermissions /timelines: get: @@ -3047,28 +3126,28 @@ paths: in: query description: the reference id filter responses: - '200': + "200": description: A list of timelines schema: type: array items: - $ref: '#/definitions/Timeline' - '401': + $ref: "#/definitions/Timeline" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" post: tags: - timeline @@ -3083,29 +3162,29 @@ paths: name: body required: true schema: - $ref: '#/definitions/TimelineRequest' + $ref: "#/definitions/TimelineRequest" responses: - '200': + "200": description: Returns the newly created timeline schema: - $ref: '#/definitions/Timeline' - '401': + $ref: "#/definitions/Timeline" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/timelines/{timelineId}': + $ref: "#/definitions/ErrorModel" + "/timelines/{timelineId}": get: tags: - timeline @@ -3115,32 +3194,32 @@ paths: security: - Bearer: [] responses: - '200': + "200": description: a timeline schema: - $ref: '#/definitions/Timeline' - '401': + $ref: "#/definitions/Timeline" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/timelineIdParam' + - $ref: "#/parameters/timelineIdParam" operationId: getTimeline patch: tags: @@ -3152,37 +3231,37 @@ paths: Update a timeline. All users who can edit the project can access this endpoint. responses: - '200': + "200": description: Successfully updated timeline. schema: - $ref: '#/definitions/Timeline' - '401': + $ref: "#/definitions/Timeline" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - - $ref: '#/parameters/timelineIdParam' + - $ref: "#/parameters/timelineIdParam" - name: body in: body required: true schema: - $ref: '#/definitions/TimelineRequest' + $ref: "#/definitions/TimelineRequest" delete: tags: - timeline @@ -3192,33 +3271,33 @@ paths: security: - Bearer: [] parameters: - - $ref: '#/parameters/timelineIdParam' + - $ref: "#/parameters/timelineIdParam" responses: - '204': + "204": description: Timeline successfully removed - '401': + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/timelines/{timelineId}/milestones': + $ref: "#/definitions/ErrorModel" + "/timelines/{timelineId}/milestones": parameters: - - $ref: '#/parameters/timelineIdParam' + - $ref: "#/parameters/timelineIdParam" get: tags: - milestone @@ -3235,28 +3314,28 @@ paths: in: query type: string responses: - '200': + "200": description: A list of milestones schema: type: array items: - $ref: '#/definitions/Milestone' - '401': + $ref: "#/definitions/Milestone" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" post: tags: - milestone @@ -3273,28 +3352,28 @@ paths: name: body required: true schema: - $ref: '#/definitions/MilestonePostRequest' + $ref: "#/definitions/MilestonePostRequest" responses: - '200': + "200": description: Returns the newly created milestone schema: - $ref: '#/definitions/Milestone' - '401': + $ref: "#/definitions/Milestone" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" patch: tags: - milestone @@ -3313,36 +3392,36 @@ paths: schema: type: array items: - $ref: '#/definitions/Milestone' + $ref: "#/definitions/Milestone" responses: - '200': + "200": description: Aggregation of bulk operations schema: - $ref: '#/definitions/BulkMilestoneUpdateResponse' - '401': + $ref: "#/definitions/BulkMilestoneUpdateResponse" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/timelines/{timelineId}/milestones/{milestoneId}': + $ref: "#/definitions/ErrorModel" + "/timelines/{timelineId}/milestones/{milestoneId}": parameters: - - $ref: '#/parameters/timelineIdParam' - - $ref: '#/parameters/milestoneIdParam' + - $ref: "#/parameters/timelineIdParam" + - $ref: "#/parameters/milestoneIdParam" get: tags: - milestone @@ -3352,30 +3431,30 @@ paths: security: - Bearer: [] responses: - '200': + "200": description: a milestone schema: - $ref: '#/definitions/Milestone' - '401': + $ref: "#/definitions/Milestone" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" operationId: getMilestone patch: tags: @@ -3389,30 +3468,30 @@ paths: existing fields, or add new if the fields don't exist in the JSON object. responses: - '200': + "200": description: Successfully updated milestone. schema: - $ref: '#/definitions/Milestone' - '401': + $ref: "#/definitions/Milestone" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - name: body in: body @@ -3420,7 +3499,7 @@ paths: schema: type: array items: - $ref: '#/definitions/MilestonePatchRequest' + $ref: "#/definitions/MilestonePatchRequest" delete: tags: - milestone @@ -3430,28 +3509,28 @@ paths: security: - Bearer: [] responses: - '204': + "204": description: Milestone successfully removed - '401': + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" /timelines/metadata/milestoneTemplates: get: tags: @@ -3479,24 +3558,24 @@ paths: in: query type: string responses: - '200': + "200": description: A list of milestone templates schema: type: array items: - $ref: '#/definitions/MilestoneTemplate' - '401': + $ref: "#/definitions/MilestoneTemplate" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" post: tags: - milestoneTemplates @@ -3513,28 +3592,28 @@ paths: name: body required: true schema: - $ref: '#/definitions/MilestoneTemplate' + $ref: "#/definitions/MilestoneTemplate" responses: - '200': + "200": description: Returns the newly created milestone template schema: - $ref: '#/definitions/MilestoneTemplate' - '401': + $ref: "#/definitions/MilestoneTemplate" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" /timelines/metadata/milestoneTemplates/clone: post: tags: @@ -3550,37 +3629,37 @@ paths: name: body required: true schema: - $ref: '#/definitions/MilestoneCloneTemplateRequest' + $ref: "#/definitions/MilestoneCloneTemplateRequest" responses: - '200': + "200": description: Returns the list of cloned milestone templates schema: type: array items: - $ref: '#/definitions/MilestoneTemplate' - '401': + $ref: "#/definitions/MilestoneTemplate" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/timelines/metadata/milestoneTemplates/{milestoneTemplateId}': + $ref: "#/definitions/ErrorModel" + "/timelines/metadata/milestoneTemplates/{milestoneTemplateId}": parameters: - - $ref: '#/parameters/milestoneTemplateIdParam' + - $ref: "#/parameters/milestoneTemplateIdParam" get: tags: - milestoneTemplates @@ -3590,26 +3669,26 @@ paths: security: - Bearer: [] responses: - '200': + "200": description: a milestone template schema: - $ref: '#/definitions/MilestoneTemplate' - '401': + $ref: "#/definitions/MilestoneTemplate" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" operationId: getMilestoneTemplate patch: tags: @@ -3621,36 +3700,36 @@ paths: Update a milestone template. Only connect manager, connect admin, and admin can access this endpoint. responses: - '200': + "200": description: Successfully updated milestone template. schema: - $ref: '#/definitions/MilestoneTemplate' - '401': + $ref: "#/definitions/MilestoneTemplate" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: - name: body in: body required: true schema: - $ref: '#/definitions/MilestoneTemplate' + $ref: "#/definitions/MilestoneTemplate" delete: tags: - milestoneTemplates @@ -3660,29 +3739,29 @@ paths: security: - Bearer: [] responses: - '204': + "204": description: Milestone template successfully removed - '401': + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not found schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/{projectId}/invites': + $ref: "#/definitions/ErrorModel" + "/projects/{projectId}/invites": get: tags: - project member invite @@ -3694,20 +3773,20 @@ paths: Otherwise user can only see his/her own invitation in this project. If user has no invitation in this project or this project doesn't exist, an empty array will be returned. parameters: - - $ref: '#/parameters/projectIdParam' + - $ref: "#/parameters/projectIdParam" responses: - '200': + "200": description: The invite for current user schema: - $ref: '#/definitions/ProjectMemberInviteListResult' - '403': + $ref: "#/definitions/ProjectMemberInviteListResult" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" post: tags: - project member invite @@ -3718,30 +3797,30 @@ paths: Create an invite. All users who can access this endpoint, however more restriction will be applied based on role to be added. parameters: - - $ref: '#/parameters/projectIdParam' + - $ref: "#/parameters/projectIdParam" - in: body name: body required: true schema: - $ref: '#/definitions/AddProjectMemberInvitesRequest' + $ref: "#/definitions/AddProjectMemberInvitesRequest" responses: - '201': + "201": description: Created schema: - $ref: '#/definitions/ProjectMemberInviteSuccessAndFailure' - '400': + $ref: "#/definitions/ProjectMemberInviteSuccessAndFailure" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/{projectId}/invites/{inviteId}': + $ref: "#/definitions/ErrorModel" + "/projects/{projectId}/invites/{inviteId}": get: tags: - project member invite @@ -3753,25 +3832,25 @@ paths: User got invited by this inviteId can also see this invitation. If project/invitation doesn't exist, or this invitation is not for logged-in user, it will return 404 response. parameters: - - $ref: '#/parameters/projectIdParam' - - $ref: '#/parameters/inviteIdParam' + - $ref: "#/parameters/projectIdParam" + - $ref: "#/parameters/inviteIdParam" responses: - '200': + "200": description: Returns the newly updated invite schema: - $ref: '#/definitions/ProjectMemberInvite' - '403': + $ref: "#/definitions/ProjectMemberInvite" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not Found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" patch: tags: - project member invite @@ -3782,34 +3861,34 @@ paths: Update an invite. All users who can access this endpoint, however more restriction will be applied based on role to be updated. parameters: - - $ref: '#/parameters/projectIdParam' - - $ref: '#/parameters/inviteIdParam' + - $ref: "#/parameters/projectIdParam" + - $ref: "#/parameters/inviteIdParam" - in: body name: body required: true schema: - $ref: '#/definitions/UpdateProjectMemberInviteRequest' + $ref: "#/definitions/UpdateProjectMemberInviteRequest" responses: - '200': + "200": description: Returns the newly updated invite schema: - $ref: '#/definitions/ProjectMemberInvite' - '400': + $ref: "#/definitions/ProjectMemberInvite" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not Found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" delete: tags: - project member invite @@ -3820,29 +3899,29 @@ paths: Cancel an invite. All users who can access this endpoint, however more restriction will be applied based on role to be cancelled. parameters: - - $ref: '#/parameters/projectIdParam' - - $ref: '#/parameters/inviteIdParam' + - $ref: "#/parameters/projectIdParam" + - $ref: "#/parameters/inviteIdParam" responses: - '204': + "204": description: Cancel success - '400': + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '403': + $ref: "#/definitions/ErrorModel" + "403": description: Forbidden schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Not Found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" - '/projects/metadata/form/{key}': + "/projects/metadata/form/{key}": get: tags: - form version @@ -3850,29 +3929,29 @@ paths: - Bearer: [] description: get the latest revision of latest version for key. parameters: - - $ref: '#/parameters/modelKeyParam' + - $ref: "#/parameters/modelKeyParam" responses: - '200': + "200": description: The model for the latest revision of latest version schema: - $ref: '#/definitions/Form' - '400': + $ref: "#/definitions/Form" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: key not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/metadata/form/{key}/versions': + $ref: "#/definitions/ErrorModel" + "/projects/metadata/form/{key}/versions": get: tags: - form version @@ -3880,30 +3959,30 @@ paths: - Bearer: [] description: get all versions for key. parameters: - - $ref: '#/parameters/modelKeyParam' + - $ref: "#/parameters/modelKeyParam" responses: - '200': + "200": description: The model list for the all version schema: type: array items: - $ref: '#/definitions/Form' - '401': + $ref: "#/definitions/Form" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: key not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" post: tags: - form version @@ -3911,34 +3990,34 @@ paths: - Bearer: [] description: create version for key parameters: - - $ref: '#/parameters/modelKeyParam' + - $ref: "#/parameters/modelKeyParam" - in: body name: body required: true schema: - $ref: '#/definitions/NewForm' + $ref: "#/definitions/NewForm" responses: - '200': + "200": description: The model created schema: - $ref: '#/definitions/Form' - '400': + $ref: "#/definitions/Form" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: key not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/metadata/form/{key}/versions/{version}': + $ref: "#/definitions/ErrorModel" + "/projects/metadata/form/{key}/versions/{version}": get: tags: - form version @@ -3946,29 +4025,29 @@ paths: - Bearer: [] description: get particular version for key. parameters: - - $ref: '#/parameters/modelKeyParam' - - $ref: '#/parameters/modelVersionParam' + - $ref: "#/parameters/modelKeyParam" + - $ref: "#/parameters/modelVersionParam" responses: - '200': + "200": description: The model for the particular version schema: - $ref: '#/definitions/Form' - '400': + $ref: "#/definitions/Form" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: key not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" patch: tags: - form version @@ -3976,34 +4055,34 @@ paths: - Bearer: [] description: update version for key parameters: - - $ref: '#/parameters/modelKeyParam' - - $ref: '#/parameters/modelVersionParam' + - $ref: "#/parameters/modelKeyParam" + - $ref: "#/parameters/modelVersionParam" - in: body name: body required: true schema: - $ref: '#/definitions/NewForm' + $ref: "#/definitions/NewForm" responses: - '200': + "200": description: The model updated schema: - $ref: '#/definitions/Form' - '401': + $ref: "#/definitions/Form" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: key not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" delete: tags: - form version @@ -4011,28 +4090,28 @@ paths: - Bearer: [] description: delete version for key parameters: - - $ref: '#/parameters/modelKeyParam' - - $ref: '#/parameters/modelVersionParam' + - $ref: "#/parameters/modelKeyParam" + - $ref: "#/parameters/modelVersionParam" responses: - '204': + "204": description: Delete succuessful - '401': + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '400': + $ref: "#/definitions/ErrorModel" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: key not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/metadata/form/{key}/versions/{version}/revisions': + $ref: "#/definitions/ErrorModel" + "/projects/metadata/form/{key}/versions/{version}/revisions": get: tags: - form revision @@ -4040,31 +4119,31 @@ paths: - Bearer: [] description: get all revision for version. parameters: - - $ref: '#/parameters/modelKeyParam' - - $ref: '#/parameters/modelVersionParam' + - $ref: "#/parameters/modelKeyParam" + - $ref: "#/parameters/modelVersionParam" responses: - '200': + "200": description: The model for the particular version schema: type: array items: - $ref: '#/definitions/Form' - '400': + $ref: "#/definitions/Form" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Model not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" post: tags: - form revision @@ -4072,35 +4151,35 @@ paths: - Bearer: [] description: create revision for key parameters: - - $ref: '#/parameters/modelKeyParam' - - $ref: '#/parameters/modelVersionParam' + - $ref: "#/parameters/modelKeyParam" + - $ref: "#/parameters/modelVersionParam" - in: body name: body required: true schema: - $ref: '#/definitions/NewForm' + $ref: "#/definitions/NewForm" responses: - '200': + "200": description: The model created schema: - $ref: '#/definitions/Form' - '400': + $ref: "#/definitions/Form" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Model not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/metadata/form/{key}/versions/{version}/revisions/{revision}': + $ref: "#/definitions/ErrorModel" + "/projects/metadata/form/{key}/versions/{version}/revisions/{revision}": get: tags: - form revision @@ -4108,30 +4187,30 @@ paths: - Bearer: [] description: get particular revision for key. parameters: - - $ref: '#/parameters/modelKeyParam' - - $ref: '#/parameters/modelVersionParam' - - $ref: '#/parameters/modelRevisionParam' + - $ref: "#/parameters/modelKeyParam" + - $ref: "#/parameters/modelVersionParam" + - $ref: "#/parameters/modelRevisionParam" responses: - '200': + "200": description: The model for the particular version schema: - $ref: '#/definitions/Form' - '400': + $ref: "#/definitions/Form" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Model not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" delete: tags: - form revision @@ -4139,30 +4218,30 @@ paths: - Bearer: [] description: delete particular revision parameters: - - $ref: '#/parameters/modelKeyParam' - - $ref: '#/parameters/modelVersionParam' - - $ref: '#/parameters/modelRevisionParam' + - $ref: "#/parameters/modelKeyParam" + - $ref: "#/parameters/modelVersionParam" + - $ref: "#/parameters/modelRevisionParam" responses: - '204': + "204": description: Delete succuessful - '400': + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Model not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" - '/projects/metadata/priceConfig/{key}': + "/projects/metadata/priceConfig/{key}": get: tags: - priceConfig version @@ -4170,29 +4249,29 @@ paths: - Bearer: [] description: get the latest revision of latest version for key. parameters: - - $ref: '#/parameters/modelKeyParam' + - $ref: "#/parameters/modelKeyParam" responses: - '200': + "200": description: The model for the latest revision of latest version schema: - $ref: '#/definitions/PriceConfig' - '400': + $ref: "#/definitions/PriceConfig" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Model not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/metadata/priceConfig/{key}/versions': + $ref: "#/definitions/ErrorModel" + "/projects/metadata/priceConfig/{key}/versions": get: tags: - priceConfig version @@ -4200,30 +4279,30 @@ paths: - Bearer: [] description: get all versions for key. parameters: - - $ref: '#/parameters/modelKeyParam' + - $ref: "#/parameters/modelKeyParam" responses: - '200': + "200": description: The model list for the all version schema: type: array items: - $ref: '#/definitions/PriceConfig' - '400': + $ref: "#/definitions/PriceConfig" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Model not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" post: tags: - priceConfig version @@ -4231,30 +4310,30 @@ paths: - Bearer: [] description: create version for key parameters: - - $ref: '#/parameters/modelKeyParam' + - $ref: "#/parameters/modelKeyParam" - in: body name: body required: true schema: - $ref: '#/definitions/NewPriceConfig' + $ref: "#/definitions/NewPriceConfig" responses: - '200': + "200": description: The model created schema: - $ref: '#/definitions/PriceConfig' - '400': + $ref: "#/definitions/PriceConfig" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/metadata/priceConfig/{key}/versions/{version}': + $ref: "#/definitions/ErrorModel" + "/projects/metadata/priceConfig/{key}/versions/{version}": get: tags: - priceConfig version @@ -4262,29 +4341,29 @@ paths: - Bearer: [] description: get particular version for key. parameters: - - $ref: '#/parameters/modelKeyParam' - - $ref: '#/parameters/modelVersionParam' + - $ref: "#/parameters/modelKeyParam" + - $ref: "#/parameters/modelVersionParam" responses: - '200': + "200": description: The model for the particular version schema: - $ref: '#/definitions/PriceConfig' - '400': + $ref: "#/definitions/PriceConfig" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Model not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" patch: tags: - priceConfig version @@ -4292,34 +4371,34 @@ paths: - Bearer: [] description: update version for key parameters: - - $ref: '#/parameters/modelKeyParam' - - $ref: '#/parameters/modelVersionParam' + - $ref: "#/parameters/modelKeyParam" + - $ref: "#/parameters/modelVersionParam" - in: body name: body required: true schema: - $ref: '#/definitions/NewPriceConfig' + $ref: "#/definitions/NewPriceConfig" responses: - '200': + "200": description: The model updated schema: - $ref: '#/definitions/PriceConfig' - '400': + $ref: "#/definitions/PriceConfig" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Model not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" delete: tags: - priceConfig version @@ -4327,28 +4406,28 @@ paths: - Bearer: [] description: delete version for key parameters: - - $ref: '#/parameters/modelKeyParam' - - $ref: '#/parameters/modelVersionParam' + - $ref: "#/parameters/modelKeyParam" + - $ref: "#/parameters/modelVersionParam" responses: - '204': + "204": description: Delete succuessful - '400': + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Model not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/metadata/priceConfig/{key}/versions/{version}/revisions': + $ref: "#/definitions/ErrorModel" + "/projects/metadata/priceConfig/{key}/versions/{version}/revisions": get: tags: - priceConfig revision @@ -4356,31 +4435,31 @@ paths: - Bearer: [] description: get all revision for version. parameters: - - $ref: '#/parameters/modelKeyParam' - - $ref: '#/parameters/modelVersionParam' + - $ref: "#/parameters/modelKeyParam" + - $ref: "#/parameters/modelVersionParam" responses: - '200': + "200": description: The model for the particular version schema: type: array items: - $ref: '#/definitions/PriceConfig' - '400': + $ref: "#/definitions/PriceConfig" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Model not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" post: tags: - priceConfig revision @@ -4388,35 +4467,35 @@ paths: - Bearer: [] description: create revision for key parameters: - - $ref: '#/parameters/modelKeyParam' - - $ref: '#/parameters/modelVersionParam' + - $ref: "#/parameters/modelKeyParam" + - $ref: "#/parameters/modelVersionParam" - in: body name: body required: true schema: - $ref: '#/definitions/NewPriceConfig' + $ref: "#/definitions/NewPriceConfig" responses: - '200': + "200": description: The model created schema: - $ref: '#/definitions/PriceConfig' - '400': + $ref: "#/definitions/PriceConfig" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Model not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/metadata/priceConfig/{key}/versions/{version}/revisions/{revision}': + $ref: "#/definitions/ErrorModel" + "/projects/metadata/priceConfig/{key}/versions/{version}/revisions/{revision}": get: tags: - priceConfig revision @@ -4424,30 +4503,30 @@ paths: - Bearer: [] description: get particular revision for key. parameters: - - $ref: '#/parameters/modelKeyParam' - - $ref: '#/parameters/modelVersionParam' - - $ref: '#/parameters/modelRevisionParam' + - $ref: "#/parameters/modelKeyParam" + - $ref: "#/parameters/modelVersionParam" + - $ref: "#/parameters/modelRevisionParam" responses: - '200': + "200": description: The model for the particular version schema: - $ref: '#/definitions/PriceConfig' - '400': + $ref: "#/definitions/PriceConfig" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Model not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" delete: tags: - priceConfig revision @@ -4455,30 +4534,30 @@ paths: - Bearer: [] description: delete particular revision parameters: - - $ref: '#/parameters/modelKeyParam' - - $ref: '#/parameters/modelVersionParam' - - $ref: '#/parameters/modelRevisionParam' + - $ref: "#/parameters/modelKeyParam" + - $ref: "#/parameters/modelVersionParam" + - $ref: "#/parameters/modelRevisionParam" responses: - '204': + "204": description: Delete succuessful - '400': + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Model not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" - '/projects/metadata/planConfig/{key}': + "/projects/metadata/planConfig/{key}": get: tags: - planConfig version @@ -4486,29 +4565,29 @@ paths: - Bearer: [] description: get the latest revision of latest version for key. parameters: - - $ref: '#/parameters/modelKeyParam' + - $ref: "#/parameters/modelKeyParam" responses: - '200': + "200": description: The model for the latest revision of latest version schema: - $ref: '#/definitions/PlanConfig' - '400': + $ref: "#/definitions/PlanConfig" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Model not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/metadata/planConfig/{key}/versions': + $ref: "#/definitions/ErrorModel" + "/projects/metadata/planConfig/{key}/versions": get: tags: - planConfig version @@ -4516,30 +4595,30 @@ paths: - Bearer: [] description: get all versions for key. parameters: - - $ref: '#/parameters/modelKeyParam' + - $ref: "#/parameters/modelKeyParam" responses: - '200': + "200": description: The model list for the all version schema: type: array items: - $ref: '#/definitions/PlanConfig' - '400': + $ref: "#/definitions/PlanConfig" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Model not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" post: tags: - planConfig version @@ -4547,30 +4626,30 @@ paths: - Bearer: [] description: create version for key parameters: - - $ref: '#/parameters/modelKeyParam' + - $ref: "#/parameters/modelKeyParam" - in: body name: body required: true schema: - $ref: '#/definitions/NewPlanConfig' + $ref: "#/definitions/NewPlanConfig" responses: - '200': + "200": description: The model created schema: - $ref: '#/definitions/PlanConfig' - '400': + $ref: "#/definitions/PlanConfig" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/metadata/planConfig/{key}/versions/{version}': + $ref: "#/definitions/ErrorModel" + "/projects/metadata/planConfig/{key}/versions/{version}": get: tags: - planConfig version @@ -4578,29 +4657,29 @@ paths: - Bearer: [] description: get particular version for key. parameters: - - $ref: '#/parameters/modelKeyParam' - - $ref: '#/parameters/modelVersionParam' + - $ref: "#/parameters/modelKeyParam" + - $ref: "#/parameters/modelVersionParam" responses: - '200': + "200": description: The model for the particular version schema: - $ref: '#/definitions/PlanConfig' - '400': + $ref: "#/definitions/PlanConfig" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Model not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" patch: tags: - planConfig version @@ -4608,34 +4687,34 @@ paths: - Bearer: [] description: update version for key parameters: - - $ref: '#/parameters/modelKeyParam' - - $ref: '#/parameters/modelVersionParam' + - $ref: "#/parameters/modelKeyParam" + - $ref: "#/parameters/modelVersionParam" - in: body name: body required: true schema: - $ref: '#/definitions/NewPlanConfig' + $ref: "#/definitions/NewPlanConfig" responses: - '200': + "200": description: The model updated schema: - $ref: '#/definitions/PlanConfig' - '400': + $ref: "#/definitions/PlanConfig" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Model not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" delete: tags: - planConfig version @@ -4643,28 +4722,28 @@ paths: - Bearer: [] description: delete version for key parameters: - - $ref: '#/parameters/modelKeyParam' - - $ref: '#/parameters/modelVersionParam' + - $ref: "#/parameters/modelKeyParam" + - $ref: "#/parameters/modelVersionParam" responses: - '204': + "204": description: Delete succuessful - '400': + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Model not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/metadata/planConfig/{key}/versions/{version}/revisions': + $ref: "#/definitions/ErrorModel" + "/projects/metadata/planConfig/{key}/versions/{version}/revisions": get: tags: - planConfig revision @@ -4672,31 +4751,31 @@ paths: - Bearer: [] description: get all revision for version. parameters: - - $ref: '#/parameters/modelKeyParam' - - $ref: '#/parameters/modelVersionParam' + - $ref: "#/parameters/modelKeyParam" + - $ref: "#/parameters/modelVersionParam" responses: - '200': + "200": description: The model for the particular version schema: type: array items: - $ref: '#/definitions/PlanConfig' - '400': + $ref: "#/definitions/PlanConfig" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Model not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" post: tags: - planConfig revision @@ -4704,35 +4783,35 @@ paths: - Bearer: [] description: create revision for key parameters: - - $ref: '#/parameters/modelKeyParam' - - $ref: '#/parameters/modelVersionParam' + - $ref: "#/parameters/modelKeyParam" + - $ref: "#/parameters/modelVersionParam" - in: body name: body required: true schema: - $ref: '#/definitions/NewPlanConfig' + $ref: "#/definitions/NewPlanConfig" responses: - '200': + "200": description: The model created schema: - $ref: '#/definitions/PlanConfig' - '400': + $ref: "#/definitions/PlanConfig" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Model not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' - '/projects/metadata/planConfig/{key}/versions/{version}/revisions/{revision}': + $ref: "#/definitions/ErrorModel" + "/projects/metadata/planConfig/{key}/versions/{version}/revisions/{revision}": get: tags: - planConfig revision @@ -4740,30 +4819,30 @@ paths: - Bearer: [] description: get particular revision for key. parameters: - - $ref: '#/parameters/modelKeyParam' - - $ref: '#/parameters/modelVersionParam' - - $ref: '#/parameters/modelRevisionParam' + - $ref: "#/parameters/modelKeyParam" + - $ref: "#/parameters/modelVersionParam" + - $ref: "#/parameters/modelRevisionParam" responses: - '200': + "200": description: The model for the particular version schema: - $ref: '#/definitions/PlanConfig' - '400': + $ref: "#/definitions/PlanConfig" + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Model not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" delete: tags: - planConfig revision @@ -4771,28 +4850,28 @@ paths: - Bearer: [] description: delete particular revision parameters: - - $ref: '#/parameters/modelKeyParam' - - $ref: '#/parameters/modelVersionParam' - - $ref: '#/parameters/modelRevisionParam' + - $ref: "#/parameters/modelKeyParam" + - $ref: "#/parameters/modelVersionParam" + - $ref: "#/parameters/modelRevisionParam" responses: - '204': + "204": description: Delete succuessful - '400': + "400": description: Bad request schema: - $ref: '#/definitions/ErrorModel' - '401': + $ref: "#/definitions/ErrorModel" + "401": description: Unauthorized schema: - $ref: '#/definitions/ErrorModel' - '404': + $ref: "#/definitions/ErrorModel" + "404": description: Model not found schema: - $ref: '#/definitions/ErrorModel' - '500': + $ref: "#/definitions/ErrorModel" + "500": description: Internal Server Error schema: - $ref: '#/definitions/ErrorModel' + $ref: "#/definitions/ErrorModel" parameters: projectIdParam: @@ -5071,7 +5150,7 @@ definitions: type: string external: type: object - description: 'READ-ONLY, OPTIONAL. Refernce to external task/issue.' + description: "READ-ONLY, OPTIONAL. Refernce to external task/issue." properties: id: type: string @@ -5119,14 +5198,14 @@ definitions: bookmarks: type: array items: - $ref: '#/definitions/ProjectBookMark' + $ref: "#/definitions/ProjectBookMark" challengeEligibility: description: List of eligibility criteria (one entry per role) type: array items: - $ref: '#/definitions/ChallengeEligibility' + $ref: "#/definitions/ChallengeEligibility" details: - $ref: '#/definitions/ProjectDetails' + $ref: "#/definitions/ProjectDetails" utm: description: READ-ONLY. Used for tracking type: object @@ -5211,7 +5290,7 @@ definitions: description: Project description external: type: object - description: 'READ-ONLY, OPTIONAL. Refernce to external task/issue.' + description: "READ-ONLY, OPTIONAL. Refernce to external task/issue." properties: id: type: string @@ -5243,32 +5322,32 @@ definitions: - completed cancelReason: type: string - description: 'If a project is cancelled, define the reason of cancellation' + description: "If a project is cancelled, define the reason of cancellation" challengeEligibility: description: List of eligibility criteria (one entry per role) type: array items: - $ref: '#/definitions/ChallengeEligibility' + $ref: "#/definitions/ChallengeEligibility" bookmarks: type: array items: - $ref: '#/definitions/ProjectBookMark' + $ref: "#/definitions/ProjectBookMark" members: description: | READ-ONLY. List of project members. Use project member api to add/remove members type: array items: - $ref: '#/definitions/ProjectMember' + $ref: "#/definitions/ProjectMember" attachments: description: | READ-ONLY. List of project attachmens. Use project attachment api to add/remove attachments type: array items: - $ref: '#/definitions/ProjectAttachment' + $ref: "#/definitions/ProjectAttachment" details: - $ref: '#/definitions/ProjectDetails' + $ref: "#/definitions/ProjectDetails" templateId: description: the project template identifier type: number @@ -5386,8 +5465,8 @@ definitions: type: string description: The attachment type, one of 'link' or 'file' enum: - - link - - file + - link + - file tags: type: array description: The attachment tags @@ -5453,18 +5532,18 @@ definitions: type: string description: The attachment type, one of 'file' or 'link' enum: - - link - - file + - link + - file tags: type: array description: The attachment tags array items: - type: string + type: string allowedUsers: type: array description: The array of ids of the users allowed to access this attachment items: - type: number + type: number path: type: string description: The attachment path @@ -5578,27 +5657,27 @@ definitions: type: object description: the project template phases form: - $ref: '#/definitions/VersionModelParam' + $ref: "#/definitions/VersionModelParam" priceConfig: - $ref: '#/definitions/VersionModelParam' + $ref: "#/definitions/VersionModelParam" planConfig: - $ref: '#/definitions/VersionModelParam' + $ref: "#/definitions/VersionModelParam" ProjectTemplateUpgradeBody: title: Project template type: object properties: form: - $ref: '#/definitions/VersionModelParam' + $ref: "#/definitions/VersionModelParam" priceConfig: - $ref: '#/definitions/VersionModelParam' + $ref: "#/definitions/VersionModelParam" planConfig: - $ref: '#/definitions/VersionModelParam' + $ref: "#/definitions/VersionModelParam" ProductTemplateUpgradeBody: title: Product template type: object properties: form: - $ref: '#/definitions/VersionModelParam' + $ref: "#/definitions/VersionModelParam" VersionModelParam: title: version model param type: object @@ -5642,7 +5721,7 @@ definitions: format: int64 description: READ-ONLY. User that last updated this object readOnly: true - - $ref: '#/definitions/ProjectTemplateRequest' + - $ref: "#/definitions/ProjectTemplateRequest" ProductTemplateRequest: title: Product template request object type: object @@ -5681,7 +5760,7 @@ definitions: type: object description: the product template template form: - $ref: '#/definitions/VersionModelParam' + $ref: "#/definitions/VersionModelParam" isAddOn: type: boolean description: the flag that shows if the product template is an add on @@ -5722,8 +5801,20 @@ definitions: category: type: string description: The product category of the product template - - $ref: '#/definitions/ProductTemplateRequest' + - $ref: "#/definitions/ProductTemplateRequest" ProjectPhaseRequest: + title: Project phase request object + allOf: + - $ref: "#/definitions/ProjectPhaseRequestBase" + - type: object + properties: + members: + type: array + items: + type: integer + format: int64 + description: "The user id." + ProjectPhaseRequestBase: title: Project phase request object type: object required: @@ -5798,7 +5889,36 @@ definitions: format: int64 description: READ-ONLY. User that last updated this object readOnly: true - - $ref: '#/definitions/ProjectPhaseRequest' + - $ref: "#/definitions/ProjectPhaseRequestBase" + - type: object + properties: + members: + type: array + items: + type: object + required: + - userId + properties: + userId: + type: integer + format: int64 + handle: + type: string + photoURL: + type: string + format: url + - type: object + properties: + approvals: + type: array + items: + $ref: "#/definitions/PhaseApproval" + - type: object + properties: + products: + type: array + items: + $ref: "#/definitions/PhaseProduct" PhaseProductRequest: title: Phase product request object type: object @@ -5860,7 +5980,7 @@ definitions: format: int64 description: READ-ONLY. User that last updated this object readOnly: true - - $ref: '#/definitions/PhaseProductRequest' + - $ref: "#/definitions/PhaseProductRequest" ProductCategoryRequest: title: Product category request object type: object @@ -5881,7 +6001,7 @@ definitions: key: type: string description: the product category key - - $ref: '#/definitions/ProductCategoryRequest' + - $ref: "#/definitions/ProductCategoryRequest" ProductCategory: title: Product category object allOf: @@ -5913,14 +6033,23 @@ definitions: format: int64 description: READ-ONLY. User that last updated this object readOnly: true - - $ref: '#/definitions/ProductCategoryCreateRequest' + - $ref: "#/definitions/ProductCategoryCreateRequest" PhaseMember: - title: Phase member object + title: Phase members object type: object required: + - id - phaseId - userId + - createdAt + - createdBy + - updatedAt + - updatedBy properties: + id: + type: integer + format: int64 + description: phase approval id phaseId: type: integer format: int64 @@ -5929,13 +6058,95 @@ definitions: type: integer format: int64 description: references to member's userId + createdAt: + type: string + description: Datetime (GMT) when object was created + createdBy: + type: integer + format: int64 + description: User who created this object + updatedAt: + type: string + description: Datetime (GMT) when object was updated + updatedBy: + type: integer + format: int64 + description: User that last updated this object NewPhaseMember: title: New Phase members to Update - type: array - items: - type: integer - format: int64 - description: "The user id." + type: object + properties: + userIds: + type: array + items: + type: integer + format: int64 + description: "The user id." + PhaseApproval: + title: Phase approval object + allOf: + - type: object + required: + - id + - phaseId + - decision + - comment + - startDate + - expectedEndDate + - createdAt + - createdBy + - updatedAt + - updatedBy + properties: + id: + type: integer + format: int64 + description: phase approval id + phaseId: + type: integer + format: int64 + description: references to project phase id + createdAt: + type: string + description: Datetime (GMT) when object was created + createdBy: + type: integer + format: int64 + description: User who created this object + updatedAt: + type: string + description: Datetime (GMT) when object was updated + updatedBy: + type: integer + format: int64 + description: User that last updated this object + - $ref: "#/definitions/NewPhaseApproval" + NewPhaseApproval: + title: New Phase members to Update + type: object + required: + - decision + - comment + - startDate + - expectedEndDate + properties: + decision: + type: string + enum: + - approve + - reject + comment: + type: string + maxLength: 255 + startDate: + type: string + format: date + endDate: + type: string + format: date + expectedEndDate: + type: string + format: date ProjectTypeRequest: title: Project type request object type: object @@ -5956,7 +6167,7 @@ definitions: key: type: string description: the project type key - - $ref: '#/definitions/ProjectTypeRequest' + - $ref: "#/definitions/ProjectTypeRequest" ProjectType: title: Project type object allOf: @@ -5988,7 +6199,7 @@ definitions: format: int64 description: READ-ONLY. User that last updated this object readOnly: true - - $ref: '#/definitions/ProjectTypeCreateRequest' + - $ref: "#/definitions/ProjectTypeCreateRequest" OrgConfigRequest: title: Organization config request object type: object @@ -6011,7 +6222,7 @@ definitions: configValue: type: string description: the organization config id - - $ref: '#/definitions/OrgConfigRequest' + - $ref: "#/definitions/OrgConfigRequest" OrgConfig: title: Organization config object allOf: @@ -6056,7 +6267,7 @@ definitions: format: int64 description: READ-ONLY. User that last updated this object readOnly: true - - $ref: '#/definitions/OrgConfigCreateRequest' + - $ref: "#/definitions/OrgConfigCreateRequest" TimelineRequest: title: Timeline request object type: object @@ -6125,7 +6336,7 @@ definitions: format: int64 description: READ-ONLY. User that last updated this object readOnly: true - - $ref: '#/definitions/TimelineRequest' + - $ref: "#/definitions/TimelineRequest" MilestonePostRequest: title: Milestone request object type: object @@ -6261,7 +6472,7 @@ definitions: format: int64 description: the id statusHistory: - $ref: '#/definitions/StatusHistory' + $ref: "#/definitions/StatusHistory" createdAt: type: string description: Datetime (GMT) when object was created @@ -6280,12 +6491,12 @@ definitions: format: int64 description: READ-ONLY. User that last updated this object readOnly: true - - $ref: '#/definitions/MilestonePostRequest' + - $ref: "#/definitions/MilestonePostRequest" BulkMilestoneUpdateResponse: title: Bulk milestone update response object type: array items: - $ref: '#/definitions/Milestone' + $ref: "#/definitions/Milestone" MilestoneTemplateRequest: title: Milestone template request object @@ -6391,7 +6602,7 @@ definitions: format: int64 description: READ-ONLY. User that last updated this object readOnly: true - - $ref: '#/definitions/MilestoneTemplateRequest' + - $ref: "#/definitions/MilestoneTemplateRequest" AllMetadataResponse: title: All metadata response object type: object @@ -6399,27 +6610,27 @@ definitions: projectTemplates: type: array items: - $ref: '#/definitions/ProjectTemplate' + $ref: "#/definitions/ProjectTemplate" productTemplates: type: array items: - $ref: '#/definitions/ProductTemplate' + $ref: "#/definitions/ProductTemplate" milestoneTemplates: type: array items: - $ref: '#/definitions/MilestoneTemplate' + $ref: "#/definitions/MilestoneTemplate" projectTypes: type: array items: - $ref: '#/definitions/ProjectType' + $ref: "#/definitions/ProjectType" productCategories: type: array items: - $ref: '#/definitions/ProductCategory' + $ref: "#/definitions/ProductCategory" buildingBlocks: - type: array - items: - $ref: '#/definitions/BuildingBlock' + type: array + items: + $ref: "#/definitions/BuildingBlock" ProjectMemberInvite: type: object properties: @@ -6477,7 +6688,7 @@ definitions: success: type: array items: - $ref: '#/definitions/ProjectMemberInvite' + $ref: "#/definitions/ProjectMemberInvite" failed: type: array items: @@ -6495,13 +6706,13 @@ definitions: ProjectMemberInviteListResult: type: array items: - $ref: '#/definitions/ProjectMemberInvite' + $ref: "#/definitions/ProjectMemberInvite" AddProjectMemberInvitesRequest: title: Add project member invites request object type: object properties: handles: - description: 'The user handle list, could not present with emails' + description: "The user handle list, could not present with emails" type: array items: type: string @@ -6509,7 +6720,7 @@ definitions: type: array items: type: string - description: 'The user email list, could not present with handles' + description: "The user email list, could not present with handles" role: description: The target role in the project type: string @@ -6696,7 +6907,7 @@ definitions: format: int64 description: READ-ONLY. User that last updated this object readOnly: true - - $ref: '#/definitions/ProjectSettingRequest' + - $ref: "#/definitions/ProjectSettingRequest" ProjectSettingRequest: title: Project setting request object type: object @@ -6763,7 +6974,7 @@ definitions: format: int64 description: READ-ONLY. User that last updated this object readOnly: true - - $ref: '#/definitions/WorkStreamRequest' + - $ref: "#/definitions/WorkStreamRequest" WorkStreamRequest: title: Work stream request object type: object @@ -6845,7 +7056,7 @@ definitions: format: int64 description: READ-ONLY. User that last updated this object readOnly: true - - $ref: '#/definitions/WorkManagementPermissionCreateRequest' + - $ref: "#/definitions/WorkManagementPermissionCreateRequest" StatusHistory: title: Status history object type: object diff --git a/migrations/20210802_project_phase_approval_table.sql b/migrations/20210802_project_phase_approval_table.sql new file mode 100644 index 00000000..2feed0fd --- /dev/null +++ b/migrations/20210802_project_phase_approval_table.sql @@ -0,0 +1,27 @@ +CREATE SEQUENCE project_phase_approval_id_seq + INCREMENT 1 + START 1 + MINVALUE 1 + MAXVALUE 9223372036854775807 + CACHE 1; + +DROP TYPE IF EXISTS "enum_project_phase_approval_decision"; +CREATE TYPE "enum_project_phase_approval_decision" AS ENUM ('approve, reject'); + +CREATE TABLE "project_phase_approval" ( + "id" int8 NOT NULL DEFAULT nextval('project_phase_approval_id_seq'::regclass), + "phaseId" int8 NOT NULL, + "decision" "enum_project_phase_approval_decision" NOT NULL, + "comment" varchar NOT NULL, + "startDate" timestamptz NOT NULL, + "endDate" timestamptz, + "expectedEndDate" timestamptz NOT NULL, + "deletedAt" timestamptz, + "createdAt" timestamptz, + "updatedAt" timestamptz, + "deletedBy" int4, + "createdBy" int4 NOT NULL, + "updatedBy" int4 NOT NULL, + CONSTRAINT "project_phase_approval_phaseId_fkey" FOREIGN KEY ("phaseId") REFERENCES "project_phases"("id") ON DELETE CASCADE ON UPDATE CASCADE, + PRIMARY KEY ("id") +); \ No newline at end of file diff --git a/src/models/projectPhase.js b/src/models/projectPhase.js index 7c22c7fe..de30a332 100644 --- a/src/models/projectPhase.js +++ b/src/models/projectPhase.js @@ -43,6 +43,7 @@ module.exports = function defineProjectPhase(sequelize, DataTypes) { ProjectPhase.associate = (models) => { ProjectPhase.hasMany(models.PhaseProduct, { as: 'products', foreignKey: 'phaseId' }); ProjectPhase.hasMany(models.ProjectPhaseMember, { as: 'members', foreignKey: 'phaseId' }); + ProjectPhase.hasMany(models.ProjectPhaseApproval, { as: 'approvals', foreignKey: 'phaseId' }); ProjectPhase.belongsToMany(models.WorkStream, { through: models.PhaseWorkStream, foreignKey: 'phaseId' }); }; diff --git a/src/models/projectPhaseApproval.js b/src/models/projectPhaseApproval.js new file mode 100644 index 00000000..c23261db --- /dev/null +++ b/src/models/projectPhaseApproval.js @@ -0,0 +1,50 @@ +module.exports = function defineProjectPhaseApproval(sequelize, DataTypes) { + const ProjectPhaseApproval = sequelize.define('ProjectPhaseApproval', { + id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, + phaseId: { type: DataTypes.BIGINT, allowNull: false }, + decision: { type: DataTypes.ENUM, values: ['approve', 'reject'], allowNull: false }, + comment: { type: DataTypes.STRING, allowNull: false }, + startDate: { type: DataTypes.DATE, allowNull: false }, + endDate: { type: DataTypes.DATE, allowNull: true }, + expectedEndDate: { type: DataTypes.DATE, allowNull: false }, + deletedAt: { type: DataTypes.DATE, allowNull: true }, + createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + deletedBy: { type: DataTypes.INTEGER, allowNull: true }, + createdBy: { type: DataTypes.INTEGER, allowNull: false }, + updatedBy: { type: DataTypes.INTEGER, allowNull: false }, + }, + { + tableName: 'project_phase_approval', + paranoid: true, + timestamps: true, + updatedAt: 'updatedAt', + createdAt: 'createdAt', + deletedAt: 'deletedAt', + defaultScope: { + attributes: { + exclude: ['deletedAt', 'deletedBy'], + }, + }, + hooks: { + afterCreate: (projectPhaseApproval) => { + // eslint-disable-next-line no-param-reassign + delete projectPhaseApproval.dataValues.deletedAt; + // eslint-disable-next-line no-param-reassign + delete projectPhaseApproval.dataValues.deletedBy; + }, + }, + }); + + ProjectPhaseApproval.getPhaseApprovals = (phaseId, raw = true) => ProjectPhaseApproval.findAll({ + where: { + phaseId, + }, + raw, + }); + + ProjectPhaseApproval.associate = (models) => { + ProjectPhaseApproval.belongsTo(models.ProjectPhase, { foreignKey: { name: 'phaseId', allowNull: false } }); + }; + return ProjectPhaseApproval; +}; diff --git a/src/permissions/constants.js b/src/permissions/constants.js index 6f3cf9e7..31ad8979 100644 --- a/src/permissions/constants.js +++ b/src/permissions/constants.js @@ -633,6 +633,18 @@ export const PERMISSION = { // eslint-disable-line import/prefer-default-export scopes: SCOPES_PROJECTS_WRITE, }, + /* + * Project Phase Approval + */ + CREATE_PROJECT_PHASE_APPROVE: { + meta: { + title: 'Create Project Phase Approval', + group: 'Project Phase Approval', + description: 'Who can create project phase approval', + }, + projectRoles: [PROJECT_MEMBER_ROLE.CUSTOMER], + }, + /* * DEPRECATED - THIS PERMISSION RULE HAS TO BE REMOVED * diff --git a/src/permissions/index.js b/src/permissions/index.js index 8dc1f2ff..e7e6d0a1 100644 --- a/src/permissions/index.js +++ b/src/permissions/index.js @@ -102,6 +102,9 @@ module.exports = () => { Authorizer.setPolicy('phaseMember.delete', copilotAndAbove); Authorizer.setPolicy('phaseMember.view', generalPermission(PERMISSION.READ_PROJECT_MEMBER)); + Authorizer.setPolicy('phaseApproval.create', generalPermission(PERMISSION.CREATE_PROJECT_PHASE_APPROVE)); + Authorizer.setPolicy('phaseApproval.view', projectView); + Authorizer.setPolicy('milestoneTemplate.clone', projectAdmin); Authorizer.setPolicy('milestoneTemplate.create', projectAdmin); Authorizer.setPolicy('milestoneTemplate.edit', projectAdmin); diff --git a/src/routes/index.js b/src/routes/index.js index a20e1a04..9e142073 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -190,6 +190,10 @@ router.route('/v5/projects/:projectId(\\d+)/phases/:phaseId(\\d+)/members') router.route('/v5/projects/:projectId(\\d+)/phases/:phaseId(\\d+)/members/:userId(\\d+)') .delete(require('./phaseMembers/delete')); +router.route('/v5/projects/:projectId(\\d+)/phases/:phaseId(\\d+)/approvals') + .get(require('./phaseApprovals/list')) + .post(require('./phaseApprovals/create')); + router.route('/v5/projects/metadata/productCategories') .post(require('./productCategories/create')); diff --git a/src/routes/phaseApprovals/create.js b/src/routes/phaseApprovals/create.js new file mode 100644 index 00000000..6455426a --- /dev/null +++ b/src/routes/phaseApprovals/create.js @@ -0,0 +1,76 @@ +import _ from 'lodash'; +import Joi from 'joi'; +import validate from 'express-validation'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import models from '../../models'; +import util from '../../util'; +import { EVENT, RESOURCES, ROUTES } from '../../constants'; + +/** + * API to create a project phase approval. + */ +const permissions = tcMiddleware.permissions; + +const createPhaseApprovalValidations = { + body: Joi.object().keys({ + decision: Joi.string().valid('approve', 'reject').required(), + comment: Joi.string().trim().max(255).required(), + startDate: Joi.date().required(), + endDate: Joi.date().min(Joi.ref('startDate')).optional(), + expectedEndDate: Joi.date().min(Joi.ref('startDate')).required(), + }), + params: { + projectId: Joi.number().integer().positive().required(), + phaseId: Joi.number().integer().positive().required(), + }, +}; + +module.exports = [ + // handles request validations + validate(createPhaseApprovalValidations), + permissions('phaseApproval.create'), + async (req, res, next) => { + const projectId = _.parseInt(req.params.projectId); + const phaseId = _.parseInt(req.params.phaseId); + const approvalData = req.body; + const createdBy = _.parseInt(req.authUser.userId); + const updatedBy = _.parseInt(req.authUser.userId); + _.assign(approvalData, { phaseId, createdBy, updatedBy }); + try { + // check if project and phase exist + const phase = await models.ProjectPhase.findOne({ + where: { + id: phaseId, + projectId, + }, + include: [{ + model: models.ProjectPhaseApproval, + as: 'approvals', + }], + }); + if (!phase) { + const err = new Error('No active project phase found for project id ' + + `${projectId} and phase id ${phaseId}`); + err.status = 404; + throw (err); + } + const phaseApproval = (await models.ProjectPhaseApproval.create(approvalData)).toJSON(); + req.log.debug('created phase approval', JSON.stringify(phaseApproval, null, 2)); + const updatedPhase = _.cloneDeep(phase.toJSON()); + const approvals = _.isArray(updatedPhase.approvals) ? updatedPhase.approvals : []; + approvals.push(phaseApproval); + _.assign(updatedPhase, { approvals }); + // emit event + util.sendResourceToKafkaBus( + req, + EVENT.ROUTING_KEY.PROJECT_PHASE_UPDATED, + RESOURCES.PHASE, + updatedPhase, + phase.toJSON(), + ROUTES.PHASES.UPDATE); + res.json(phaseApproval); + } catch (err) { + next(err); + } + }, +]; diff --git a/src/routes/phaseApprovals/create.spec.js b/src/routes/phaseApprovals/create.spec.js new file mode 100644 index 00000000..d498c209 --- /dev/null +++ b/src/routes/phaseApprovals/create.spec.js @@ -0,0 +1,294 @@ +/** + * Tests for update.js + */ +import _ from 'lodash'; +import config from 'config'; +import request from 'supertest'; +import chai from 'chai'; +import util from '../../util'; +import models from '../../models'; +import server from '../../app'; +import testUtil from '../../tests/util'; + +const should = chai.should(); + +const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName'); +const ES_PROJECT_TYPE = config.get('elasticsearchConfig.docType'); +const eClient = util.getElasticSearchClient(); + +describe('Create phase approvals', () => { + let projectId; + let phaseId; + const requestBody = { + decision: 'approve', + comment: 'good', + startDate: '2021-08-02', + endDate: '2021-08-03', + expectedEndDate: '2021-08-03', + }; + const validateApproval = (resJson, expectedApproval) => { + should.exist(resJson); + resJson.decision.should.be.eql(expectedApproval.decision); + resJson.comment.should.be.eql(expectedApproval.comment); + resJson.startDate.should.be.a('string').and.satisfy(date => + date.startsWith(expectedApproval.startDate)); + resJson.endDate.should.be.a('string').and.satisfy(date => + date.startsWith(expectedApproval.endDate)); + resJson.expectedEndDate.should.be.a('string').and.satisfy(date => + date.startsWith(expectedApproval.expectedEndDate)); + }; + const validateError = (resJson, expectedMessage) => { + should.exist(resJson); + resJson.message.should.be.eql(expectedMessage); + }; + before((done) => { + // mocks + testUtil.clearDb() + .then(() => { + models.Project.create({ + type: 'generic', + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', + }).then((p) => { + const project = p.toJSON(); + projectId = project.id; + // create members + models.ProjectMember.create({ + userId: testUtil.userIds.member, + projectId, + role: 'customer', + isPrimary: true, + createdBy: 1, + updatedBy: 1, + }).then(() => { + models.ProjectPhase.create({ + name: 'test project phase', + projectId, + 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', + }, + createdBy: 1, + updatedBy: 1, + }).then((ph) => { + const phase = ph.toJSON(); + phaseId = phase.id; + // Index to ES + // Overwrite lastActivityAt as otherwise ES fill not be able to parse it + project.lastActivityAt = 1; + project.phases = [phase]; + return eClient.index({ + index: ES_PROJECT_INDEX, + type: ES_PROJECT_TYPE, + id: project.id, + body: project, + }).then(() => { + done(); + }); + }); + }); + }); + }); + }); + + after((done) => { + testUtil.clearDb(done); + }); + describe('POST /projects/{projectId}/phases/{phaseId}/approvals', () => { + it('should return 403 for anonymous user', (done) => { + request(server) + .post(`/v5/projects/${projectId}/phases/${phaseId}/approvals`) + .send(requestBody) + .expect(403, done); + }); + + it('should return 403 for non project user', (done) => { + request(server) + .post(`/v5/projects/${projectId}/phases/${phaseId}/approvals`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member2}`, + }) + .send(requestBody) + .expect(403, done); + }); + + it('should return 403 for connect admin', (done) => { + request(server) + .post(`/v5/projects/${projectId}/phases/${phaseId}/approvals`) + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .send(requestBody) + .expect(403, done); + }); + + it('should return 403 for admin', (done) => { + request(server) + .post(`/v5/projects/${projectId}/phases/${phaseId}/approvals`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(requestBody) + .expect(403, done); + }); + + it('should return 403 for manager', (done) => { + request(server) + .post(`/v5/projects/${projectId}/phases/${phaseId}/approvals`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send(requestBody) + .expect(403, done); + }); + + it('should return 200 for project customer', (done) => { + request(server) + .post(`/v5/projects/${projectId}/phases/${phaseId}/approvals`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send(requestBody) + .expect(200) + .end((err, res) => { + const resJson = res.body; + validateApproval(resJson, requestBody); + done(); + }); + }); + + it('should return 400 when decision field is missing', (done) => { + request(server) + .post(`/v5/projects/${projectId}/phases/${phaseId}/approvals`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send(_.omit(requestBody, 'decision')) + .expect(400) + .end((err, res) => { + const resJson = res.body; + validateError(resJson, 'validation error: "decision" is required'); + done(); + }); + }); + + it('should return 400 when comment field is missing', (done) => { + request(server) + .post(`/v5/projects/${projectId}/phases/${phaseId}/approvals`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send(_.omit(requestBody, 'comment')) + .expect(400) + .end((err, res) => { + const resJson = res.body; + validateError(resJson, 'validation error: "comment" is required'); + done(); + }); + }); + + it('should return 400 when startDate field is missing', (done) => { + request(server) + .post(`/v5/projects/${projectId}/phases/${phaseId}/approvals`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send(_.omit(requestBody, 'startDate')) + .expect(400) + .end((err, res) => { + const resJson = res.body; + validateError(resJson, 'validation error: "startDate" is required,' + + '"endDate" references "startDate" which is not a date,' + + '"expectedEndDate" references "startDate" which is not a date'); + done(); + }); + }); + + it('should return 400 when expectedEndDate field is missing', (done) => { + request(server) + .post(`/v5/projects/${projectId}/phases/${phaseId}/approvals`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send(_.omit(requestBody, 'expectedEndDate')) + .expect(400) + .end((err, res) => { + const resJson = res.body; + validateError(resJson, 'validation error: "expectedEndDate" is required'); + done(); + }); + }); + + it('should return 400 when decision field is invalid', (done) => { + request(server) + .post(`/v5/projects/${projectId}/phases/${phaseId}/approvals`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send(_.assign({}, requestBody, { decision: 'ok' })) + .expect(400) + .end((err, res) => { + const resJson = res.body; + validateError(resJson, 'validation error: "decision" must be one of [approve, reject]'); + done(); + }); + }); + + it('should return 400 when comment field is invalid', (done) => { + request(server) + .post(`/v5/projects/${projectId}/phases/${phaseId}/approvals`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send(_.assign({}, requestBody, { comment: '' })) + .expect(400) + .end((err, res) => { + const resJson = res.body; + validateError(resJson, 'validation error: "comment" is not allowed to be empty'); + done(); + }); + }); + + it('should return 400 when comment field is invalid', (done) => { + request(server) + .post(`/v5/projects/${projectId}/phases/${phaseId}/approvals`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send(_.assign({}, requestBody, { comment: '' })) + .expect(400) + .end((err, res) => { + const resJson = res.body; + validateError(resJson, 'validation error: "comment" is not allowed to be empty'); + done(); + }); + }); + + it('should return 400 when endDate is before startDate', (done) => { + request(server) + .post(`/v5/projects/${projectId}/phases/${phaseId}/approvals`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send(_.assign({}, requestBody, { endDate: '2021-08-01' })) + .expect(400) + .end((err, res) => { + const resJson = res.body; + resJson.message.should.be.a('string').and.satisfy(message => + message.startsWith('validation error: "endDate" must be larger than or equal to')); + done(); + }); + }); + }); +}); diff --git a/src/routes/phaseApprovals/list.js b/src/routes/phaseApprovals/list.js new file mode 100644 index 00000000..3b97e129 --- /dev/null +++ b/src/routes/phaseApprovals/list.js @@ -0,0 +1,86 @@ +import _ from 'lodash'; +import config from 'config'; +import Joi from 'joi'; +import validate from 'express-validation'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import models from '../../models'; +import util from '../../util'; + +const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName'); +const ES_PROJECT_TYPE = config.get('elasticsearchConfig.docType'); + +/** + * API to list a project phase approvals. + */ +const permissions = tcMiddleware.permissions; + +const listPhaseMemberValidations = { + params: { + projectId: Joi.number().integer().positive().required(), + phaseId: Joi.number().integer().positive().required(), + }, +}; + +module.exports = [ + // handles request validations + validate(listPhaseMemberValidations), + permissions('phaseApproval.view'), + async (req, res, next) => { + const projectId = _.parseInt(req.params.projectId); + const phaseId = _.parseInt(req.params.phaseId); + try { + const esClient = util.getElasticSearchClient(); + const project = await esClient.search({ index: ES_PROJECT_INDEX, + type: ES_PROJECT_TYPE, + body: { + query: { + bool: { + must: [ + { term: { id: projectId } }, + { nested: { + path: 'phases', + query: { + term: { 'phases.id': phaseId }, + }, + } }, + ], + }, + }, + }, + }); + if (!project.hits.total) { + throw new Error(); + } + // eslint-disable-next-line no-underscore-dangle + const phase = _.find(project.hits.hits[0]._source.phases, ['id', phaseId]); + const approvals = phase.approvals || []; + res.json(approvals); + return; + } catch (err) { + req.log.debug('No active project phase found in ES for project id ' + + `${projectId} and phase id ${phaseId}`); + } + try { + req.log.debug('Fall back to DB'); + const phase = await models.ProjectPhase.findOne({ + where: { + id: phaseId, + projectId, + }, + include: [{ + model: models.ProjectPhaseApproval, + as: 'approvals', + }], + }); + if (!phase) { + const err = new Error(`No active project phase found for project id ${projectId} and phase id ${phaseId}`); + err.status = 404; + throw (err); + } + const approvals = phase.toJSON().approvals; + res.json(approvals); + } catch (err) { + next(err); + } + }, +]; diff --git a/src/routes/phaseApprovals/list.spec.js b/src/routes/phaseApprovals/list.spec.js new file mode 100644 index 00000000..77f202d3 --- /dev/null +++ b/src/routes/phaseApprovals/list.spec.js @@ -0,0 +1,218 @@ +/** + * Tests for list.js + */ +import _ from 'lodash'; +import config from 'config'; +import request from 'supertest'; +import chai from 'chai'; +import util from '../../util'; +import models from '../../models'; +import server from '../../app'; +import testUtil from '../../tests/util'; + +const should = chai.should(); + +const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName'); +const ES_PROJECT_TYPE = config.get('elasticsearchConfig.docType'); +const eClient = util.getElasticSearchClient(); + +describe('List phase approvals', () => { + let projectId; + let phaseId; + const approvalObject = { + decision: 'approve', + comment: 'good', + startDate: '2021-08-02', + endDate: '2021-08-03', + expectedEndDate: '2021-08-03', + }; + const validateApproval = (resJson, expectedApproval) => { + should.exist(resJson); + resJson.decision.should.be.eql(expectedApproval.decision); + resJson.comment.should.be.eql(expectedApproval.comment); + resJson.startDate.should.be.a('string').and.satisfy(date => + date.startsWith(expectedApproval.startDate)); + resJson.endDate.should.be.a('string').and.satisfy(date => + date.startsWith(expectedApproval.endDate)); + resJson.expectedEndDate.should.be.a('string').and.satisfy(date => + date.startsWith(expectedApproval.expectedEndDate)); + }; + before((done) => { + // mocks + testUtil.clearDb() + .then(() => { + models.Project.create({ + type: 'generic', + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', + }).then((p) => { + const project = p.toJSON(); + projectId = project.id; + // create members + models.ProjectMember.bulkCreate([{ + userId: testUtil.userIds.member, + projectId, + role: 'customer', + isPrimary: true, + createdBy: 1, + updatedBy: 1, + }, { + userId: testUtil.userIds.copilot, + projectId, + role: 'copilot', + isPrimary: false, + createdBy: 1, + updatedBy: 1, + }]).then(() => { + models.ProjectPhase.create({ + name: 'test project phase', + projectId, + 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', + }, + createdBy: 1, + updatedBy: 1, + }).then((ph) => { + const phase = ph.toJSON(); + phaseId = phase.id; + models.ProjectPhaseApproval.create( + _.assign(approvalObject, { + phaseId, + createdBy: 1, + updatedBy: 1, + })).then((pa) => { + _.assign(phase, { approvals: [pa.toJSON()] }); + // Index to ES + // Overwrite lastActivityAt as otherwise ES fill not be able to parse it + project.lastActivityAt = 1; + project.phases = [phase]; + return eClient.index({ + index: ES_PROJECT_INDEX, + type: ES_PROJECT_TYPE, + id: project.id, + body: project, + }).then(() => { + done(); + }); + }); + }); + }); + }); + }); + }); + + after((done) => { + testUtil.clearDb(done); + }); + describe('GET /projects/{projectId}/phases/{phaseId}/approvals', () => { + it('should return 403 for anonymous user', (done) => { + request(server) + .get(`/v5/projects/${projectId}/phases/${phaseId}/approvals`) + .expect(403, done); + }); + + it('should return 403 for non project member user', (done) => { + request(server) + .get(`/v5/projects/${projectId}/phases/${phaseId}/approvals`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member2}`, + }) + .expect(403, done); + }); + + it('should return 200 for project customer user', (done) => { + request(server) + .get(`/v5/projects/${projectId}/phases/${phaseId}/approvals`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(200) + .end((err, res) => { + const resJson = res.body; + validateApproval(resJson[0], approvalObject); + done(); + }); + }); + + it('should return 200 for connect admin', (done) => { + request(server) + .get(`/v5/projects/${projectId}/phases/${phaseId}/approvals`) + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(200) + .end((err, res) => { + const resJson = res.body; + validateApproval(resJson[0], approvalObject); + done(); + }); + }); + + it('should return 200 for admin', (done) => { + request(server) + .get(`/v5/projects/${projectId}/phases/${phaseId}/approvals`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end((err, res) => { + const resJson = res.body; + validateApproval(resJson[0], approvalObject); + done(); + }); + }); + + it('should return 200 for manager', (done) => { + request(server) + .get(`/v5/projects/${projectId}/phases/${phaseId}/approvals`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(200) + .end((err, res) => { + const resJson = res.body; + validateApproval(resJson[0], approvalObject); + done(); + }); + }); + + it('should return 200 for project copilot user', (done) => { + request(server) + .get(`/v5/projects/${projectId}/phases/${phaseId}/approvals`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(200) + .end((err, res) => { + const resJson = res.body; + validateApproval(resJson[0], approvalObject); + done(); + }); + }); + + it('should return 403 for non project copilot user', (done) => { + models.ProjectMember.destroy({ + where: { userId: testUtil.userIds.copilot, projectId }, + }).then(() => { + request(server) + .get(`/v5/projects/${projectId}/phases/${phaseId}/approvals`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(403, done); + }); + }); + }); +}); diff --git a/src/routes/phaseMembers/delete.spec.js b/src/routes/phaseMembers/delete.spec.js index d6988d65..71e1441d 100644 --- a/src/routes/phaseMembers/delete.spec.js +++ b/src/routes/phaseMembers/delete.spec.js @@ -31,11 +31,9 @@ describe('Delete phase member', () => { lastName: 'lName', email: 'some@abc.com', }; - before(function beforeHook(done) { - this.timeout(20000); + before((done) => { // mocks testUtil.clearDb() - .then(() => testUtil.clearES()) .then(() => { models.Project.create({ type: 'generic', @@ -53,7 +51,6 @@ describe('Delete phase member', () => { project = p.toJSON(); // create members models.ProjectMember.bulkCreate([{ - id: 1, userId: copilotUser.userId, projectId: id, role: 'copilot', @@ -61,7 +58,6 @@ describe('Delete phase member', () => { createdBy: 1, updatedBy: 1, }, { - id: 2, userId: memberUser.userId, projectId: id, role: 'customer', @@ -159,7 +155,7 @@ describe('Delete phase member', () => { it('should return 403 for copilot which is not member of project', (done) => { models.ProjectMember.destroy({ - where: { userId: testUtil.userIds.copilot, id }, + where: { userId: testUtil.userIds.copilot, projectId: id }, }).then(() => { request(server) .delete(`/v5/projects/${id}/phases/${phaseId}/members/${copilotUser.userId}`) diff --git a/src/routes/phaseMembers/list.spec.js b/src/routes/phaseMembers/list.spec.js index 34e9436c..074a0c36 100644 --- a/src/routes/phaseMembers/list.spec.js +++ b/src/routes/phaseMembers/list.spec.js @@ -27,11 +27,9 @@ describe('List phase members', () => { lastName: 'lName', email: 'some@abc.com', }; - before(function beforeHook(done) { - this.timeout(20000); + before((done) => { // mocks testUtil.clearDb() - .then(() => testUtil.clearES()) .then(() => { models.Project.create({ type: 'generic', @@ -49,7 +47,6 @@ describe('List phase members', () => { project = p.toJSON(); // create members models.ProjectMember.create({ - id: 1, userId: copilotUser.userId, projectId: id, role: 'copilot', diff --git a/src/routes/phaseMembers/update.js b/src/routes/phaseMembers/update.js index 64c55e54..26f75077 100644 --- a/src/routes/phaseMembers/update.js +++ b/src/routes/phaseMembers/update.js @@ -31,7 +31,7 @@ module.exports = [ const phaseId = _.parseInt(req.params.phaseId); const newPhaseMembers = req.body.userIds; try { - // chekc if project and phase exist + // check if project and phase exist const phase = await models.ProjectPhase.findOne({ where: { id: phaseId, diff --git a/src/routes/phaseMembers/update.spec.js b/src/routes/phaseMembers/update.spec.js index 91251277..963c7b3f 100644 --- a/src/routes/phaseMembers/update.spec.js +++ b/src/routes/phaseMembers/update.spec.js @@ -34,11 +34,9 @@ describe('Update phase members', () => { lastName: 'lName', email: 'some@abc.com', }; - before(function beforeHook(done) { - this.timeout(20000); + before((done) => { // mocks testUtil.clearDb() - .then(() => testUtil.clearES()) .then(() => { models.Project.create({ type: 'generic', @@ -56,7 +54,6 @@ describe('Update phase members', () => { project = p.toJSON(); // create members models.ProjectMember.bulkCreate([{ - id: 1, userId: copilotUser.userId, projectId: id, role: 'copilot', @@ -64,7 +61,6 @@ describe('Update phase members', () => { createdBy: 1, updatedBy: 1, }, { - id: 2, userId: memberUser.userId, projectId: id, role: 'customer', @@ -179,7 +175,7 @@ describe('Update phase members', () => { it('should return 403 for copilot which is not member of project', (done) => { models.ProjectMember.destroy({ - where: { userId: testUtil.userIds.copilot, id }, + where: { userId: testUtil.userIds.copilot, projectId: id }, }).then(() => { request(server) .post(`/v5/projects/${id}/phases/${phaseId}/members`) diff --git a/src/routes/phaseMembers/updateService.js b/src/routes/phaseMembers/updateService.js index b0781c34..a75ba1b2 100644 --- a/src/routes/phaseMembers/updateService.js +++ b/src/routes/phaseMembers/updateService.js @@ -38,7 +38,7 @@ async function update(currentUser, projectId, phaseId, newPhaseMembers, _transac } if (membersToAdd.length > 0) { const createData = _.map(membersToAdd, userId => ({ phaseId, userId, createdBy, updatedBy })); - const result = await models.ProjectPhaseMember.bulkCreate(createData, { transaction }); + const result = await models.ProjectPhaseMember.bulkCreate(createData, { individualHooks: true, transaction }); phaseMembers.push(..._.map(result, item => item.toJSON())); } if (_.isUndefined(_transaction)) { diff --git a/src/routes/phases/get.js b/src/routes/phases/get.js index 5d329363..32c64e75 100644 --- a/src/routes/phases/get.js +++ b/src/routes/phases/get.js @@ -42,6 +42,10 @@ module.exports = [ include: [{ model: models.ProjectPhaseMember, as: 'members', + }, + { + model: models.ProjectPhaseApproval, + as: 'approvals', }], }) .then((phase) => { diff --git a/src/routes/phases/list.js b/src/routes/phases/list.js index cb69e8ee..76092569 100644 --- a/src/routes/phases/list.js +++ b/src/routes/phases/list.js @@ -52,7 +52,7 @@ module.exports = [ // Sort phases = _.orderBy(phases, [sortColumnAndOrder[0]], [sortColumnAndOrder[1]]); - fields = _.intersection(fields, [...PHASE_ATTRIBUTES, 'products', 'members']); + fields = _.intersection(fields, [...PHASE_ATTRIBUTES, 'products', 'members', 'approvals']); if (_.indexOf(fields, 'id') < 0) { fields.push('id'); } @@ -85,6 +85,12 @@ module.exports = [ as: 'members', }); } + if (_.indexOf(fields, 'approvals') >= 0) { + include.include.push({ + model: models.ProjectPhaseApproval, + as: 'approvals', + }); + } // Load the phases return models.Project.findByPk(projectId, { include: [include], @@ -106,7 +112,7 @@ module.exports = [ // Sort phases = _.orderBy(phases, [sortColumnAndOrder[0]], [sortColumnAndOrder[1]]); _.remove(PHASE_ATTRIBUTES, attribute => _.includes(['deletedAt', 'deletedBy'], attribute)); - fields = _.intersection(fields, [...PHASE_ATTRIBUTES, 'products', 'members']); + fields = _.intersection(fields, [...PHASE_ATTRIBUTES, 'products', 'members', 'approvals']); if (_.indexOf(fields, 'id') < 0) { fields.push('id'); } From 60b674e86529515a187399608652dc9fae68896a Mon Sep 17 00:00:00 2001 From: eisbilir Date: Tue, 3 Aug 2021 18:47:12 +0300 Subject: [PATCH 13/17] make phase validation dates optional --- src/routes/phaseApprovals/create.js | 6 +++--- src/routes/phaseApprovals/create.spec.js | 19 ++----------------- 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/src/routes/phaseApprovals/create.js b/src/routes/phaseApprovals/create.js index 6455426a..01f8f554 100644 --- a/src/routes/phaseApprovals/create.js +++ b/src/routes/phaseApprovals/create.js @@ -15,9 +15,9 @@ const createPhaseApprovalValidations = { body: Joi.object().keys({ decision: Joi.string().valid('approve', 'reject').required(), comment: Joi.string().trim().max(255).required(), - startDate: Joi.date().required(), - endDate: Joi.date().min(Joi.ref('startDate')).optional(), - expectedEndDate: Joi.date().min(Joi.ref('startDate')).required(), + startDate: Joi.date().default(Date()), + endDate: Joi.date().min(Joi.ref('startDate')).default(Date()), + expectedEndDate: Joi.date().min(Joi.ref('startDate')).default(Date()), }), params: { projectId: Joi.number().integer().positive().required(), diff --git a/src/routes/phaseApprovals/create.spec.js b/src/routes/phaseApprovals/create.spec.js index d498c209..042e54bd 100644 --- a/src/routes/phaseApprovals/create.spec.js +++ b/src/routes/phaseApprovals/create.spec.js @@ -198,7 +198,7 @@ describe('Create phase approvals', () => { }); }); - it('should return 400 when startDate field is missing', (done) => { + it.skip('should return 400 when startDate field is missing', (done) => { request(server) .post(`/v5/projects/${projectId}/phases/${phaseId}/approvals`) .set({ @@ -215,7 +215,7 @@ describe('Create phase approvals', () => { }); }); - it('should return 400 when expectedEndDate field is missing', (done) => { + it.skip('should return 400 when expectedEndDate field is missing', (done) => { request(server) .post(`/v5/projects/${projectId}/phases/${phaseId}/approvals`) .set({ @@ -260,21 +260,6 @@ describe('Create phase approvals', () => { }); }); - it('should return 400 when comment field is invalid', (done) => { - request(server) - .post(`/v5/projects/${projectId}/phases/${phaseId}/approvals`) - .set({ - Authorization: `Bearer ${testUtil.jwts.member}`, - }) - .send(_.assign({}, requestBody, { comment: '' })) - .expect(400) - .end((err, res) => { - const resJson = res.body; - validateError(resJson, 'validation error: "comment" is not allowed to be empty'); - done(); - }); - }); - it('should return 400 when endDate is before startDate', (done) => { request(server) .post(`/v5/projects/${projectId}/phases/${phaseId}/approvals`) From 087e37878dbcc437bfb4f78d38e7b44470727827 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Wed, 4 Aug 2021 15:01:12 +0300 Subject: [PATCH 14/17] update phase status --- .../20210802_project_phase_approval_table.sql | 2 +- src/models/projectPhaseApproval.js | 2 +- src/routes/phaseApprovals/create.js | 20 ++++++++-- src/routes/phaseApprovals/create.spec.js | 39 +++++++++++-------- src/routes/phaseProducts/delete.js | 2 +- src/routes/phases/create.js | 4 +- src/routes/phases/update.js | 4 +- src/routes/phases/update.spec.js | 2 +- 8 files changed, 48 insertions(+), 27 deletions(-) diff --git a/migrations/20210802_project_phase_approval_table.sql b/migrations/20210802_project_phase_approval_table.sql index 2feed0fd..19633050 100644 --- a/migrations/20210802_project_phase_approval_table.sql +++ b/migrations/20210802_project_phase_approval_table.sql @@ -12,7 +12,7 @@ CREATE TABLE "project_phase_approval" ( "id" int8 NOT NULL DEFAULT nextval('project_phase_approval_id_seq'::regclass), "phaseId" int8 NOT NULL, "decision" "enum_project_phase_approval_decision" NOT NULL, - "comment" varchar NOT NULL, + "comment" varchar, "startDate" timestamptz NOT NULL, "endDate" timestamptz, "expectedEndDate" timestamptz NOT NULL, diff --git a/src/models/projectPhaseApproval.js b/src/models/projectPhaseApproval.js index c23261db..5b37698d 100644 --- a/src/models/projectPhaseApproval.js +++ b/src/models/projectPhaseApproval.js @@ -3,7 +3,7 @@ module.exports = function defineProjectPhaseApproval(sequelize, DataTypes) { id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, phaseId: { type: DataTypes.BIGINT, allowNull: false }, decision: { type: DataTypes.ENUM, values: ['approve', 'reject'], allowNull: false }, - comment: { type: DataTypes.STRING, allowNull: false }, + comment: { type: DataTypes.STRING, allowNull: true }, startDate: { type: DataTypes.DATE, allowNull: false }, endDate: { type: DataTypes.DATE, allowNull: true }, expectedEndDate: { type: DataTypes.DATE, allowNull: false }, diff --git a/src/routes/phaseApprovals/create.js b/src/routes/phaseApprovals/create.js index 01f8f554..dc124172 100644 --- a/src/routes/phaseApprovals/create.js +++ b/src/routes/phaseApprovals/create.js @@ -4,7 +4,7 @@ import validate from 'express-validation'; import { middleware as tcMiddleware } from 'tc-core-library-js'; import models from '../../models'; import util from '../../util'; -import { EVENT, RESOURCES, ROUTES } from '../../constants'; +import { EVENT, RESOURCES, ROUTES, PROJECT_PHASE_STATUS } from '../../constants'; /** * API to create a project phase approval. @@ -14,7 +14,7 @@ const permissions = tcMiddleware.permissions; const createPhaseApprovalValidations = { body: Joi.object().keys({ decision: Joi.string().valid('approve', 'reject').required(), - comment: Joi.string().trim().max(255).required(), + comment: Joi.string().trim().max(255).optional(), startDate: Joi.date().default(Date()), endDate: Joi.date().min(Joi.ref('startDate')).default(Date()), expectedEndDate: Joi.date().min(Joi.ref('startDate')).default(Date()), @@ -36,6 +36,7 @@ module.exports = [ const createdBy = _.parseInt(req.authUser.userId); const updatedBy = _.parseInt(req.authUser.userId); _.assign(approvalData, { phaseId, createdBy, updatedBy }); + let transaction; try { // check if project and phase exist const phase = await models.ProjectPhase.findOne({ @@ -54,7 +55,16 @@ module.exports = [ err.status = 404; throw (err); } - const phaseApproval = (await models.ProjectPhaseApproval.create(approvalData)).toJSON(); + if (phase.status !== PROJECT_PHASE_STATUS.IN_REVIEW) { + const err = new Error(`Phase with id ${phaseId} must be ` + + `${PROJECT_PHASE_STATUS.IN_REVIEW} status to make approval`); + err.status = 400; + throw (err); + } + transaction = await models.sequelize.transaction(); + const created = await models.ProjectPhaseApproval.create(approvalData, { transaction }); + await phase.update({ status: PROJECT_PHASE_STATUS.REVIEWED }, { transaction }); + const phaseApproval = created.toJSON(); req.log.debug('created phase approval', JSON.stringify(phaseApproval, null, 2)); const updatedPhase = _.cloneDeep(phase.toJSON()); const approvals = _.isArray(updatedPhase.approvals) ? updatedPhase.approvals : []; @@ -68,8 +78,12 @@ module.exports = [ updatedPhase, phase.toJSON(), ROUTES.PHASES.UPDATE); + await transaction.commit(); res.json(phaseApproval); } catch (err) { + if (!_.isUndefined(transaction)) { + await transaction.rollback(); + } next(err); } }, diff --git a/src/routes/phaseApprovals/create.spec.js b/src/routes/phaseApprovals/create.spec.js index 042e54bd..7ee74faf 100644 --- a/src/routes/phaseApprovals/create.spec.js +++ b/src/routes/phaseApprovals/create.spec.js @@ -71,7 +71,7 @@ describe('Create phase approvals', () => { models.ProjectPhase.create({ name: 'test project phase', projectId, - status: 'active', + status: 'in_review', startDate: '2018-05-15T00:00:00Z', endDate: '2018-05-15T12:00:00Z', budget: 20.0, @@ -168,6 +168,13 @@ describe('Create phase approvals', () => { }); }); + it('should update phase status to "reviewed" after approve', (done) => { + models.ProjectPhase.findOne({ id: phaseId }).then((phase) => { + phase.dataValues.status.should.be.eql('reviewed'); + done(); + }); + }); + it('should return 400 when decision field is missing', (done) => { request(server) .post(`/v5/projects/${projectId}/phases/${phaseId}/approvals`) @@ -183,21 +190,6 @@ describe('Create phase approvals', () => { }); }); - it('should return 400 when comment field is missing', (done) => { - request(server) - .post(`/v5/projects/${projectId}/phases/${phaseId}/approvals`) - .set({ - Authorization: `Bearer ${testUtil.jwts.member}`, - }) - .send(_.omit(requestBody, 'comment')) - .expect(400) - .end((err, res) => { - const resJson = res.body; - validateError(resJson, 'validation error: "comment" is required'); - done(); - }); - }); - it.skip('should return 400 when startDate field is missing', (done) => { request(server) .post(`/v5/projects/${projectId}/phases/${phaseId}/approvals`) @@ -275,5 +267,20 @@ describe('Create phase approvals', () => { done(); }); }); + + it('should return 400 when phase status is not in_review', (done) => { + request(server) + .post(`/v5/projects/${projectId}/phases/${phaseId}/approvals`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send(requestBody) + .expect(400) + .end((err, res) => { + const resJson = res.body; + validateError(resJson, `Phase with id ${phaseId} must be in_review status to make approval`); + done(); + }); + }); }); }); diff --git a/src/routes/phaseProducts/delete.js b/src/routes/phaseProducts/delete.js index b9620336..4a527dc1 100644 --- a/src/routes/phaseProducts/delete.js +++ b/src/routes/phaseProducts/delete.js @@ -44,7 +44,7 @@ module.exports = [ req, EVENT.ROUTING_KEY.PROJECT_PHASE_PRODUCT_REMOVED, RESOURCES.PHASE_PRODUCT, - _.pick(deleted.toJSON(), 'id')); + _.pick(deleted.toJSON(), ['id', 'projectId'])); res.status(204).json({}); }) diff --git a/src/routes/phases/create.js b/src/routes/phases/create.js index 1463d898..d852db5a 100644 --- a/src/routes/phases/create.js +++ b/src/routes/phases/create.js @@ -4,7 +4,7 @@ import Joi from 'joi'; import models from '../../models'; import util from '../../util'; -import { EVENT, RESOURCES } from '../../constants'; +import { EVENT, RESOURCES, PROJECT_PHASE_STATUS } from '../../constants'; import updatePhaseMemberService from '../phaseMembers/updateService'; @@ -16,7 +16,7 @@ const addProjectPhaseValidations = { name: Joi.string().required(), description: Joi.string().optional(), requirements: Joi.string().optional(), - status: Joi.string().required(), + status: Joi.string().valid(..._.values(PROJECT_PHASE_STATUS)).required(), startDate: Joi.date().optional(), endDate: Joi.date().optional(), duration: Joi.number().min(0).optional(), diff --git a/src/routes/phases/update.js b/src/routes/phases/update.js index 3008f583..8d4e4e8c 100644 --- a/src/routes/phases/update.js +++ b/src/routes/phases/update.js @@ -5,7 +5,7 @@ import Joi from 'joi'; import { middleware as tcMiddleware } from 'tc-core-library-js'; import models from '../../models'; import util from '../../util'; -import { EVENT, RESOURCES, ROUTES } from '../../constants'; +import { EVENT, RESOURCES, ROUTES, PROJECT_PHASE_STATUS } from '../../constants'; import updatePhaseMemberService from '../phaseMembers/updateService'; @@ -16,7 +16,7 @@ const updateProjectPhaseValidation = { name: Joi.string().optional(), description: Joi.string().optional(), requirements: Joi.string().optional(), - status: Joi.string().optional(), + status: Joi.string().valid(..._.values(PROJECT_PHASE_STATUS)).optional(), startDate: Joi.date().optional(), endDate: Joi.date().optional(), duration: Joi.number().min(0).optional(), diff --git a/src/routes/phases/update.spec.js b/src/routes/phases/update.spec.js index 18554c86..79489652 100644 --- a/src/routes/phases/update.spec.js +++ b/src/routes/phases/update.spec.js @@ -35,7 +35,7 @@ const updateBody = { name: 'test project phase xxx', description: 'test project phase description xxx', requirements: 'test project phase requirements xxx', - status: 'inactive', + status: 'in_review', startDate: '2018-05-11T00:00:00Z', endDate: '2018-05-12T12:00:00Z', budget: 123456.789, From 9ef86a75047dccf14d9fe74e46b425fa0fc65872 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Wed, 4 Aug 2021 16:08:17 +0300 Subject: [PATCH 15/17] update postman tests --- docs/Project API.postman_collection.json | 43 +++++++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/docs/Project API.postman_collection.json b/docs/Project API.postman_collection.json index 612452d2..2fa603b5 100644 --- a/docs/Project API.postman_collection.json +++ b/docs/Project API.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "34327e29-e237-4bca-9101-5d2b6a1e25ff", + "_postman_id": "52f34e21-5b0b-4eb0-99fa-cbd1ac7f215a", "name": "Project API", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -5240,7 +5240,7 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"name\": \"test project phase xxx\",\n\t\"status\": \"inactive\",\n\t\"startDate\": \"2018-05-14T00:00:00\",\n\t\"endDate\": \"2018-05-15T00:00:00\",\n\t\"budget\": 30,\n\t\"progress\": 15,\n\t\"details\": {\n\t\t\"message\": \"phase details\"\n\t}\n}" + "raw": "{\n\t\"name\": \"test project phase xxx\",\n\t\"status\": \"in_review\",\n\t\"startDate\": \"2018-05-14T00:00:00\",\n\t\"endDate\": \"2018-05-15T00:00:00\",\n\t\"budget\": 30,\n\t\"progress\": 15,\n\t\"details\": {\n\t\t\"message\": \"phase details\"\n\t}\n}" }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}", @@ -5273,7 +5273,7 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"name\": \"test project phase xxx\",\n\t\"status\": \"inactive\",\n\t\"startDate\": \"2018-05-14T00:00:00\",\n\t\"endDate\": \"2018-05-15T00:00:00\",\n\t\"budget\": 30,\n\t\"progress\": 15,\n\t\"details\": {\n\t\t\"message\": \"phase details\"\n\t},\n\t\"order\": 1\n}" + "raw": "{\n\t\"name\": \"test project phase xxx\",\n\t\"status\": \"in_review\",\n\t\"startDate\": \"2018-05-14T00:00:00\",\n\t\"endDate\": \"2018-05-15T00:00:00\",\n\t\"budget\": 30,\n\t\"progress\": 15,\n\t\"details\": {\n\t\t\"message\": \"phase details\"\n\t},\n\t\"order\": 1\n}" }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}", @@ -5306,7 +5306,7 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"name\": \"test project phase xxx\",\n\t\"status\": \"inactive\",\n\t\"startDate\": \"2018-05-14T00:00:00\",\n\t\"endDate\": \"2018-05-15T00:00:00\",\n\t\"budget\": 30,\n\t\"progress\": 15,\n\t\"details\": {\n\t\t\"message\": \"phase details\"\n\t},\n \"members\": [{{phaseMemberId-1}},{{phaseMemberId-2}}]\n}" + "raw": "{\n\t\"name\": \"test project phase xxx\",\n\t\"status\": \"in_review\",\n\t\"startDate\": \"2018-05-14T00:00:00\",\n\t\"endDate\": \"2018-05-15T00:00:00\",\n\t\"budget\": 30,\n\t\"progress\": 15,\n\t\"details\": {\n\t\t\"message\": \"phase details\"\n\t},\n \"members\": [{{phaseMemberId-1}},{{phaseMemberId-2}}]\n}" }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}", @@ -6106,7 +6106,7 @@ ], "body": { "mode": "raw", - "raw": "{\n\t\"name\": \"test project phase\",\n\t\"status\": \"active\",\n\t\"startDate\": \"2018-05-15T00:00:00\",\n\t\"endDate\": \"2018-05-16T00:00:00\",\n\t\"budget\": 20,\n\t\"details\": {\n\t\t\"aDetails\": \"a details\"\n\t}\n}" + "raw": "{\n\t\"name\": \"test project phase\",\n\t\"status\": \"in_review\",\n\t\"startDate\": \"2018-05-15T00:00:00\",\n\t\"endDate\": \"2018-05-16T00:00:00\",\n\t\"budget\": 20,\n\t\"details\": {\n\t\t\"aDetails\": \"a details\"\n\t}\n}" }, "url": { "raw": "{{api-url}}/projects/{{projectId}}/phases", @@ -6171,6 +6171,39 @@ }, "response": [] }, + { + "name": "Update Phase", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{jwt-token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"status\": \"in_review\"\n}" + }, + "url": { + "raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}", + "host": [ + "{{api-url}}" + ], + "path": [ + "projects", + "{{projectId}}", + "phases", + "{{phaseId}}" + ] + } + }, + "response": [] + }, { "name": "Create Phase Approval - approve Copy", "event": [ From 8114e136a7da36bae5fc8191e53e06db6748e072 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Thu, 5 Aug 2021 15:36:42 +0300 Subject: [PATCH 16/17] fix: product delete sync with ES --- src/routes/phaseProducts/delete.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/routes/phaseProducts/delete.js b/src/routes/phaseProducts/delete.js index 4a527dc1..98189aeb 100644 --- a/src/routes/phaseProducts/delete.js +++ b/src/routes/phaseProducts/delete.js @@ -17,7 +17,7 @@ module.exports = [ const phaseId = _.parseInt(req.params.phaseId); const productId = _.parseInt(req.params.productId); - models.sequelize.transaction(() => + models.sequelize.transaction(transaction => // soft delete the record models.PhaseProduct.findOne({ where: { @@ -34,9 +34,9 @@ module.exports = [ err.status = 404; return Promise.reject(err); } - return existing.update({ deletedBy: req.authUser.userId }); + return existing.update({ deletedBy: req.authUser.userId }, { transaction }); }) - .then(entity => entity.destroy())) + .then(entity => entity.destroy({ transaction }))) .then((deleted) => { req.log.debug('deleted phase product', JSON.stringify(deleted, null, 2)); // emit the event @@ -44,7 +44,7 @@ module.exports = [ req, EVENT.ROUTING_KEY.PROJECT_PHASE_PRODUCT_REMOVED, RESOURCES.PHASE_PRODUCT, - _.pick(deleted.toJSON(), ['id', 'projectId'])); + _.pick(deleted.toJSON(), ['id', 'projectId', 'phaseId'])); res.status(204).json({}); }) From 365840298a051960035213b2c4c0e13c0ff34c25 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Sat, 7 Aug 2021 20:48:01 +0300 Subject: [PATCH 17/17] fix: phase approval migration script --- migrations/20210802_project_phase_approval_table.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/20210802_project_phase_approval_table.sql b/migrations/20210802_project_phase_approval_table.sql index 19633050..343ddf87 100644 --- a/migrations/20210802_project_phase_approval_table.sql +++ b/migrations/20210802_project_phase_approval_table.sql @@ -6,7 +6,7 @@ CREATE SEQUENCE project_phase_approval_id_seq CACHE 1; DROP TYPE IF EXISTS "enum_project_phase_approval_decision"; -CREATE TYPE "enum_project_phase_approval_decision" AS ENUM ('approve, reject'); +CREATE TYPE "enum_project_phase_approval_decision" AS ENUM ('approve', 'reject'); CREATE TABLE "project_phase_approval" ( "id" int8 NOT NULL DEFAULT nextval('project_phase_approval_id_seq'::regclass),