From 1bccaa1991d2171261b570949584433d0ebe279b Mon Sep 17 00:00:00 2001 From: eisbilir Date: Mon, 19 Jul 2021 02:35:06 +0300 Subject: [PATCH] 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(); + } + }); + }); }); });