From d53dfedf0d41efd098750639794d42be32f946a1 Mon Sep 17 00:00:00 2001 From: sachin-maheshwari Date: Mon, 27 May 2019 17:51:04 +0530 Subject: [PATCH] Revert "V5 API standards" --- config/default.js | 1 - docs/swagger_api.yaml | 625 ++++++++-------- ...ication-server-api.postman_collection.json | 619 ++++++++-------- package.json | 5 +- src/common/tcApiHelper.js | 50 -- src/controllers/NotificationController.js | 23 +- src/routes.js | 10 - src/services/NotificationService.js | 665 ++++++++---------- 8 files changed, 879 insertions(+), 1119 deletions(-) diff --git a/config/default.js b/config/default.js index 5ec1698..d2f85b6 100644 --- a/config/default.js +++ b/config/default.js @@ -32,7 +32,6 @@ module.exports = { TC_API_V4_BASE_URL: process.env.TC_API_V4_BASE_URL || '', TC_API_V5_BASE_URL: process.env.TC_API_V5_BASE_URL || '', API_CONTEXT_PATH: process.env.API_CONTEXT_PATH || '/v5/notifications', - API_BASE_URL: process.env.API_BASE_URL || 'http://api.topcoder-dev.com', // Configuration for generating machine to machine auth0 token. // The token will be used for calling another internal API. diff --git a/docs/swagger_api.yaml b/docs/swagger_api.yaml index 1e77b61..1afa80f 100644 --- a/docs/swagger_api.yaml +++ b/docs/swagger_api.yaml @@ -1,350 +1,275 @@ -swagger: "2.0" -info: - title: "TOPCODER NOTIFICATIONS SERIES - NOTIFICATIONS SERVER" - description: "TOPCODER NOTIFICATIONS SERIES - NOTIFICATIONS SERVER" - version: "1.0.0" -host: "localhost:4000" -basePath: "/v5/notifications" -schemes: - - "http" -securityDefinitions: - jwt: - type: apiKey - name: Authorization - in: header - description: JWT Authentication. Provide API Key in the form 'Bearer <token>'. - -paths: - /: - get: - description: - list notifications - produces: - - application/json - security: - - jwt: [] - parameters: - - name: page - in: query - description: The page - required: false - type: integer - format: int32 - - name: per_page - in: query - description: The number of rows served - required: false - type: integer - format: int32 - - name: platform - in: query - description: The platform - required: false - type: string - - name: type - in: query - description: The type - required: false - type: string - - name: read - in: query - description: The read flag, either 'true' or 'false' - required: false - type: string - responses: - 200: - description: OK - schema: - type: object - properties: - items: - type: array - items: - type: object - properties: - id: - type: integer - format: int64 - description: the notification id - userId: - type: integer - format: int64 - description: user id - type: - type: string - description: notification type - read: - type: boolean - description: read flag - seen: - type: boolean - description: seen flag - contents: - type: object - description: the event message in JSON format - createdAt: - type: string - description: created at date string - updatedAt: - type: string - description: updated at date string - page: - type: integer - format: int32 - description: the page - per_page: - type: integer - format: int32 - description: the per_page - totalCount: - type: integer - format: int32 - description: the total count - 401: - description: "Authentication failed." - schema: - $ref: "#/definitions/Error" - 500: - description: "Internal server error." - schema: - $ref: "#/definitions/Error" - /{id}: - patch: - description: - update notification - security: - - jwt: [] - parameters: - - in: path - name: id - description: notification id - required: true - type: integer - format: int64 - - in: body - name: body - description: notification payload - required: true - schema: - $ref: "#/definitions/NotificationUpdatePayload" - responses: - 200: - description: OK, the notification(s) are updated - schema: - $ref: "#/definitions/Notification" - 400: - description: "Invalid input" - schema: - $ref: "#/definitions/Error" - 401: - description: "authentication failed" - schema: - $ref: "#/definitions/Error" - 403: - description: "Action not allowed." - schema: - $ref: "#/definitions/Error" - 404: - description: "Notification is not found" - schema: - $ref: "#/definitions/Error" - 500: - description: "Internal server error." - schema: - $ref: "#/definitions/Error" - /{id}/read: - put: - description: - mark notification(s) as read, id can be single id or '-' separated ids - security: - - jwt: [] - parameters: - - in: path - name: id - description: notification id - required: true - type: integer - format: int64 - responses: - 200: - description: OK, the notification(s) are marked as read - 400: - description: "Invalid input" - schema: - $ref: "#/definitions/Error" - 401: - description: "authentication failed" - schema: - $ref: "#/definitions/Error" - 403: - description: "Action not allowed." - schema: - $ref: "#/definitions/Error" - 404: - description: "Notification is not found" - schema: - $ref: "#/definitions/Error" - 500: - description: "Internal server error." - schema: - $ref: "#/definitions/Error" - /read: - put: - description: - mark all notifications as read - security: - - jwt: [] - responses: - 200: - description: OK, all notifications are marked as read - 401: - description: "authentication failed" - schema: - $ref: "#/definitions/Error" - 500: - description: "Internal server error." - schema: - $ref: "#/definitions/Error" - /{id}/seen: - put: - description: - mark notification(s) as seen, id can be single id or '-' separated ids - security: - - jwt: [] - parameters: - - in: path - name: id - description: notification id - required: true - type: integer - format: int64 - responses: - 200: - description: OK, the notification(s) are marked as seen - 400: - description: "Invalid input" - schema: - $ref: "#/definitions/Error" - 401: - description: "authentication failed" - schema: - $ref: "#/definitions/Error" - 403: - description: "Action not allowed." - schema: - $ref: "#/definitions/Error" - 404: - description: "Notification is not found" - schema: - $ref: "#/definitions/Error" - 500: - description: "Internal server error." - schema: - $ref: "#/definitions/Error" - /settings: - get: - description: - get notification settings - produces: - - application/json - security: - - jwt: [] - responses: - 200: - description: OK. Each key is topic name, value is object of deliveryMethod - value mappings for the topic - schema: - type: object - 401: - description: "Authentication failed." - schema: - $ref: "#/definitions/Error" - 500: - description: "Internal server error." - schema: - $ref: "#/definitions/Error" - put: - description: - update notification settings - consumes: - - application/json - security: - - jwt: [] - parameters: - - in: body - name: body - description: notification settings - required: true - schema: - type: array - items: - type: object - properties: - topic: - type: string - description: the topic - deliveryMethod: - type: string - description: the delivery method - value: - type: string - description: the value for the delivery method - responses: - 200: - description: OK - 400: - description: "Invalid input" - schema: - $ref: "#/definitions/Error" - 401: - description: "authentication failed" - schema: - $ref: "#/definitions/Error" - 500: - description: "Internal server error." - schema: - $ref: "#/definitions/Error" - -definitions: - NotificationUpdatePayload: - properties: - read: - type: boolean - seen: - type: boolean - Notification: - properties: - id: - type: integer - userId: - type: integer - type: - type: string - contents: - type: object - version: - type: integer - read: - type: boolean - seen: - type: boolean - createdAt: - type: string - updatedAt: - type: string - Error: - properties: - error: - type: string - details: - type: array - items: - type: object - properties: - message: - type: string - path: - type: string - type: - type: string - context: - type: object +swagger: "2.0" +info: + title: "TOPCODER NOTIFICATIONS SERIES - NOTIFICATIONS SERVER" + description: "TOPCODER NOTIFICATIONS SERIES - NOTIFICATIONS SERVER" + version: "1.0.0" +host: "localhost:4000" +basePath: "/v5/notifications" +schemes: +- "http" +securityDefinitions: + jwt: + type: apiKey + name: Authorization + in: header + description: JWT Authentication. Provide API Key in the form 'Bearer <token>'. + +paths: + /list: + get: + description: + list notifications + produces: + - application/json + security: + - jwt: [] + parameters: + - name: offset + in: query + description: The offset + required: false + type: integer + format: int32 + - name: limit + in: query + description: The limit + required: false + type: integer + format: int32 + - name: type + in: query + description: The type + required: false + type: string + - name: read + in: query + description: The read flag, either 'true' or 'false' + required: false + type: string + responses: + 200: + description: OK + schema: + type: object + properties: + items: + type: array + items: + type: object + properties: + id: + type: integer + format: int64 + description: the notification id + userId: + type: integer + format: int64 + description: user id + type: + type: string + description: notification type + read: + type: boolean + description: read flag + seen: + type: boolean + description: seen flag + contents: + type: object + description: the event message in JSON format + createdAt: + type: string + description: created at date string + updatedAt: + type: string + description: updated at date string + offset: + type: integer + format: int32 + description: the offset + limit: + type: integer + format: int32 + description: the limit + totalCount: + type: integer + format: int32 + description: the total count + 401: + description: "Authentication failed." + schema: + $ref: "#/definitions/Error" + 500: + description: "Internal server error." + schema: + $ref: "#/definitions/Error" + /{id}/read: + put: + description: + mark notification(s) as read, id can be single id or '-' separated ids + security: + - jwt: [] + parameters: + - in: path + name: id + description: notification id + required: true + type: integer + format: int64 + responses: + 200: + description: OK, the notification(s) are marked as read + 400: + description: "Invalid input" + schema: + $ref: "#/definitions/Error" + 401: + description: "authentication failed" + schema: + $ref: "#/definitions/Error" + 403: + description: "Action not allowed." + schema: + $ref: "#/definitions/Error" + 404: + description: "Notification is not found" + schema: + $ref: "#/definitions/Error" + 500: + description: "Internal server error." + schema: + $ref: "#/definitions/Error" + /read: + put: + description: + mark all notifications as read + security: + - jwt: [] + responses: + 200: + description: OK, all notifications are marked as read + 401: + description: "authentication failed" + schema: + $ref: "#/definitions/Error" + 500: + description: "Internal server error." + schema: + $ref: "#/definitions/Error" + /{id}/seen: + put: + description: + mark notification(s) as seen, id can be single id or '-' separated ids + security: + - jwt: [] + parameters: + - in: path + name: id + description: notification id + required: true + type: integer + format: int64 + responses: + 200: + description: OK, the notification(s) are marked as seen + 400: + description: "Invalid input" + schema: + $ref: "#/definitions/Error" + 401: + description: "authentication failed" + schema: + $ref: "#/definitions/Error" + 403: + description: "Action not allowed." + schema: + $ref: "#/definitions/Error" + 404: + description: "Notification is not found" + schema: + $ref: "#/definitions/Error" + 500: + description: "Internal server error." + schema: + $ref: "#/definitions/Error" + /settings: + get: + description: + get notification settings + produces: + - application/json + security: + - jwt: [] + responses: + 200: + description: OK. Each key is topic name, value is object of deliveryMethod - value mappings for the topic + schema: + type: object + 401: + description: "Authentication failed." + schema: + $ref: "#/definitions/Error" + 500: + description: "Internal server error." + schema: + $ref: "#/definitions/Error" + put: + description: + update notification settings + consumes: + - application/json + security: + - jwt: [] + parameters: + - in: body + name: body + description: notification settings + required: true + schema: + type: array + items: + type: object + properties: + topic: + type: string + description: the topic + deliveryMethod: + type: string + description: the delivery method + value: + type: string + description: the value for the delivery method + responses: + 200: + description: OK + 400: + description: "Invalid input" + schema: + $ref: "#/definitions/Error" + 401: + description: "authentication failed" + schema: + $ref: "#/definitions/Error" + 500: + description: "Internal server error." + schema: + $ref: "#/definitions/Error" + +definitions: + Error: + properties: + error: + type: string + details: + type: array + items: + type: object + properties: + message: + type: string + path: + type: string + type: + type: string + context: + type: object diff --git a/docs/tc-notification-server-api.postman_collection.json b/docs/tc-notification-server-api.postman_collection.json index 7267bf8..f7c65c0 100644 --- a/docs/tc-notification-server-api.postman_collection.json +++ b/docs/tc-notification-server-api.postman_collection.json @@ -1,108 +1,83 @@ { - "id": "8fe3fe1d-744f-4c0e-ac34-e2029e6308e9", + "id": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", "name": "tc-notification-server-api", "description": "", "auth": null, "events": null, - "variables": [], + "variables": null, "order": [ - "68de66b4-c2c6-4b3b-9df4-42f7cf4c4fa3", - "c31aa2cc-e377-4b59-a4ee-9e5181449996", - "4dc587b5-2da8-4055-8317-88f7a677eb34", - "e323baef-7406-4665-9bbe-3f64ce4a427c", - "af4744ee-ceba-4a35-a14a-eb38290139fb", - "0012295a-9139-47bb-91b3-1f53d94bd928", - "8981f01c-fd95-4d19-abcf-36265682a610" + "19332a51-03e8-4f5c-8f85-4d28d6dfe6f4", + "543cab06-2c7d-4aed-8cf3-0808463254d5", + "76779830-a8a4-4636-8c03-1801b3d1863d", + "cb2299a5-dac7-4c40-80c4-7b1694138354", + "d57ba947-a5e7-410a-b978-76882f33c86e", + "fce69847-5bf8-4b07-bcaf-6352db4ba923" ], "folders_order": [ - "060feceb-1658-4fee-9d71-0fedc75effe9" + "dbebd550-6c33-4778-b467-d56decf16c91" ], "folders": [ { - "id": "060feceb-1658-4fee-9d71-0fedc75effe9", + "id": "dbebd550-6c33-4778-b467-d56decf16c91", "name": "failure", "description": "", "auth": null, "events": null, - "collection": "8fe3fe1d-744f-4c0e-ac34-e2029e6308e9", + "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", "folder": null, "order": [ - "47aaac74-b661-48d6-bed1-d61efd13b668", - "c2d28896-3208-4326-a65e-1718c1f891c5", - "c8bb53de-91fc-443c-a8fc-d13f503d0e5f", - "208a8afb-8287-4b93-88fd-67320d0a7f0f", - "021b87b0-caca-41e9-85ea-16a9d09bab22", - "334aae9e-38fc-425e-9649-e09c6188ddc2", - "9445564c-74bb-4599-8eca-ae292d5b37fc", - "88038b95-0a16-4a36-8835-0713c731e433" + "1b3b6480-ea94-4027-8898-f82f28e2bea6", + "59fc9f2b-28c5-4cff-b21b-11ab51bf67d8", + "cbc03cb1-6dfe-43fd-8e99-8c56923c2978", + "d293d2c5-230d-4f34-8c97-1adc1f2f89b4", + "da23d550-55b3-4f7d-9131-735956d62f6d", + "f2246cf7-7aae-4ea0-9d92-1d932d340302", + "f3f3a847-46f6-4059-b167-b436078fb112" ], "folders_order": [] } ], "requests": [ { - "id": "0012295a-9139-47bb-91b3-1f53d94bd928", - "name": "updateNotification", - "url": "{{URL}}/1", + "id": "19332a51-03e8-4f5c-8f85-4d28d6dfe6f4", + "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", + "name": "getSettings", + "url": "{{URL}}/settings", "description": "", "data": [], "dataMode": "raw", "headerData": [ { - "description": "", - "enabled": true, "key": "Content-Type", - "value": "application/json" + "value": "application/json", + "description": "", + "enabled": true }, { - "description": "", - "enabled": true, "key": "Authorization", - "value": "Bearer {{TOKEN}}" + "value": "Bearer {{TOKEN}}", + "description": "", + "enabled": true } ], - "method": "PATCH", + "method": "GET", "pathVariableData": [], "queryParams": [], "auth": null, "events": null, "folder": null, - "rawModeData": "{\n\t\"read\": true\n}", + "rawModeData": "", "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", "pathVariables": {} }, { - "id": "021b87b0-caca-41e9-85ea-16a9d09bab22", - "name": "markAllRead - missing token", - "url": "{{URL}}/read", + "id": "1b3b6480-ea94-4027-8898-f82f28e2bea6", + "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", + "name": "listNotifications - invalid read filter", + "url": "{{URL}}/list?offset=0&limit=20&type=notifications.connect.project.updated&read=yes", "description": "", "data": [], "dataMode": "raw", - "headerData": [ - { - "key": "Content-Type", - "value": "application/json", - "description": "", - "enabled": false - } - ], - "method": "PUT", - "pathVariableData": [], - "queryParams": [], - "auth": null, - "events": null, - "folder": "060feceb-1658-4fee-9d71-0fedc75effe9", - "rawModeData": "", - "headers": "//Content-Type: application/json\n", - "pathVariables": {} - }, - { - "id": "208a8afb-8287-4b93-88fd-67320d0a7f0f", - "name": "listNotifications - invalid limit", - "url": "{{URL}}/list?offset=0&limit=abc&type=notifications.connect.project.updated", - "description": "", - "data": null, - "dataMode": null, "headerData": [ { "key": "Content-Type", @@ -129,7 +104,7 @@ }, { "key": "limit", - "value": "abc", + "value": "20", "equals": true, "description": "", "enabled": true @@ -143,22 +118,24 @@ }, { "key": "read", - "value": "false", + "value": "yes", "equals": true, "description": "", - "enabled": false + "enabled": true } ], "auth": null, "events": null, - "folder": "060feceb-1658-4fee-9d71-0fedc75effe9", + "folder": "dbebd550-6c33-4778-b467-d56decf16c91", + "rawModeData": "", "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", "pathVariables": {} }, { - "id": "334aae9e-38fc-425e-9649-e09c6188ddc2", - "name": "updateSettings - invalid body", - "url": "{{URL}}/settings", + "id": "543cab06-2c7d-4aed-8cf3-0808463254d5", + "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", + "name": "markAllRead", + "url": "{{URL}}/read", "description": "", "data": [], "dataMode": "raw", @@ -167,7 +144,7 @@ "key": "Content-Type", "value": "application/json", "description": "", - "enabled": true + "enabled": false }, { "key": "Authorization", @@ -181,18 +158,19 @@ "queryParams": [], "auth": null, "events": null, - "folder": "060feceb-1658-4fee-9d71-0fedc75effe9", - "rawModeData": "[\n\t{\n\t\t\"wrong\": \"notifications.connect.project.created\",\n\t\t\"deliveryMethod\": 123,\n\t\t\"value\": \"on\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.project.created\",\n\t\t\"deliveryMethod\": \"sms\",\n\t\t\"value\": \"off\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.project.created\",\n\t\t\"deliveryMethod\": \"web\",\n\t\t\"value\": \"off\"\n\t},\n\t{\n\t\t\"topic\": \"notifications.connect.message.posted\",\n\t\t\"deliveryMethod\": \"email\",\n\t\t\"value\": \"off\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.message.posted\",\n\t\t\"deliveryMethod\": \"sms\",\n\t\t\"value\": \"on\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.message.posted\",\n\t\t\"deliveryMethod\": \"web\",\n\t\t\"value\": \"on\"\n\t}\n]", - "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", + "folder": null, + "rawModeData": "", + "headers": "//Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", "pathVariables": {} }, { - "id": "47aaac74-b661-48d6-bed1-d61efd13b668", - "name": "listNotifications - invalid read filter", - "url": "{{URL}}/list?offset=0&limit=20&type=notifications.connect.project.updated&read=yes", + "id": "59fc9f2b-28c5-4cff-b21b-11ab51bf67d8", + "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", + "name": "getSettings - invalid token", + "url": "{{URL}}/settings", "description": "", - "data": null, - "dataMode": null, + "data": [], + "dataMode": "raw", "headerData": [ { "key": "Content-Type", @@ -202,51 +180,24 @@ }, { "key": "Authorization", - "value": "Bearer {{TOKEN}}", + "value": "Bearer invalid", "description": "", "enabled": true } ], "method": "GET", "pathVariableData": [], - "queryParams": [ - { - "key": "offset", - "value": "0", - "equals": true, - "description": "", - "enabled": true - }, - { - "key": "limit", - "value": "20", - "equals": true, - "description": "", - "enabled": true - }, - { - "key": "type", - "value": "notifications.connect.project.updated", - "equals": true, - "description": "", - "enabled": true - }, - { - "key": "read", - "value": "yes", - "equals": true, - "description": "", - "enabled": true - } - ], + "queryParams": [], "auth": null, "events": null, - "folder": "060feceb-1658-4fee-9d71-0fedc75effe9", - "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", + "folder": "dbebd550-6c33-4778-b467-d56decf16c91", + "rawModeData": "", + "headers": "Content-Type: application/json\nAuthorization: Bearer invalid\n", "pathVariables": {} }, { - "id": "4dc587b5-2da8-4055-8317-88f7a677eb34", + "id": "76779830-a8a4-4636-8c03-1801b3d1863d", + "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", "name": "markAsRead", "url": "{{URL}}/1/read", "description": "", @@ -277,12 +228,13 @@ "pathVariables": {} }, { - "id": "68de66b4-c2c6-4b3b-9df4-42f7cf4c4fa3", - "name": "getSettings", - "url": "{{URL}}/settings", + "id": "cb2299a5-dac7-4c40-80c4-7b1694138354", + "name": "TC API - get project", + "url": "https://api.topcoder-dev.com/v4/projects/1936", + "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", "description": "", - "data": null, - "dataMode": null, + "data": [], + "dataMode": "raw", "headerData": [ { "key": "Content-Type", @@ -291,8 +243,8 @@ "enabled": true }, { - "key": "Authorization", - "value": "Bearer {{TOKEN}}", + "key": "authorization", + "value": "Bearer {{TC_ADMIN_TOKEN}}", "description": "", "enabled": true } @@ -303,47 +255,143 @@ "auth": null, "events": null, "folder": null, - "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", + "responses": [ + { + "id": "ae658c70-e29d-4d49-aefd-944af0e4f811", + "name": "test111", + "status": "", + "mime": "", + "language": "json", + "text": "{\"id\":\"95744bd2-2830-4014-8885-7182a6225953\",\"version\":\"v4\",\"result\":{\"success\":true,\"status\":200,\"content\":{\"id\":1936,\"directProjectId\":12147,\"billingAccountId\":null,\"name\":\"Test-prj\",\"description\":\"Test description\",\"external\":null,\"bookmarks\":[],\"estimatedPrice\":null,\"actualPrice\":null,\"terms\":[],\"type\":\"app_dev\",\"status\":\"draft\",\"details\":{\"products\":[\"api_dev\"],\"appDefinition\":{\"primaryTarget\":\"desktop\",\"goal\":{\"value\":\"Goal\"},\"users\":{\"value\":\"Developers\"},\"notes\":\"Notes\"},\"utm\":{},\"hideDiscussions\":true},\"challengeEligibility\":[],\"cancelReason\":null,\"createdAt\":\"2017-11-01T15:45:51.000Z\",\"updatedAt\":\"2017-11-01T15:45:51.000Z\",\"createdBy\":305384,\"updatedBy\":305384,\"members\":[{\"id\":2997,\"userId\":305384,\"role\":\"customer\",\"isPrimary\":true,\"createdAt\":\"2017-11-01T15:45:51.000Z\",\"updatedAt\":\"2017-11-01T15:45:51.000Z\",\"createdBy\":305384,\"updatedBy\":305384,\"projectId\":1936}],\"attachments\":[]},\"metadata\":{\"totalCount\":1}}}", + "responseCode": { + "code": 200, + "name": "OK", + "detail": "Standard response for successful HTTP requests. The actual response will depend on the request method used. In a GET request, the response will contain an entity corresponding to the requested resource. In a POST request the response will contain an entity describing or containing the result of the action." + }, + "requestObject": null, + "headers": [ + { + "name": "access-control-allow-credentials", + "key": "access-control-allow-credentials", + "value": "true", + "description": "Indicates whether or not the response to the request can be exposed when the credentials flag is true. When used as part of a response to a preflight request, this indicates whether or not the actual request can be made using credentials." + }, + { + "name": "access-control-allow-headers", + "key": "access-control-allow-headers", + "value": "Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since", + "description": "Used in response to a preflight request to indicate which HTTP headers can be used when making the actual request." + }, + { + "name": "access-control-allow-methods", + "key": "access-control-allow-methods", + "value": "GET, POST, OPTIONS, DELETE, PUT, PATCH", + "description": "Specifies the method or methods allowed when accessing the resource. This is used in response to a preflight request." + }, + { + "name": "connection", + "key": "connection", + "value": "keep-alive", + "description": "Options that are desired for the connection" + }, + { + "name": "content-encoding", + "key": "content-encoding", + "value": "gzip", + "description": "The type of encoding used on the data." + }, + { + "name": "content-length", + "key": "content-length", + "value": "491", + "description": "The length of the response body in octets (8-bit bytes)" + }, + { + "name": "content-type", + "key": "content-type", + "value": "application/json; charset=utf-8", + "description": "The mime type of this content" + }, + { + "name": "date", + "key": "date", + "value": "Thu, 02 Nov 2017 04:28:20 GMT", + "description": "The date and time that the message was sent" + }, + { + "name": "etag", + "key": "etag", + "value": "W/\"3a6-4pbtTNq19Shn10rc0k+HRsoAyMw\"", + "description": "An identifier for a specific version of a resource, often a message digest" + }, + { + "name": "server", + "key": "server", + "value": "nginx/1.9.7", + "description": "A name for the server" + }, + { + "name": "x-powered-by", + "key": "x-powered-by", + "value": "Express", + "description": "Specifies the technology (ASP.NET, PHP, JBoss, e.g.) supporting the web application (version details are often in X-Runtime, X-Version, or X-AspNet-Version)" + }, + { + "name": "x-request-id", + "key": "x-request-id", + "value": "95744bd2-2830-4014-8885-7182a6225953", + "description": "Custom header" + } + ], + "cookies": [], + "request": "cb2299a5-dac7-4c40-80c4-7b1694138354", + "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452" + } + ], + "rawModeData": "", + "headers": "Content-Type: application/json\nauthorization: Bearer {{TC_ADMIN_TOKEN}}\n", "pathVariables": {} }, { - "id": "88038b95-0a16-4a36-8835-0713c731e433", - "name": "updateNotification - invalid action", - "url": "{{URL}}/1", + "id": "cbc03cb1-6dfe-43fd-8e99-8c56923c2978", + "name": "markAsRead - not found", + "url": "{{URL}}/1111111/read", "description": "", + "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", "data": [], "dataMode": "raw", "headerData": [ { - "description": "", - "enabled": true, "key": "Content-Type", - "value": "application/json" + "value": "application/json", + "description": "", + "enabled": false }, { - "description": "", - "enabled": true, "key": "Authorization", - "value": "Bearer {{TOKEN}}" + "value": "Bearer {{TOKEN}}", + "description": "", + "enabled": true } ], - "method": "PATCH", + "method": "PUT", "pathVariableData": [], "queryParams": [], "auth": null, "events": null, - "folder": "060feceb-1658-4fee-9d71-0fedc75effe9", - "rawModeData": "{\n\t\"read\": true,\n\t\"seen\": false\n}", - "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", + "folder": "dbebd550-6c33-4778-b467-d56decf16c91", + "rawModeData": "", + "headers": "//Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", "pathVariables": {} }, { - "id": "8981f01c-fd95-4d19-abcf-36265682a610", - "name": "listNotifications", - "url": "{{URL}}?page=1&per_page=20&platform=connect", + "id": "d293d2c5-230d-4f34-8c97-1adc1f2f89b4", + "name": "listNotifications - invalid limit", + "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", + "url": "{{URL}}/list?offset=0&limit=abc&type=notifications.connect.project.updated", "description": "", - "data": null, - "dataMode": null, + "data": [], + "dataMode": "raw", "headerData": [ { "key": "Content-Type", @@ -362,74 +410,22 @@ "pathVariableData": [], "queryParams": [ { - "key": "page", - "value": "1", - "equals": true, - "description": "", - "enabled": true - }, - { - "key": "per_page", - "value": "20", - "equals": true, - "description": "", - "enabled": true - }, - { - "key": "platform", - "value": "connect", + "key": "offset", + "value": "0", "equals": true, "description": "", "enabled": true }, { - "key": "read", - "value": "false", - "equals": true, - "description": "", - "enabled": false - } - ], - "auth": null, - "events": null, - "folder": null, - "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", - "pathVariables": {} - }, - { - "id": "9445564c-74bb-4599-8eca-ae292d5b37fc", - "name": "listNotifications - invalid page", - "url": "{{URL}}/list?page=-1&per_page=20", - "description": "", - "data": null, - "dataMode": null, - "headerData": [ - { - "description": "", - "enabled": true, - "key": "Content-Type", - "value": "application/json" - }, - { - "description": "", - "enabled": true, - "key": "Authorization", - "value": "Bearer {{TOKEN}}" - } - ], - "method": "GET", - "pathVariableData": [], - "queryParams": [ - { - "key": "page", - "value": "-1", + "key": "limit", + "value": "abc", "equals": true, "description": "", "enabled": true }, { - "key": "per_page", - "value": "20", + "key": "type", + "value": "notifications.connect.project.updated", "equals": true, "description": "", "enabled": true @@ -444,14 +440,16 @@ ], "auth": null, "events": null, - "folder": "060feceb-1658-4fee-9d71-0fedc75effe9", + "folder": "dbebd550-6c33-4778-b467-d56decf16c91", + "rawModeData": "", "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", "pathVariables": {} }, { - "id": "af4744ee-ceba-4a35-a14a-eb38290139fb", + "id": "d57ba947-a5e7-410a-b978-76882f33c86e", "name": "updateSettings", "url": "{{URL}}/settings", + "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", "description": "", "data": [], "dataMode": "raw", @@ -480,39 +478,36 @@ "pathVariables": {} }, { - "id": "c2d28896-3208-4326-a65e-1718c1f891c5", - "name": "getSettings - invalid token", - "url": "{{URL}}/settings", + "id": "da23d550-55b3-4f7d-9131-735956d62f6d", + "name": "markAllRead - missing token", + "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", + "url": "{{URL}}/read", "description": "", - "data": null, - "dataMode": null, + "data": [], + "dataMode": "raw", "headerData": [ { "key": "Content-Type", "value": "application/json", "description": "", - "enabled": true - }, - { - "key": "Authorization", - "value": "Bearer invalid", - "description": "", - "enabled": true + "enabled": false } ], - "method": "GET", + "method": "PUT", "pathVariableData": [], "queryParams": [], "auth": null, "events": null, - "folder": "060feceb-1658-4fee-9d71-0fedc75effe9", - "headers": "Content-Type: application/json\nAuthorization: Bearer invalid\n", + "folder": "dbebd550-6c33-4778-b467-d56decf16c91", + "rawModeData": "", + "headers": "//Content-Type: application/json\n", "pathVariables": {} }, { - "id": "c31aa2cc-e377-4b59-a4ee-9e5181449996", - "name": "markAllRead", - "url": "{{URL}}/read", + "id": "f2246cf7-7aae-4ea0-9d92-1d932d340302", + "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", + "name": "updateSettings - invalid body", + "url": "{{URL}}/settings", "description": "", "data": [], "dataMode": "raw", @@ -521,7 +516,7 @@ "key": "Content-Type", "value": "application/json", "description": "", - "enabled": false + "enabled": true }, { "key": "Authorization", @@ -535,15 +530,16 @@ "queryParams": [], "auth": null, "events": null, - "folder": null, - "rawModeData": "", - "headers": "//Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", + "folder": "dbebd550-6c33-4778-b467-d56decf16c91", + "rawModeData": "[\n\t{\n\t\t\"wrong\": \"notifications.connect.project.created\",\n\t\t\"deliveryMethod\": 123,\n\t\t\"value\": \"on\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.project.created\",\n\t\t\"deliveryMethod\": \"sms\",\n\t\t\"value\": \"off\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.project.created\",\n\t\t\"deliveryMethod\": \"web\",\n\t\t\"value\": \"off\"\n\t},\n\t{\n\t\t\"topic\": \"notifications.connect.message.posted\",\n\t\t\"deliveryMethod\": \"email\",\n\t\t\"value\": \"off\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.message.posted\",\n\t\t\"deliveryMethod\": \"sms\",\n\t\t\"value\": \"on\"\n\t}, {\n\t\t\"topic\": \"notifications.connect.message.posted\",\n\t\t\"deliveryMethod\": \"web\",\n\t\t\"value\": \"on\"\n\t}\n]", + "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", "pathVariables": {} }, { - "id": "c8bb53de-91fc-443c-a8fc-d13f503d0e5f", - "name": "markAsRead - not found", - "url": "{{URL}}/1111111/read", + "id": "f3f3a847-46f6-4059-b167-b436078fb112", + "name": "listNotifications - invalid offset", + "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", + "url": "{{URL}}/list?offset=-1&limit=20&type=notifications.connect.project.updated", "description": "", "data": [], "dataMode": "raw", @@ -552,7 +548,7 @@ "key": "Content-Type", "value": "application/json", "description": "", - "enabled": false + "enabled": true }, { "key": "Authorization", @@ -561,23 +557,53 @@ "enabled": true } ], - "method": "PUT", + "method": "GET", "pathVariableData": [], - "queryParams": [], + "queryParams": [ + { + "key": "offset", + "value": "-1", + "equals": true, + "description": "", + "enabled": true + }, + { + "key": "limit", + "value": "20", + "equals": true, + "description": "", + "enabled": true + }, + { + "key": "type", + "value": "notifications.connect.project.updated", + "equals": true, + "description": "", + "enabled": true + }, + { + "key": "read", + "value": "false", + "equals": true, + "description": "", + "enabled": false + } + ], "auth": null, "events": null, - "folder": "060feceb-1658-4fee-9d71-0fedc75effe9", + "folder": "dbebd550-6c33-4778-b467-d56decf16c91", "rawModeData": "", - "headers": "//Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", + "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", "pathVariables": {} }, { - "id": "e323baef-7406-4665-9bbe-3f64ce4a427c", - "name": "TC API - get project", - "url": "https://api.topcoder-dev.com/v4/projects/1936", + "id": "fce69847-5bf8-4b07-bcaf-6352db4ba923", + "collectionId": "3f30c4e3-3b7a-491b-bdb2-6629d081a452", + "name": "listNotifications", + "url": "{{URL}}/list?offset=0&limit=20", "description": "", - "data": null, - "dataMode": null, + "data": [], + "dataMode": "raw", "headerData": [ { "key": "Content-Type", @@ -586,113 +612,50 @@ "enabled": true }, { - "key": "authorization", - "value": "Bearer {{TC_ADMIN_TOKEN}}", + "key": "Authorization", + "value": "Bearer {{TOKEN}}", "description": "", "enabled": true } ], "method": "GET", "pathVariableData": [], - "queryParams": [], - "auth": null, - "events": null, - "folder": null, - "responses": [ + "queryParams": [ { - "id": "3e0e5441-9c98-4a09-a256-9d825e2c76f8", - "name": "test111", - "status": "", - "mime": "", - "language": "json", - "text": "{\"id\":\"95744bd2-2830-4014-8885-7182a6225953\",\"version\":\"v4\",\"result\":{\"success\":true,\"status\":200,\"content\":{\"id\":1936,\"directProjectId\":12147,\"billingAccountId\":null,\"name\":\"Test-prj\",\"description\":\"Test description\",\"external\":null,\"bookmarks\":[],\"estimatedPrice\":null,\"actualPrice\":null,\"terms\":[],\"type\":\"app_dev\",\"status\":\"draft\",\"details\":{\"products\":[\"api_dev\"],\"appDefinition\":{\"primaryTarget\":\"desktop\",\"goal\":{\"value\":\"Goal\"},\"users\":{\"value\":\"Developers\"},\"notes\":\"Notes\"},\"utm\":{},\"hideDiscussions\":true},\"challengeEligibility\":[],\"cancelReason\":null,\"createdAt\":\"2017-11-01T15:45:51.000Z\",\"updatedAt\":\"2017-11-01T15:45:51.000Z\",\"createdBy\":305384,\"updatedBy\":305384,\"members\":[{\"id\":2997,\"userId\":305384,\"role\":\"customer\",\"isPrimary\":true,\"createdAt\":\"2017-11-01T15:45:51.000Z\",\"updatedAt\":\"2017-11-01T15:45:51.000Z\",\"createdBy\":305384,\"updatedBy\":305384,\"projectId\":1936}],\"attachments\":[]},\"metadata\":{\"totalCount\":1}}}", - "responseCode": { - "code": 200, - "name": "OK", - "detail": "Standard response for successful HTTP requests. The actual response will depend on the request method used. In a GET request, the response will contain an entity corresponding to the requested resource. In a POST request the response will contain an entity describing or containing the result of the action." - }, - "requestObject": null, - "headers": [ - { - "key": "access-control-allow-credentials", - "value": "true", - "name": "access-control-allow-credentials", - "description": "Indicates whether or not the response to the request can be exposed when the credentials flag is true. When used as part of a response to a preflight request, this indicates whether or not the actual request can be made using credentials." - }, - { - "key": "access-control-allow-headers", - "value": "Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since", - "name": "access-control-allow-headers", - "description": "Used in response to a preflight request to indicate which HTTP headers can be used when making the actual request." - }, - { - "key": "access-control-allow-methods", - "value": "GET, POST, OPTIONS, DELETE, PUT, PATCH", - "name": "access-control-allow-methods", - "description": "Specifies the method or methods allowed when accessing the resource. This is used in response to a preflight request." - }, - { - "key": "connection", - "value": "keep-alive", - "name": "connection", - "description": "Options that are desired for the connection" - }, - { - "key": "content-encoding", - "value": "gzip", - "name": "content-encoding", - "description": "The type of encoding used on the data." - }, - { - "key": "content-length", - "value": "491", - "name": "content-length", - "description": "The length of the response body in octets (8-bit bytes)" - }, - { - "key": "content-type", - "value": "application/json; charset=utf-8", - "name": "content-type", - "description": "The mime type of this content" - }, - { - "key": "date", - "value": "Thu, 02 Nov 2017 04:28:20 GMT", - "name": "date", - "description": "The date and time that the message was sent" - }, - { - "key": "etag", - "value": "W/\"3a6-4pbtTNq19Shn10rc0k+HRsoAyMw\"", - "name": "etag", - "description": "An identifier for a specific version of a resource, often a message digest" - }, - { - "key": "server", - "value": "nginx/1.9.7", - "name": "server", - "description": "A name for the server" - }, - { - "key": "x-powered-by", - "value": "Express", - "name": "x-powered-by", - "description": "Specifies the technology (ASP.NET, PHP, JBoss, e.g.) supporting the web application (version details are often in X-Runtime, X-Version, or X-AspNet-Version)" - }, - { - "key": "x-request-id", - "value": "95744bd2-2830-4014-8885-7182a6225953", - "name": "x-request-id", - "description": "Custom header" - } - ], - "cookies": [], - "request": "e323baef-7406-4665-9bbe-3f64ce4a427c", - "collection": "8fe3fe1d-744f-4c0e-ac34-e2029e6308e9" + "key": "offset", + "value": "0", + "equals": true, + "description": "", + "enabled": true + }, + { + "key": "limit", + "value": "20", + "equals": true, + "description": "", + "enabled": true + }, + { + "key": "type", + "value": "notifications.connect.project.updated", + "equals": true, + "description": "", + "enabled": false + }, + { + "key": "read", + "value": "false", + "equals": true, + "description": "", + "enabled": false } ], - "headers": "Content-Type: application/json\nauthorization: Bearer {{TC_ADMIN_TOKEN}}\n", + "auth": null, + "events": null, + "folder": null, + "rawModeData": "", + "headers": "Content-Type: application/json\nAuthorization: Bearer {{TOKEN}}\n", "pathVariables": {} } ] -} \ No newline at end of file +} diff --git a/package.json b/package.json index 3789de8..bff9422 100644 --- a/package.json +++ b/package.json @@ -53,9 +53,8 @@ "sequelize": "^4.21.0", "superagent": "^3.8.0", "tc-core-library-js": "appirio-tech/tc-core-library-js.git#v2.6", - "topcoder-healthcheck-dropin": "^1.0.3", - "urijs": "^1.19.1", - "winston": "^2.2.0" + "winston": "^2.2.0", + "topcoder-healthcheck-dropin": "^1.0.3" }, "engines": { "node": "6.x" diff --git a/src/common/tcApiHelper.js b/src/common/tcApiHelper.js index f039904..4d84987 100644 --- a/src/common/tcApiHelper.js +++ b/src/common/tcApiHelper.js @@ -2,7 +2,6 @@ * Contains generic helper methods for TC API */ const _ = require('lodash'); -const URI = require('urijs'); const config = require('config'); const request = require('superagent'); const m2mAuth = require('tc-core-library-js').auth.m2m; @@ -319,54 +318,6 @@ function* modifyNotificationNode(ruleSet, data) { return notification; } -/** - * generate header based on v5 specification - * @param {String} url the api url to fetch - * @param {Number} perPage the number served in one page - * @param {Number} currentPage the current page number - * @param {Number} total the total number of rows/entities - * - * @returns {Object} the header response - */ -function generateV5Header({ url, perPage, currentPage, total }) { - const links = []; - const fullUrl = `${config.API_BASE_URL}${url}`; - const generateUrl = (url_, page, rel) => { - const newUrl = new URI(url_); - newUrl.setQuery({ - page, - }); - links.push(`<${newUrl.toString()}>; rel="${rel}"`); - }; - - const totalPages = perPage ? Math.ceil(total / perPage) : 1; - const headers = { - 'X-Page': currentPage || 1, - 'X-Total': total, - 'X-Total-Pages': totalPages || 1, - }; - if (perPage) { - headers['X-Per-Page'] = perPage; - } - - if (currentPage > 1) { - headers['X-Prev-Page'] = currentPage - 1; - generateUrl(fullUrl, currentPage - 1, 'prev'); - generateUrl(fullUrl, 1, 'first'); - } - - if (currentPage < totalPages) { - headers['X-Next-Page'] = currentPage + 1; - - generateUrl(fullUrl, currentPage + 1, 'next'); - generateUrl(fullUrl, totalPages, 'last'); - } - - headers.Link = links.join(','); - - return headers; -} - module.exports = { getM2MToken, getUsersBySkills, @@ -378,5 +329,4 @@ module.exports = { getUsersInfoFromChallenge, filterChallengeUsers, modifyNotificationNode, - generateV5Header, }; diff --git a/src/controllers/NotificationController.js b/src/controllers/NotificationController.js index d4ce321..4b7988b 100644 --- a/src/controllers/NotificationController.js +++ b/src/controllers/NotificationController.js @@ -4,7 +4,6 @@ 'use strict'; const NotificationService = require('../services/NotificationService'); -const tcApiHelper = require('../common/tcApiHelper'); /** * List notifications. @@ -12,26 +11,7 @@ const tcApiHelper = require('../common/tcApiHelper'); * @param res the response */ function* listNotifications(req, res) { - const { - items, - perPage, - currentPage, - total, - } = yield NotificationService.listNotifications(req.query, req.user.userId); - - const headers = tcApiHelper.generateV5Header({ - url: req.originalUrl, - perPage, - currentPage, - total, - }); - - res.set(headers); - res.json(items); -} - -function* updateNotification(req, res) { - res.json(yield NotificationService.updateNotification(req.user.userId, req.params.id, req.body)); + res.json(yield NotificationService.listNotifications(req.query, req.user.userId)); } /** @@ -91,5 +71,4 @@ module.exports = { markAsSeen, getSettings, updateSettings, - updateNotification, }; diff --git a/src/routes.js b/src/routes.js index c3e2b97..cf1df2e 100644 --- a/src/routes.js +++ b/src/routes.js @@ -7,16 +7,6 @@ module.exports = { method: 'listNotifications', }, }, - '/:id': { - patch: { - controller: 'NotificationController', - method: 'updateNotification', - }, - post: { - controller: 'NotificationController', - method: 'updateNotification', - }, - }, '/:id/read': { put: { controller: 'NotificationController', diff --git a/src/services/NotificationService.js b/src/services/NotificationService.js index 7e85ad5..027116d 100644 --- a/src/services/NotificationService.js +++ b/src/services/NotificationService.js @@ -1,358 +1,313 @@ -/** - * Service for notification functinoalities. - */ - -'use strict'; - -const _ = require('lodash'); -const Joi = require('joi'); -const errors = require('../common/errors'); +/** + * Service for notification functinoalities. + */ + +'use strict'; + +const _ = require('lodash'); +const Joi = require('joi'); +const errors = require('../common/errors'); const logger = require('../common/logger'); -const models = require('../models'); - -const DEFAULT_LIMIT = 10; - -/** - * Get notification settings. - * @param {Number} userId the user id - * @returns {Object} the notification settings - */ -function* getSettings(userId) { - const notificationSettings = yield models.NotificationSetting.findAll({ where: { userId } }); - const serviceSettings = yield models.ServiceSettings.findAll({ where: { userId } }); - - // format settings per notification type - const notifications = {}; - _.each(notificationSettings, (setting) => { - if (!notifications[setting.topic]) { - notifications[setting.topic] = {}; - } - if (!notifications[setting.topic][setting.serviceId]) { - notifications[setting.topic][setting.serviceId] = {}; - } - notifications[setting.topic][setting.serviceId][setting.name] = setting.value; - }); - - // format settings per service - const services = {}; - _.each(serviceSettings, (setting) => { - if (!services[setting.serviceId]) { - services[setting.serviceId] = {}; - } - services[setting.serviceId][setting.name] = setting.value; - }); - return { - notifications, - services, - }; -} - -getSettings.schema = { - userId: Joi.number().required(), -}; - -/** - * Save notification setting entry. If the entry is not found, it will be created; otherwise it will be updated. - * @param {Object} entry the notification setting entry - * @param {Number} userId the user id - */ -function* saveNotificationSetting(entry, userId) { - const setting = yield models.NotificationSetting.findOne({ where: { - userId, topic: entry.topic, serviceId: entry.serviceId, name: entry.name } }); - if (setting) { - setting.value = entry.value; - yield setting.save(); - } else { - yield models.NotificationSetting.create({ - userId, - topic: entry.topic, - serviceId: entry.serviceId, - name: entry.name, - value: entry.value, - }); - } -} - -/** - * Save service setting entry. If the entry is not found, it will be created; otherwise it will be updated. - * @param {Object} entry the service setting entry - * @param {Number} userId the user id - */ -function* saveServiceSetting(entry, userId) { - const setting = yield models.ServiceSettings.findOne({ where: { - userId, serviceId: entry.serviceId, name: entry.name } }); - if (setting) { - setting.value = entry.value; - yield setting.save(); - } else { - yield models.ServiceSettings.create({ - userId, - serviceId: entry.serviceId, - name: entry.name, - value: entry.value, - }); - } -} - -/** - * Update notification settings. Un-specified settings are not changed. - * @param {Array} data the notification settings data - * @param {Number} userId the user id - */ -function* updateSettings(data, userId) { - // convert notification settings object to the list of entries - const notifications = []; - _.forOwn(data.notifications, (notification, topic) => { - _.forOwn(notification, (serviceSettings, serviceId) => { - _.forOwn(serviceSettings, (value, name) => { - notifications.push({ - topic, - serviceId, - name, - value, - }); - }); - }); - }); - - // validation - // there should be no duplicate (topic + serviceId + name) - const triples = {}; - notifications.forEach((entry) => { - const key = `${entry.topic} | ${entry.serviceId} | ${entry.name}`; - if (triples[key]) { - throw new errors.BadRequestError(`There are duplicate data for topic: ${ - entry.topic}, serviceId: ${entry.serviceId}, name: ${entry.name}`); - } - triples[key] = entry; - }); - - // save each entry in parallel - yield _.map(notifications, (entry) => saveNotificationSetting(entry, userId)); - - // convert services settings object the the list of entries - const services = []; - _.forOwn(data.services, (service, serviceId) => { - _.forOwn(service, (value, name) => { - services.push({ - serviceId, - name, - value, - }); - }); - }); - - // validation - // there should be no duplicate (serviceId + name) - const paris = {}; - services.forEach((entry) => { - const key = `${entry.serviceId} | ${entry.name}`; - if (paris[key]) { - throw new errors.BadRequestError('There are duplicate data for' - + ` serviceId: ${entry.serviceId}, name: ${entry.name}`); - } - paris[key] = entry; - }); - - yield _.map(services, (entry) => saveServiceSetting(entry, userId)); -} - -updateSettings.schema = { - data: Joi.object().keys({ - notifications: Joi.object(), - services: Joi.object(), - }).required(), - userId: Joi.number().required(), -}; - -/** - * List notifications. - * - * This method returns only notifications for 'web' - * Also this method filters notifications by the user and filters out notifications, - * which user disabled in his settings. - * - * @param {Object} query the query parameters - * @param {Number} userId the user id - * @returns {Object} the search result - */ -function* listNotifications(query, userId) { - const settings = yield getSettings(userId); - const notificationSettings = settings.notifications; - const limit = query.per_page; - const offset = (query.page - 1) * limit; - const filter = { where: { - userId, - }, offset, limit, order: [['createdAt', 'DESC']] }; - if (query.platform) { - filter.where.type = { $like: `notifications\.${query.platform}\.%` }; - } - if (_.keys(notificationSettings).length > 0) { - // only filter out notifications types which were explicitly set to 'no' - so we return notification by default - const notifications = _.keys(notificationSettings).filter((notificationType) => - !notificationSettings[notificationType] && - !notificationSettings[notificationType].web && - notificationSettings[notificationType].web.enabled === 'no' - ); - filter.where.type = Object.assign(filter.where.type || {}, { $notIn: notifications }); - } - if (query.type) { - filter.where.type = Object.assign(filter.where.type || {}, { $eq: query.type }); - } - if (query.read) { - filter.where.read = (query.read === 'true'); - } - const docs = yield models.Notification.findAndCountAll(filter); - const items = _.map(docs.rows, r => { - const item = r.toJSON(); - // id and userId are BIGINT in database, sequelize maps them to string values, - // convert them back to Number values - item.id = Number(item.id); - item.userId = Number(item.userId); - return item; - }); - return { - items, - perPage: query.per_page, - currentPage: query.page, - total: docs.count, - }; -} - -listNotifications.schema = { - query: Joi.object().keys({ - page: Joi.number().integer().min(1).default(1), - per_page: Joi.number().integer().min(1).default(DEFAULT_LIMIT), - type: Joi.string(), - platform: Joi.string(), - // when it is true, return only read notifications - // when it is false, return only un-read notifications - // when it is no provided, no read flag filtering - read: Joi.string().valid('true', 'false'), - }).required(), - userId: Joi.number().required(), -}; - -/** - * Update notification. - * - * Update notification based on notification id - * - * @param {Number} userId the user id - * @param {Number} notificationId the notification id - * @param {Object} payload the update notification payload - * @returns {Object} the updated notification - */ -function* updateNotification(userId, notificationId, payload) { - if (payload.read === false) { - throw new errors.ValidationError('Cannot set notification to be unread'); - } - if (payload.seen === false) { - throw new errors.ValidationError('Cannot set notification to be unseen'); - } - - const entity = yield models.Notification.findOne({ where: { id: Number(notificationId) } }); - if (!entity) { - throw new errors.NotFoundError(`Cannot find Notification where id = ${notificationId}`); - } - if (Number(entity.userId) !== userId) { - throw new errors.ForbiddenError(`Cannot access Notification where id = ${entity.id}`); - } - yield models.Notification.update(payload, { where: { id: Number(notificationId), userId: Number(userId) } }); - - return Object.assign(entity, payload); -} - -updateNotification.schema = { - userId: Joi.number().required(), - notificationId: Joi.number().required(), - payload: Joi.object().keys({ - read: Joi.boolean(), - seen: Joi.boolean(), - }), -}; - -/** - * Mark notification(s) as read. - * @param {Number} id the notification id or '-' separated ids - * @param {Number} userId the user id - */ -function* markAsRead(id, userId) { - const ids = _.map(id.split('-'), (str) => { - const idInt = Number(str); - if (!_.isInteger(idInt)) { - throw new errors.BadRequestError(`Notification id should be integer: ${str}`); - } - return idInt; - }); - const entities = yield models.Notification.findAll({ where: { id: { $in: ids }, read: false } }); - if (!entities || entities.length === 0) { - throw new errors.NotFoundError(`Cannot find un-read Notification where id = ${id}`); - } - _.each(entities, (entity) => { - if (Number(entity.userId) !== userId) { - throw new errors.ForbiddenError(`Cannot access Notification where id = ${entity.id}`); - } - }); - yield models.Notification.update({ read: true }, { where: { id: { $in: ids }, read: false } }); -} - -markAsRead.schema = { - id: Joi.string().required(), - userId: Joi.number().required(), -}; - -/** - * Mark all notifications as read. - * @param {Number} userId the user id - */ -function* markAllRead(userId) { - yield models.Notification.update({ read: true }, { where: { userId, read: false } }); -} - -markAllRead.schema = { - userId: Joi.number().required(), -}; - -/** - * Mark notification(s) as seen. - * @param {Number} id the notification id or '-' separated ids - * @param {Number} userId the user id - */ -function* markAsSeen(id, userId) { - const ids = _.map(id.split('-'), (str) => { - const idInt = Number(str); - if (!_.isInteger(idInt)) { - throw new errors.BadRequestError(`Notification id should be integer: ${str}`); - } - return idInt; - }); - const entities = yield models.Notification.findAll({ where: { id: { $in: ids }, seen: { $not: true } } }); - if (!entities || entities.length === 0) { - throw new errors.NotFoundError(`Cannot find un-seen Notification where id = ${id}`); - } - _.each(entities, (entity) => { - if (Number(entity.userId) !== userId) { - throw new errors.ForbiddenError(`Cannot access Notification where id = ${entity.id}`); - } - }); - yield models.Notification.update({ seen: true }, { where: { id: { $in: ids }, seen: { $not: true } } }); -} - -markAsSeen.schema = { - id: Joi.string().required(), - userId: Joi.number().required(), -}; - -// Exports -module.exports = { - listNotifications, - markAsRead, - markAllRead, - markAsSeen, - getSettings, - updateSettings, - updateNotification, -}; +const models = require('../models'); + +const DEFAULT_LIMIT = 10; + +/** + * Get notification settings. + * @param {Number} userId the user id + * @returns {Object} the notification settings + */ +function* getSettings(userId) { + const notificationSettings = yield models.NotificationSetting.findAll({ where: { userId } }); + const serviceSettings = yield models.ServiceSettings.findAll({ where: { userId } }); + + // format settings per notification type + const notifications = {}; + _.each(notificationSettings, (setting) => { + if (!notifications[setting.topic]) { + notifications[setting.topic] = {}; + } + if (!notifications[setting.topic][setting.serviceId]) { + notifications[setting.topic][setting.serviceId] = {}; + } + notifications[setting.topic][setting.serviceId][setting.name] = setting.value; + }); + + // format settings per service + const services = {}; + _.each(serviceSettings, (setting) => { + if (!services[setting.serviceId]) { + services[setting.serviceId] = {}; + } + services[setting.serviceId][setting.name] = setting.value; + }); + return { + notifications, + services, + }; +} + +getSettings.schema = { + userId: Joi.number().required(), +}; + +/** + * Save notification setting entry. If the entry is not found, it will be created; otherwise it will be updated. + * @param {Object} entry the notification setting entry + * @param {Number} userId the user id + */ +function* saveNotificationSetting(entry, userId) { + const setting = yield models.NotificationSetting.findOne({ where: { + userId, topic: entry.topic, serviceId: entry.serviceId, name: entry.name } }); + if (setting) { + setting.value = entry.value; + yield setting.save(); + } else { + yield models.NotificationSetting.create({ + userId, + topic: entry.topic, + serviceId: entry.serviceId, + name: entry.name, + value: entry.value, + }); + } +} + +/** + * Save service setting entry. If the entry is not found, it will be created; otherwise it will be updated. + * @param {Object} entry the service setting entry + * @param {Number} userId the user id + */ +function* saveServiceSetting(entry, userId) { + const setting = yield models.ServiceSettings.findOne({ where: { + userId, serviceId: entry.serviceId, name: entry.name } }); + if (setting) { + setting.value = entry.value; + yield setting.save(); + } else { + yield models.ServiceSettings.create({ + userId, + serviceId: entry.serviceId, + name: entry.name, + value: entry.value, + }); + } +} + +/** + * Update notification settings. Un-specified settings are not changed. + * @param {Array} data the notification settings data + * @param {Number} userId the user id + */ +function* updateSettings(data, userId) { + // convert notification settings object to the list of entries + const notifications = []; + _.forOwn(data.notifications, (notification, topic) => { + _.forOwn(notification, (serviceSettings, serviceId) => { + _.forOwn(serviceSettings, (value, name) => { + notifications.push({ + topic, + serviceId, + name, + value, + }); + }); + }); + }); + + // validation + // there should be no duplicate (topic + serviceId + name) + const triples = {}; + notifications.forEach((entry) => { + const key = `${entry.topic} | ${entry.serviceId} | ${entry.name}`; + if (triples[key]) { + throw new errors.BadRequestError(`There are duplicate data for topic: ${ + entry.topic}, serviceId: ${entry.serviceId}, name: ${entry.name}`); + } + triples[key] = entry; + }); + + // save each entry in parallel + yield _.map(notifications, (entry) => saveNotificationSetting(entry, userId)); + + // convert services settings object the the list of entries + const services = []; + _.forOwn(data.services, (service, serviceId) => { + _.forOwn(service, (value, name) => { + services.push({ + serviceId, + name, + value, + }); + }); + }); + + // validation + // there should be no duplicate (serviceId + name) + const paris = {}; + services.forEach((entry) => { + const key = `${entry.serviceId} | ${entry.name}`; + if (paris[key]) { + throw new errors.BadRequestError('There are duplicate data for' + + ` serviceId: ${entry.serviceId}, name: ${entry.name}`); + } + paris[key] = entry; + }); + + yield _.map(services, (entry) => saveServiceSetting(entry, userId)); +} + +updateSettings.schema = { + data: Joi.object().keys({ + notifications: Joi.object(), + services: Joi.object(), + }).required(), + userId: Joi.number().required(), +}; + +/** + * List notifications. + * + * This method returns only notifications for 'web' + * Also this method filters notifications by the user and filters out notifications, + * which user disabled in his settings. + * + * @param {Object} query the query parameters + * @param {Number} userId the user id + * @returns {Object} the search result + */ +function* listNotifications(query, userId) { + const settings = yield getSettings(userId); + const notificationSettings = settings.notifications; + + const filter = { where: { + userId, + }, offset: query.offset, limit: query.limit, order: [['createdAt', 'DESC']] }; + if (_.keys(notificationSettings).length > 0) { + // only filter out notifications types which were explicitly set to 'no' - so we return notification by default + const notifications = _.keys(notificationSettings).filter((notificationType) => + !notificationSettings[notificationType] && + !notificationSettings[notificationType].web && + notificationSettings[notificationType].web.enabled === 'no' + ); + filter.where.type = { $notIn: notifications }; + } + if (query.type) { + filter.where.type = query.type; + } + if (query.read) { + filter.where.read = (query.read === 'true'); + } + const docs = yield models.Notification.findAndCountAll(filter); + const items = _.map(docs.rows, r => { + const item = r.toJSON(); + // id and userId are BIGINT in database, sequelize maps them to string values, + // convert them back to Number values + item.id = Number(item.id); + item.userId = Number(item.userId); + return item; + }); + return { + items, + offset: query.offset, + limit: query.limit, + totalCount: docs.count, + }; +} + +listNotifications.schema = { + query: Joi.object().keys({ + offset: Joi.number().integer().min(0).default(0), + limit: Joi.number().integer().min(1).default(DEFAULT_LIMIT), + type: Joi.string(), + // when it is true, return only read notifications + // when it is false, return only un-read notifications + // when it is no provided, no read flag filtering + read: Joi.string().valid('true', 'false'), + }).required(), + userId: Joi.number().required(), +}; + +/** + * Mark notification(s) as read. + * @param {Number} id the notification id or '-' separated ids + * @param {Number} userId the user id + */ +function* markAsRead(id, userId) { + const ids = _.map(id.split('-'), (str) => { + const idInt = Number(str); + if (!_.isInteger(idInt)) { + throw new errors.BadRequestError(`Notification id should be integer: ${str}`); + } + return idInt; + }); + const entities = yield models.Notification.findAll({ where: { id: { $in: ids }, read: false } }); + if (!entities || entities.length === 0) { + throw new errors.NotFoundError(`Cannot find un-read Notification where id = ${id}`); + } + _.each(entities, (entity) => { + if (Number(entity.userId) !== userId) { + throw new errors.ForbiddenError(`Cannot access Notification where id = ${entity.id}`); + } + }); + yield models.Notification.update({ read: true }, { where: { id: { $in: ids }, read: false } }); +} + +markAsRead.schema = { + id: Joi.string().required(), + userId: Joi.number().required(), +}; + +/** + * Mark all notifications as read. + * @param {Number} userId the user id + */ +function* markAllRead(userId) { + yield models.Notification.update({ read: true }, { where: { userId, read: false } }); +} + +markAllRead.schema = { + userId: Joi.number().required(), +}; + +/** + * Mark notification(s) as seen. + * @param {Number} id the notification id or '-' separated ids + * @param {Number} userId the user id + */ +function* markAsSeen(id, userId) { + const ids = _.map(id.split('-'), (str) => { + const idInt = Number(str); + if (!_.isInteger(idInt)) { + throw new errors.BadRequestError(`Notification id should be integer: ${str}`); + } + return idInt; + }); + const entities = yield models.Notification.findAll({ where: { id: { $in: ids }, seen: { $not: true } } }); + if (!entities || entities.length === 0) { + throw new errors.NotFoundError(`Cannot find un-seen Notification where id = ${id}`); + } + _.each(entities, (entity) => { + if (Number(entity.userId) !== userId) { + throw new errors.ForbiddenError(`Cannot access Notification where id = ${entity.id}`); + } + }); + yield models.Notification.update({ seen: true }, { where: { id: { $in: ids }, seen: { $not: true } } }); +} + +markAsSeen.schema = { + id: Joi.string().required(), + userId: Joi.number().required(), +}; + +// Exports +module.exports = { + listNotifications, + markAsRead, + markAllRead, + markAsSeen, + getSettings, + updateSettings, +}; logger.buildService(module.exports);