diff --git a/README.md b/README.md
index cd7d188..81416f3 100644
--- a/README.md
+++ b/README.md
@@ -7,24 +7,14 @@ The `@cap-js/notifications` package is a [CDS plugin](https://cap.cloud.sap/docs
### Table of Contents
- [Setup](#setup)
-- [Usage](#usage)
- - [Update Notification Configuration](#update-notification-configuration)
- - [Notification Types Path](#notification-types-path)
- - [Notification Type Prefix](#notification-type-prefix)
- - [Add Notification Types](#add-notification-types)
- - [Add code to send notifications](#add-code-to-send-notifications)
- - [Simple Notification with title](#simple-notification-with-title)
- - [Simple Notification with title and description](#simple-notification-with-title-and-description)
- - [Custom Notifications](#custom-notifications)
- - [With standard parameters](#with-standard-parameters)
- - [Passing the whole notification object](#passing-the-whole-notification-object)
- - [Sample Application with notifications](#sample-application-with-notifications)
- - [In Local Environment](#in-local-environment)
- - [In Production Environment](#in-production-environment)
- - [Notification Destination](#notification-destination)
- - [Integrate with SAP Build Work Zone](#integrate-with-sap-build-work-zone)
+- [Send Notifications](#send-notifications)
+- [Use Notification Types](#use-notification-types)
+- [API Reference](#api-reference)
+- [Test-drive Locally](#test-drive-locally)
+- [Run in Production](#run-in-production)
+- [Advanced Usage](#advanced-usage)
- [Contributing](#contributing)
- - [Code of Conduct](#code-of-conduct)
+- [Code of Conduct](#code-of-conduct)
- [Licensing](#licensing)
## Setup
@@ -32,144 +22,147 @@ The `@cap-js/notifications` package is a [CDS plugin](https://cap.cloud.sap/docs
To enable notifications, simply add this self-configuring plugin package to your project:
```sh
- cds add notifications
+ npm add @cap-js/notifications
```
-
-
-## Usage
-
In this guide, we use the [Incidents Management reference sample app](https://github.com/cap-js/incidents-app) as the base, to publish notifications.
-### Update Notification Configuration
+## Send Notifications
-`cds add notifications` will add default configurations for notifications in the `package.json` file.
+With that you can use the NotificationService as any other CAP Service like so in you event handlers:
-
+```js
+const alert = await cds.connect.to('notifications');
+```
-#### Notification Types Path
+You can use the following signature to send the simple notification with title and description
-When you run `cds add notifications`, it will add `notificationstype.json` file with template for a notification type in the project root folder. You can add the notification types in the `notificationtype.json` file for sending the custom notification types.
+```js
+alert.notify({
+ recipients: [ ...supporters() ],
+ priority: "HIGH",
+ title: "New high priority incident is assigned to you!",
+ description: "Incident titled 'Engine overheating' created by 'customer X' with priority high is assigned to you!"
+});
+```
-#### Notification Type Prefix
+* **priority** - Priority of the notification, this argument is optional, it defaults to NEUTRAL
+* **description** - Subtitle for the notification, this argument is optional
-To make notification types unique to the application, prefix is added to the type key. By default, `application name` is added as the prefix. You can update the `prefix` if required.
+## Use Notification Types
-### Add Notification Types
+### 1. Add notification types
-If you want to send custom notifications in your application, you can add the notification types in the `notificationtype.json` file.
+If you want to send custom notifications in your application, you can add the notification types in the `srv/notification-types.json` file.
-Sample: If you want to send the notification when the new incident is reported, you can modify the `notificationtypes.json` as below:
+Sample: If you want to send the notification when the incident is resolved, you can modify the `srv/notification-types.json` as below:
-```jsonc
-[
- {
- "NotificationTypeKey": "IncidentReported",
- "NotificationTypeVersion": "1",
- "Templates": [
- {
- "Language": "en",
- "TemplatePublic": "Incident Reported",
- "TemplateSensitive": "Incident '{{name}}' Reported",
- "TemplateGrouped": "New Incidents",
- "TemplateLanguage": "mustache",
- "Subtitle": "Incident '{{name}}' reported by '{{customer}}'."
- }
- ]
- }
-]
+```json
+ [
+ {
+ "NotificationTypeKey": "IncidentResolved",
+ "NotificationTypeVersion": "1",
+ "Templates": [
+ {
+ "Language": "en",
+ "TemplatePublic": "Incident Resolved",
+ "TemplateSensitive": "Incident '{{title}}' Resolved",
+ "TemplateGrouped": "Incident Status Update",
+ "TemplateLanguage": "mustache",
+ "Subtitle": "Incident from '{{customer}}' resolved by {{user}}."
+ }
+ ]
+ }
+ ]
```
-### Add code to send notifications
+### 2. Use pre-defined types in your code like that:
-In the handler files, connect to the notifications plugin by:
-
```js
-const alert = await cds.connect.to('notifications');
+ await alert.notify ('IncidentResolved', {
+ recipients: [ customer.id ],
+ data: {
+ customer: customer.info,
+ title: incident.title,
+ user: cds.context.user.id,
+ }
+ })
```
-#### Simple Notification with title
-You can use the following signature to send the simple notification with title
-```js
-alert.notify({
- recipients: ["admin1@test.com","admin2@test.com"],
- priority: "HIGH",
- title: "New incident is reported!"
-});
-```
-#### Simple Notification with title and description
-You can use the following signature to send the simple notification with title and description
-```js
-alert.notify({
- recipients: ["supportuser1@test.com"],
- priority: "HIGH",
- title: "New high priority incident is assigned to you!",
- description: "Incident titled 'Engine overheating' created by 'customer X' with priority high is assigned to you!"
-});
-```
+## API Reference
+
+* **recipients** - List of the recipients, this argument is mandatory
+* **type** - Notification type key, this argument is mandatory
+* **priority** - Priority of the notification, this argument is optional, it defaults to NEUTRAL
+* **data** - A key-value pair that is used to fill a placeholder of the notification type template, this argument is optional
+
+## Test-drive Locally
+In local environment, when you publish notification, it is mocked to publish the nofication to the console.
+
+
+
+## Run in Production
+
+#### Notification Destination
+
+As a pre-requisite to publish the notification, you need to have a [destination](https://help.sap.com/docs/build-work-zone-standard-edition/sap-build-work-zone-standard-edition/enabling-notifications-for-custom-apps-on-sap-btp-cloud-foundry#configure-the-destination-to-the-notifications-service) configured to publish the notification. In the `package.json` by default destination name `SAP_Notifications` is added, you can modify the destination name that you are configuring.
+
+#### Integrate with SAP Build Work Zone
+
+Once application is deployed and [integrated with SAP Build Work Zone](https://github.com/cap-js/calesi/tree/main/samples/notifications), you can see the notification under fiori notifications icon!
+
+
+
+
+
+## Advanced Usage
+
+### Custom Notification Types Path
+
+Notifications plugin configures `srv/notification-types.json` as default notification types file. If you are using different file, you can update the file path in `cds.env.requires.notifications.types`
+
+### Custom Notification Type Prefix
+
+To make notification types unique to the application, prefix is added to the type key. By default, `application name` is added as the prefix. You can update the `cds.env.requires.notifications.prefix` if required.
+
+### Low-level Notifications API
-#### Custom Notifications
You can use these two signature to send the custom notification with pre-defined notification types.
-##### With standard parameters
+#### With pre-defined parameters
+
+By using this approach you can send notifications with the predefined parameters - recipients, data, priority, type and other parameters listed in the [API documentation](https://help.sap.com/docs/build-work-zone-standard-edition/sap-build-work-zone-standard-edition/developing-cloud-foundry-applications-with-notifications)
-By using this approach you can post a notification by providing different parts of the notification object grouped in related units
```js
alert.notify({
- recipients: ["admin1@test.com","admin2@test.com"],
- type: "IncidentCreated"
+ recipients: [...supporters()],
+ type: "IncidentResolved",
priority: 'NEUTRAL',
- properties: [
+ data: {
+ customer: customer.info,
+ title: incident.title,
+ user: cds.context.user.id,
+ },
+ OriginId: "Example Origin Id",
+ NotificationTypeVersion: "1",
+ ProviderId: "/SAMPLEPROVIDER",
+ ActorId: "BACKENDACTORID",
+ ActorDisplayText: "ActorName",
+ ActorImageURL: "https://some-url",
+ NotificationTypeTimestamp: "2022-03-15T09:58:42.807Z",
+ TargetParameters: [
{
- Key: 'name',
- IsSensitive: false,
- Language: 'en',
- Value: 'Engine overheating',
- Type: 'String'
- },
- {
- Key: 'customer',
- IsSensitive: false,
- Language: 'en',
- Value: 'John',
- Type: 'String'
+ "Key": "string",
+ "Value": "string"
}
- ],
- navigation: {
- NavigationTargetAction: "displayInbox",
- NavigationTargetObject: "WorkflowTask",
- }
- payload: {
- Id: "01234567-89ab-cdef-0123-456789abcdef",
- OriginId: "Example Origin Id",
- NotificationTypeId: "01234567-89ab-cdef-0123-456789abcdef",
- NotificationTypeVersion: "1",
- ProviderId: "/SAMPLEPROVIDER",
- ActorId: "BACKENDACTORID",
- ActorDisplayText: "ActorName",
- ActorImageURL: "https://some-url",
- NotificationTypeTimestamp: "2022-03-15T09:58:42.807Z",
- TargetParameters: [
- {
- "Key": "string",
- "Value": "string"
- }
]
- }
-});
+ });
```
-Possible parameters:
-* **recipients** - List of the recipients, this argument is mandatory
-* **type** - Notification type key, this argument is mandatory
-* **priority** - Priority of the notification, this argument is optional, it defaults to NEUTRAL
-* **properties** - A key-value pair that is used to fill a placeholder of the notification type template, this argument is optional
-* **navigation** - All navigation related parameters, this argument is optional
-* **payload** - The rest parameters that can be passed, this argument is optional
+#### Passing the whole notification object
-##### Passing the whole notification object
+By using this approach you need to pass the whole notification object as described in the [API documentation](https://help.sap.com/docs/build-work-zone-standard-edition/sap-build-work-zone-standard-edition/developing-cloud-foundry-applications-with-notifications)
-By using this approach you need to pass the whole notification object as described in the API documentation
```js
alert.notify({
NotificationTypeKey: 'IncidentCreated',
@@ -187,46 +180,22 @@ alert.notify({
Key: 'customer',
IsSensitive: false,
Language: 'en',
- Value: 'John',
+ Value: 'Dave',
Type: 'String'
}
],
- Recipients: ["admin1@test.com","admin2@test.com"]
+ Recipients: [{ RecipientId: "supportuser1@mycompany.com" },{ RecipientId: "supportuser2@mycompany.com" }]
});
```
-### Sample Application with notifications
-
-#### In Local Environment
-In local environment, when you publish notification, it is mocked to publish the nofication to the console.
-
-
-
-#### In Production Environment
-
-##### Notification Destination
-
-As a pre-requisite to publish the notification, you need to have a [destination](https://help.sap.com/docs/build-work-zone-standard-edition/sap-build-work-zone-standard-edition/enabling-notifications-for-custom-apps-on-sap-btp-cloud-foundry#configure-the-destination-to-the-notifications-service) configured to publish the notification. In the `package.json` by default destination name `SAP_Notification` is added, you can modify the destination name that you are configuring.
-
-##### Integrate with SAP Build Work Zone
-
-Once application is deployed and [integrated with SAP Build Work Zone](https://github.com/cap-js/calesi/tree/main/samples/notifications), you can see the notification under fiori notifications icon!
-
-
-
## Contributing
This project is open to feature requests/suggestions, bug reports etc. via [GitHub issues](https://github.com/cap-js/change-tracking/issues). Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our [Contribution Guidelines](CONTRIBUTING.md).
-
-### Code of Conduct
+## Code of Conduct
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. By participating in this project, you agree to abide by its [Code of Conduct](CODE_OF_CONDUCT.md) at all times.
-
## Licensing
Copyright 2023 SAP SE or an SAP affiliate company and contributors. Please see our [LICENSE](LICENSE) for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available [via the REUSE tool](https://api.reuse.software/info/github.com/cap-js/change-tracking).
-
-
-
diff --git a/_assets/cdsAddNotifications.gif b/_assets/cdsAddNotifications.gif
deleted file mode 100644
index 9317c59..0000000
Binary files a/_assets/cdsAddNotifications.gif and /dev/null differ
diff --git a/_assets/incidentsNotificationDemo.gif b/_assets/incidentsNotificationDemo.gif
index dc06584..63a3755 100644
Binary files a/_assets/incidentsNotificationDemo.gif and b/_assets/incidentsNotificationDemo.gif differ
diff --git a/_assets/notifyToConsole.png b/_assets/notifyToConsole.png
index d00be0f..a00b0d7 100644
Binary files a/_assets/notifyToConsole.png and b/_assets/notifyToConsole.png differ
diff --git a/lib/notificationTypes.js b/lib/notificationTypes.js
index e7df758..aeac40a 100644
--- a/lib/notificationTypes.js
+++ b/lib/notificationTypes.js
@@ -1,6 +1,6 @@
const { executeHttpRequest } = require("@sap-cloud-sdk/http-client");
const { buildHeadersForDestination } = require("@sap-cloud-sdk/connectivity");
-const { getNotificationDestination, doesKeyExist, getPrefix, getNotificationTypesKeyWithPrefix } = require("./utils");
+const { getNotificationDestination, getPrefix, getNotificationTypesKeyWithPrefix } = require("./utils");
const _ = require("lodash");
const NOTIFICATION_TYPES_API_ENDPOINT = "v2/NotificationType.svc";
const cds = require("@sap/cds");
@@ -49,7 +49,7 @@ function createNotificationTypesMap(notificationTypesJSON, isLocal = false) {
// update the notification type key with prefix
notificationType.NotificationTypeKey = notificationTypeKeyWithPrefix;
- if (!doesKeyExist(types, notificationTypeKeyWithPrefix)) {
+ if (!(notificationTypeKeyWithPrefix in types)) {
types[notificationTypeKeyWithPrefix] = {};
}
diff --git a/lib/notifications.js b/lib/notifications.js
deleted file mode 100644
index 7c400aa..0000000
--- a/lib/notifications.js
+++ /dev/null
@@ -1,37 +0,0 @@
-const { buildHeadersForDestination } = require("@sap-cloud-sdk/connectivity");
-const { executeHttpRequest } = require("@sap-cloud-sdk/http-client");
-const { getNotificationDestination } = require("./utils");
-const LOG = cds.log('notifications');
-const NOTIFICATIONS_API_ENDPOINT = "v2/Notification.svc";
-
-async function postNotification(notificationData) {
- const notificationDestination = await getNotificationDestination();
- const csrfHeaders = await buildHeadersForDestination(notificationDestination, {
- url: NOTIFICATIONS_API_ENDPOINT,
- });
-
- try {
- LOG._info && LOG.info(
- `Sending notification of key: ${notificationData.NotificationTypeKey} and version: ${notificationData.NotificationTypeVersion}`
- );
- await executeHttpRequest(notificationDestination, {
- url: `${NOTIFICATIONS_API_ENDPOINT}/Notifications`,
- method: "post",
- data: notificationData,
- headers: csrfHeaders,
- });
- } catch (err) {
- const message = err.response.data?.error?.message?.value ?? err.response.message;
- const error = new cds.error(message);
-
- if (String(err.response?.status).match(/^4\d\d$/) && err.response?.status !== 429) {
- error.unrecoverable = true;
- }
-
- throw error;
- }
-}
-
-module.exports = {
- postNotification
-};
\ No newline at end of file
diff --git a/lib/utils.js b/lib/utils.js
index ae8b8b7..e4c161f 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -95,9 +95,6 @@ function validateCustomNotifyParameters(type, recipients, properties, navigation
return true;
}
-function doesKeyExist(obj, key) {
- return typeof(key) === 'string' && typeof(obj) === 'object' && key in obj;
-}
function readFile(filePath) {
if (!existsSync(filePath)) {
@@ -118,8 +115,16 @@ async function getNotificationDestination() {
return notificationDestination;
}
+let prefix // be filled in below...
function getPrefix() {
- return cds.env.requires.notifications?.prefix ?? basename(cds.root);
+ if (!prefix) {
+ prefix = cds.env.requires.notifications?.prefix
+ if (prefix === "$app-name") try {
+ prefix = require(cds.root + '/package.json').name
+ } catch { prefix = null }
+ if (!prefix) prefix = basename(cds.root)
+ }
+ return prefix
}
function getNotificationTypesKeyWithPrefix(notificationTypeKey) {
@@ -159,71 +164,72 @@ function buildDefaultNotification(
};
}
-function buildCustomNotification(notificationData) {
- return {
- Id: notificationData["payload"] ? notificationData["payload"]["Id"] : undefined,
- OriginId: notificationData["payload"] ? notificationData["payload"]["OriginId"] : undefined,
- NotificationTypeId: notificationData["payload"] ? notificationData["payload"]["NotificationTypeId"] : undefined,
- NotificationTypeKey: getNotificationTypesKeyWithPrefix(notificationData["type"]),
- NotificationTypeVersion: notificationData["payload"] && notificationData["payload"]["NotificationTypeVersion"] ? notificationData["payload"]["NotificationTypeVersion"] : "1",
- NavigationTargetAction: notificationData["navigation"] ? notificationData["navigation"]["NavigationTargetAction"] : undefined,
- NavigationTargetObject: notificationData["navigation"] ? notificationData["navigation"]["NavigationTargetObject"] : undefined,
- Priority: notificationData["priority"] ? notificationData["priority"] : "NEUTRAL",
- ProviderId: notificationData["payload"] ? notificationData["payload"]["ProviderId"] : undefined,
- ActorId: notificationData["payload"] ? notificationData["payload"]["ActorId"] : undefined,
- ActorDisplayText: notificationData["payload"] ? notificationData["payload"]["ActorDisplayText"] : undefined,
- ActorImageURL: notificationData["payload"] ? notificationData["payload"]["ActorImageURL"] : undefined,
- NotificationTypeTimestamp: notificationData["payload"] ? notificationData["payload"]["NotificationTypeTimestamp"] : undefined,
- Recipients: notificationData["recipients"].map((recipient) => ({ RecipientId: recipient })),
- Properties: notificationData["properties"] ? notificationData["properties"] : undefined,
- TargetParameters: notificationData["payload"] ? notificationData["payload"]["TargetParameters"] : undefined
- };
+function buildCustomNotification(_) {
+ let notification = {
+
+ // Properties with simple API variants
+ NotificationTypeKey: getNotificationTypesKeyWithPrefix(_.NotificationTypeKey || _.type),
+ Recipients: _.Recipients || _.recipients?.map(id => ({ RecipientId: id })),
+ Priority: _.Priority || _.priority || "NEUTRAL",
+ Properties: _.Properties || Object.entries(_.data).map(([k,v]) => ({
+ Key:k, Value:v, Language: "en", Type: typeof v, // IsSensitive: false
+ })),
+
+ // Low-level API properties
+ OriginId: _.OriginId,
+ NotificationTypeId: _.NotificationTypeId,
+ NotificationTypeVersion: _.NotificationTypeVersion || "1",
+ NavigationTargetAction: _.NavigationTargetAction,
+ NavigationTargetObject: _.NavigationTargetObject,
+ ProviderId: _.ProviderId,
+ ActorId: _.ActorId,
+ ActorDisplayText: _.ActorDisplayText,
+ ActorImageURL: _.ActorImageURL,
+ TargetParameters: _.TargetParameters,
+ NotificationTypeTimestamp: _.NotificationTypeTimestamp,
+ }
+ return notification
}
function buildNotification(notificationData) {
let notification;
- if(notificationData === undefined || notificationData === null) {
- LOG._warn && LOG.warn(messages.NO_OBJECT_FOR_NOTIFY);
- return;
- }
-
if (Object.keys(notificationData).length === 0) {
LOG._warn && LOG.warn(messages.EMPTY_OBJECT_FOR_NOTIFY);
return;
}
- if (notificationData["type"]) {
+ if (notificationData.type) {
if (!validateCustomNotifyParameters(
- notificationData["type"],
- notificationData["recipients"],
- notificationData["properties"],
- notificationData["navigation"],
- notificationData["priority"],
- notificationData["payload"])
+ notificationData.type,
+ notificationData.recipients,
+ notificationData.properties,
+ notificationData.navigation,
+ notificationData.priority,
+ notificationData.payload)
) {
return;
}
notification = buildCustomNotification(notificationData);
- } else if (notificationData["NotificationTypeKey"]) {
- notificationData["NotificationTypeKey"] = getNotificationTypesKeyWithPrefix(notificationData["NotificationTypeKey"]);
+ } else if (notificationData.NotificationTypeKey) {
+ notificationData.NotificationTypeKey = getNotificationTypesKeyWithPrefix(notificationData.NotificationTypeKey);
notification = notificationData;
} else {
if (!validateDefaultNotifyParameters(
- notificationData["recipients"],
- notificationData["priority"],
- notificationData["title"],
- notificationData["description"])
+ notificationData.recipients,
+ notificationData.priority,
+ notificationData.title,
+ notificationData.description)
) {
return;
}
notification = buildDefaultNotification(
- notificationData["recipients"],
- notificationData["priority"],
- notificationData["title"],
- notificationData["description"]
+ notificationData.recipients,
+ notificationData.priority,
+ notificationData.title,
+ notificationData.description
);
}
@@ -234,7 +240,6 @@ module.exports = {
messages,
validateNotificationTypes,
readFile,
- doesKeyExist,
getNotificationDestination,
getPrefix,
getNotificationTypesKeyWithPrefix,
diff --git a/package.json b/package.json
index 6de475c..f22a471 100644
--- a/package.json
+++ b/package.json
@@ -32,22 +32,25 @@
},
"cds": {
"requires": {
- "kinds": {
- "notifications": {
- "[development]": {
- "kind": "notify-to-console"
- },
- "[production]": {
- "kind": "notify-to-rest"
- }
+ "destinations": true,
+ "notifications": {
+ "[development]": {
+ "kind": "notify-to-console"
+ },
+ "[production]": {
+ "destination": "SAP_Notifications",
+ "kind": "notify-to-rest"
},
+ "prefix": "$app-name",
+ "types": "srv/notification-types.json",
+ "outbox": true
+ },
+ "kinds": {
"notify-to-console": {
- "impl": "@cap-js/notifications/srv/notifyToConsole",
- "outbox": false
+ "impl": "@cap-js/notifications/srv/notifyToConsole"
},
"notify-to-rest": {
- "impl": "@cap-js/notifications/srv/notifyToRest",
- "outbox": true
+ "impl": "@cap-js/notifications/srv/notifyToRest"
}
}
}
diff --git a/srv/notifyToConsole.js b/srv/notifyToConsole.js
index ad04dbf..f69320e 100644
--- a/srv/notifyToConsole.js
+++ b/srv/notifyToConsole.js
@@ -1,34 +1,37 @@
const NotificationService = require('./service');
const cds = require("@sap/cds");
const LOG = cds.log('notifications');
-const { buildNotification, doesKeyExist } = require("./../lib/utils");
module.exports = class NotifyToConsole extends NotificationService {
async init() {
- // call NotificationService's init
- await super.init();
- }
- notify() {
+ this.on("*", req => {
+ LOG._debug && LOG.debug('Handling notification event:', req.event)
+ const notification = req.data; if (!notification) return
+ console.log (
+ '\n---------------------------------------------------------------\n' +
+ 'Notification:', req.event,
+ notification,
+ '\n---------------------------------------------------------------\n',
+ )
- const notification = buildNotification(arguments[0]);
+ const { NotificationTypeKey, NotificationTypeVersion } = notification
+ const types = cds.notifications.local.types // REVISIT: what is this?
- if (notification) {
- LOG._info && LOG.info(`SAP Alert Notification Service notification: ${JSON.stringify(notification, null, 2)}`);
- const existingTypes = cds.notifications.local.types;
-
- if (!doesKeyExist(existingTypes, notification["NotificationTypeKey"])) {
+ if (!(NotificationTypeKey in types)) {
LOG._warn && LOG.warn(
- `Notification Type ${notification["NotificationTypeKey"]} is not in the notification types file`
+ `Notification Type ${NotificationTypeKey} is not in the notification types file`
);
return;
}
- if (!doesKeyExist(existingTypes[notification["NotificationTypeKey"]], notification["NotificationTypeVersion"])) {
+ if (!(NotificationTypeVersion in types[NotificationTypeKey])) {
LOG._warn && LOG.warn(
- `Notification Type Version ${notification["NotificationTypeVersion"]} for type ${notification["NotificationTypeKey"]} is not in the notification types file`
+ `Notification Type Version ${NotificationTypeVersion} for type ${NotificationTypeKey} is not in the notification types file`
);
}
- }
+ })
+
+ return super.init()
}
}
diff --git a/srv/notifyToRest.js b/srv/notifyToRest.js
index efd93d0..dcc5394 100644
--- a/srv/notifyToRest.js
+++ b/srv/notifyToRest.js
@@ -1,20 +1,43 @@
-const NotificationService = require("./service");
-const { buildNotification } = require("../lib/utils");
-const { postNotification } = require("../lib/notifications");
+const NotificationService = require("./service")
+
+const { buildHeadersForDestination } = require("@sap-cloud-sdk/connectivity");
+const { executeHttpRequest } = require("@sap-cloud-sdk/http-client");
+const { getNotificationDestination } = require("../lib/utils");
+const LOG = cds.log('notifications');
+const NOTIFICATIONS_API_ENDPOINT = "v2/Notification.svc";
-module.exports = class NotifyToRest extends NotificationService {
- async init() {
- // call NotificationService's init
- await super.init();
- this.on("postNotificationEvent", async (req) => await postNotification(req.data));
+module.exports = exports = class NotifyToRest extends NotificationService {
+ async init() {
+ this.on("*", req => this.postNotification(req.data))
+ return super.init()
}
- async notify(notificationData) {
- const notification = buildNotification(notificationData);
+ async postNotification(notificationData) {
+ const notificationDestination = await getNotificationDestination();
+ const csrfHeaders = await buildHeadersForDestination(notificationDestination, {
+ url: NOTIFICATIONS_API_ENDPOINT,
+ });
+
+ try {
+ LOG._info && LOG.info(
+ `Sending notification of key: ${notificationData.NotificationTypeKey} and version: ${notificationData.NotificationTypeVersion}`
+ );
+ await executeHttpRequest(notificationDestination, {
+ url: `${NOTIFICATIONS_API_ENDPOINT}/Notifications`,
+ method: "post",
+ data: notificationData,
+ headers: csrfHeaders,
+ });
+ } catch (err) {
+ const message = err.response.data?.error?.message?.value ?? err.response.message;
+ const error = new cds.error(message);
+
+ if (/^4\d\d$/.test(err.response?.status) && err.response?.status != 429) {
+ error.unrecoverable = true;
+ }
- if (notification) {
- await this.emit({ event: "postNotificationEvent", data: notification });
+ throw error;
}
}
-};
+}
diff --git a/srv/service.js b/srv/service.js
index cc2a833..41d34bf 100644
--- a/srv/service.js
+++ b/srv/service.js
@@ -1,14 +1,45 @@
-// REVISIT: cds.OutboxService or technique to avoid extending OutboxService
-const OutboxService = require('@sap/cds/libx/_runtime/messaging/Outbox');
+const { buildNotification, messages } = require("./../lib/utils")
+const cds = require('@sap/cds')
+const LOG = cds.log('notifications');
-module.exports = class NotificationService extends OutboxService {
- async init() {
+class NotificationService extends cds.Service {
- // call OutboxService's init
- await super.init();
+ /**
+ * Emits a notification. Method notify can be used alternatively.
+ * @param {string} [event] - The notification type.
+ * @param {object} message - The message object
+ */
+ emit (event, message) {
+ if (!event) {
+ LOG._warn && LOG.warn(messages.NO_OBJECT_FOR_NOTIFY);
+ return;
+ }
+ // Outbox calls us with a req object, e.g. { event, data, headers }
+ if (event.event) return super.emit (event)
+ // First argument is optional for convenience
+ if (!message) [ message, event ] = [ event ]
+ // CAP events translate to notification types and vice versa
+ if (event) message.type = event
+ else event = message.type || message.NotificationTypeKey || 'Default'
+ // Prepare and emit the notification
+ message = buildNotification(message)
+ return super.emit (event, message)
}
- notify() {
- // Abstract function
+ /**
+ * That's just a semantic alias for emit.
+ */
+ notify (type, message) {
+ return this.emit (type,message)
}
+
+}
+module.exports = NotificationService
+
+// Without Generic Outbox only alert.notify() can be used, not alert.emit()
+// Remove that when @sap/cds with Generic Outbox is released
+if (!cds.outboxed) {
+ class OutboxedNotificationService extends require('@sap/cds/libx/_runtime/messaging/Outbox') {}
+ OutboxedNotificationService.prototype.notify = NotificationService.prototype.emit
+ module.exports = OutboxedNotificationService
}
diff --git a/test/lib/content-deployment.test.js b/test/lib/content-deployment.test.js
index 559759e..fc000da 100644
--- a/test/lib/content-deployment.test.js
+++ b/test/lib/content-deployment.test.js
@@ -26,7 +26,6 @@ describe("contentDeployment", () => {
console.log(setGlobalLogLevel.mock.calls);
assert.expect(setGlobalLogLevel.mock.calls[0][0]).to.be.equal("error");
- assert.expect(readFile.mock.calls[0][0]).to.be.equal('');
assert.expect(validateNotificationTypes.mock.calls[0][0]).to.be.deep.equal([]);
assert.expect(processNotificationTypes.mock.calls[0][0]).to.be.deep.equal([]);
});
@@ -41,7 +40,6 @@ describe("contentDeployment", () => {
console.log(setGlobalLogLevel.mock.calls);
assert.expect(setGlobalLogLevel.mock.calls[0][0]).to.be.equal("error");
- assert.expect(readFile.mock.calls[0][0]).to.be.equal('');
assert.expect(validateNotificationTypes.mock.calls[0][0]).to.be.deep.equal([]);
assert.expect(processNotificationTypes.mock.calls[0]).to.be.deep.equal(undefined);
});
diff --git a/test/lib/notificationTypes.test.js b/test/lib/notificationTypes.test.js
index cde5111..40eccef 100644
--- a/test/lib/notificationTypes.test.js
+++ b/test/lib/notificationTypes.test.js
@@ -18,6 +18,7 @@ describe("Managing of Notification Types", () => {
httpClient.executeHttpRequest.mockReturnValue(emptyResponseBody);
connectivity.buildHeadersForDestination.mockReturnValue({});
utils.getNotificationTypesKeyWithPrefix.mockImplementation((str) => testPrefix + "/" + str);
+ // REVISIT: Never test internal APIs -> blocks us from refactoring
utils.getPrefix.mockReturnValue(testPrefix);
notificationTypes.processNotificationTypes([copy(notificationTypeWithAllProperties), copy(notificationTypeWithoutVersion)]).then(() => {
diff --git a/test/lib/notifications.test.js b/test/lib/notifications.test.js
index cb5a3f5..55d7c9f 100644
--- a/test/lib/notifications.test.js
+++ b/test/lib/notifications.test.js
@@ -1,6 +1,6 @@
const { getNotificationDestination } = require("./../../lib/utils");
const { buildHeadersForDestination } = require("@sap-cloud-sdk/connectivity");
-const { postNotification } = require("./../../lib/notifications");
+const NotifyToRest = require("./../../srv/notifyToRest");
const { executeHttpRequest } = require("@sap-cloud-sdk/http-client");
jest.mock("./../../lib/utils");
@@ -37,13 +37,14 @@ const expectedCustomNotification = {
describe("Test post notification", () => {
test("When passed whole notification object to postNotification", async () => {
+ const alert = new NotifyToRest
const infoSpy = jest.spyOn(global.console, 'info');
getNotificationDestination.mockReturnValue(undefined);
buildHeadersForDestination.mockReturnValue(undefined);
executeHttpRequest.mockReturnValue(expectedCustomNotification);
// call post notification
- await postNotification(expectedCustomNotification)
+ await alert.postNotification(expectedCustomNotification)
// check if console.info was called
expect(infoSpy).toHaveBeenCalled();
@@ -55,6 +56,7 @@ describe("Test post notification", () => {
})
test("When execute http request throws error with status code 500", async () => {
+ const alert = new NotifyToRest
const error = new Error();
error.response = {
message: "mocked error",
@@ -70,7 +72,7 @@ describe("Test post notification", () => {
// call post notification
try {
- await postNotification(expectedCustomNotification);
+ await alert.postNotification(expectedCustomNotification);
} catch (err) {
expect(err.unrecoverable).toBeFalsy();
}
@@ -85,6 +87,7 @@ describe("Test post notification", () => {
})
test("When execute http request throws error with status code 404", async () => {
+ const alert = new NotifyToRest
const error = new Error();
error.response = {
message: "mocked error",
@@ -100,7 +103,7 @@ describe("Test post notification", () => {
// call post notification
try {
- await postNotification(expectedCustomNotification);
+ await alert.postNotification(expectedCustomNotification);
} catch (err) {
expect(err.unrecoverable).toEqual(true);
}
@@ -115,6 +118,7 @@ describe("Test post notification", () => {
})
test("When execute http request throws error with status code 429", async () => {
+ const alert = new NotifyToRest
const error = new Error();
error.response = {
message: "mocked error",
@@ -130,7 +134,7 @@ describe("Test post notification", () => {
// call post notification
try {
- await postNotification(expectedCustomNotification);
+ await alert.postNotification(expectedCustomNotification);
} catch (err) {
expect(err.unrecoverable).toBeFalsy();
}
diff --git a/test/lib/utils.test.js b/test/lib/utils.test.js
index 6ac4261..883a218 100644
--- a/test/lib/utils.test.js
+++ b/test/lib/utils.test.js
@@ -1,4 +1,4 @@
-const { buildNotification, validateNotificationTypes, doesKeyExist, readFile, getNotificationDestination } = require("../../lib/utils");
+const { buildNotification, validateNotificationTypes, readFile, getNotificationDestination } = require("../../lib/utils");
const { existsSync, readFileSync } = require("fs");
const { getDestination } = require("@sap-cloud-sdk/connectivity");
@@ -125,7 +125,7 @@ describe("Test utils", () => {
buildNotification({
recipients: ["test.mail@mail.com"],
type: "TestNotificationType",
- properties: [
+ Properties: [
{
Key: "title",
IsSensitive: false,
@@ -165,7 +165,7 @@ describe("Test utils", () => {
buildNotification({
recipients: ["test.mail@mail.com"],
type: "TestNotificationType",
- properties: [
+ Properties: [
{
Key: "title",
IsSensitive: false,
@@ -174,10 +174,8 @@ describe("Test utils", () => {
Type: "String"
}
],
- navigation: {
- NavigationTargetAction: "TestTargetAction",
- NavigationTargetObject: "TestTargetObject"
- }
+ NavigationTargetAction: "TestTargetAction",
+ NavigationTargetObject: "TestTargetObject",
})
).toMatchObject(expectedNotification);
});
@@ -209,7 +207,7 @@ describe("Test utils", () => {
buildNotification({
recipients: ["test.mail@mail.com"],
type: "TestNotificationType",
- properties: [
+ Properties: [
{
Key: "title",
IsSensitive: false,
@@ -218,10 +216,8 @@ describe("Test utils", () => {
Type: "String"
}
],
- navigation: {
- NavigationTargetAction: "TestTargetAction",
- NavigationTargetObject: "TestTargetObject"
- },
+ NavigationTargetAction: "TestTargetAction",
+ NavigationTargetObject: "TestTargetObject",
priority: "HIGH"
})
).toMatchObject(expectedNotification);
@@ -229,7 +225,6 @@ describe("Test utils", () => {
test("When recipients, type, properties, navigation, priority, payload are passed to buildNotification", () => {
const expectedNotification = {
- Id: "01234567-89ab-cdef-0123-456789abcdef",
OriginId: "01234567-89ab-cdef-0123-456789abcdef",
NotificationTypeId: "01234567-89ab-cdef-0123-456789abcdef",
NotificationTypeKey: "notifications/TestNotificationType",
@@ -268,7 +263,7 @@ describe("Test utils", () => {
buildNotification({
recipients: ["test.mail@mail.com"],
type: "TestNotificationType",
- properties: [
+ Properties: [
{
Key: "title",
IsSensitive: false,
@@ -277,27 +272,22 @@ describe("Test utils", () => {
Type: "String"
}
],
- navigation: {
- NavigationTargetAction: "TestTargetAction",
- NavigationTargetObject: "TestTargetObject"
- },
+ NavigationTargetAction: "TestTargetAction",
+ NavigationTargetObject: "TestTargetObject",
priority: "HIGH",
- payload: {
- Id: "01234567-89ab-cdef-0123-456789abcdef",
- OriginId: "01234567-89ab-cdef-0123-456789abcdef",
- NotificationTypeId: "01234567-89ab-cdef-0123-456789abcdef",
- ProviderId: "SAMPLEPROVIDER",
- ActorId: "BACKENDACTORID",
- ActorDisplayText: "ActorName",
- ActorImageURL: "https://some-url",
- NotificationTypeTimestamp: "2022-03-15T09:58:42.807Z",
- TargetParameters: [
- {
- Key: "string",
- Value: "string"
- }
- ]
- }
+ OriginId: "01234567-89ab-cdef-0123-456789abcdef",
+ NotificationTypeId: "01234567-89ab-cdef-0123-456789abcdef",
+ ProviderId: "SAMPLEPROVIDER",
+ ActorId: "BACKENDACTORID",
+ ActorDisplayText: "ActorName",
+ ActorImageURL: "https://some-url",
+ NotificationTypeTimestamp: "2022-03-15T09:58:42.807Z",
+ TargetParameters: [
+ {
+ Key: "string",
+ Value: "string"
+ }
+ ]
})
).toMatchObject(expectedNotification);
});
@@ -341,7 +331,7 @@ describe("Test utils", () => {
buildNotification({
recipients: ["test.mail@mail.com"],
type: "TestNotificationType",
- properties: [
+ Properties: [
{
Key: "title",
IsSensitive: false,
@@ -350,25 +340,21 @@ describe("Test utils", () => {
Type: "String"
}
],
- navigation: {
- NavigationTargetAction: "TestTargetAction",
- NavigationTargetObject: "TestTargetObject"
- },
+ NavigationTargetAction: "TestTargetAction",
+ NavigationTargetObject: "TestTargetObject",
priority: "HIGH",
- payload: {
- NotificationTypeId: "01234567-89ab-cdef-0123-456789abcdef",
- ProviderId: "SAMPLEPROVIDER",
- ActorId: "BACKENDACTORID",
- ActorDisplayText: "ActorName",
- ActorImageURL: "https://some-url",
- NotificationTypeTimestamp: "2022-03-15T09:58:42.807Z",
- TargetParameters: [
- {
- Key: "string",
- Value: "string"
- }
- ]
- }
+ NotificationTypeId: "01234567-89ab-cdef-0123-456789abcdef",
+ ProviderId: "SAMPLEPROVIDER",
+ ActorId: "BACKENDACTORID",
+ ActorDisplayText: "ActorName",
+ ActorImageURL: "https://some-url",
+ NotificationTypeTimestamp: "2022-03-15T09:58:42.807Z",
+ TargetParameters: [
+ {
+ Key: "string",
+ Value: "string"
+ }
+ ]
})
).toMatchObject(expectedNotification);
});
@@ -553,22 +539,6 @@ describe("Test utils", () => {
).toBeFalsy();
});
- test("When invalid payload for custom notification is passed to buildNotification", () => {
- expect(
- buildNotification({
- recipients: ["test.mail@mail.com"],
- type: "TestNotificationType",
- priority: "NEUTRAL",
- payload: "invalid"
- })
- ).toBeFalsy();
- });
-
- test("When no notification data for custom notification is passed to buildNotification", () => {
- expect(buildNotification(undefined)).toBeFalsy();
- expect(buildNotification(null)).toBeFalsy();
- });
-
test("Given invalid NTypes | When validateNotificationTypes is called | Then false is returned", () => {
expect(validateNotificationTypes([{ NotificationTypeKey: "Test" }, { blabla: "Test2" }])).toEqual(false);
});
@@ -578,20 +548,6 @@ describe("Test utils", () => {
expect(validateNotificationTypes([{ NotificationTypeKey: "Test" }, { NotificationTypeKey: "Test2" }])).toEqual(true);
});
- test("Given invalid inputs | When doesKeyExist is called | Then false is returned", () => {
- expect(doesKeyExist({ test: "test1" }, {})).toEqual(false);
- expect(doesKeyExist([{ test: "test1" }], "test")).toEqual(false);
- });
-
- test("Given that key does not exist | When doesKeyExist is called | Then false is returned", () => {
- expect(doesKeyExist({ test: "test1" }, "doesnotexist")).toEqual(false);
- expect(doesKeyExist({ test: "test1" }, "test1")).toEqual(false);
- });
-
- test("Given that key does exist | When doesKeyExist is called | Then true is returned", () => {
- expect(doesKeyExist({ test: "test1" }, "test")).toEqual(true);
- });
-
test("Given that file does not exist | When readFile is called | Then empty array is returned", () => {
existsSync.mockReturnValue(false);
expect(readFile("test.json")).toMatchObject([]);
diff --git a/test/srv/notifyToRest.test.js b/test/srv/notifyToRest.test.js
index b2a0883..e2f1348 100644
--- a/test/srv/notifyToRest.test.js
+++ b/test/srv/notifyToRest.test.js
@@ -1,15 +1,11 @@
-const NotifyToRest = require("../../srv/notifyToRest");
const { messages, buildNotification } = require("../../lib/utils");
-const { postNotification } = require("../../lib/notifications");
-
-jest.mock("./../../lib/notifications");
+const NotifyToRest = require("../../srv/notifyToRest");
describe("notify to rest", () => {
it("when no object is passed", async () => {
const notifyToRest = new NotifyToRest();
const warnSpy = jest.spyOn(global.console, "warn");
notifyToRest.notify();
- expect(warnSpy).toHaveBeenCalled();
expect(warnSpy).toHaveBeenCalledWith("[notifications] -", messages.NO_OBJECT_FOR_NOTIFY);
warnSpy.mockClear();
});
@@ -18,7 +14,6 @@ describe("notify to rest", () => {
const notifyToRest = new NotifyToRest();
const warnSpy = jest.spyOn(global.console, "warn");
notifyToRest.notify({});
- expect(warnSpy).toHaveBeenCalled();
expect(warnSpy).toHaveBeenCalledWith("[notifications] -", messages.EMPTY_OBJECT_FOR_NOTIFY);
warnSpy.mockClear();
});
@@ -27,7 +22,6 @@ describe("notify to rest", () => {
const notifyToRest = new NotifyToRest();
const warnSpy = jest.spyOn(global.console, "warn");
notifyToRest.notify({ dummy: true });
- expect(warnSpy).toHaveBeenCalled();
expect(warnSpy).toHaveBeenCalledWith("[notifications] -", messages.MANDATORY_PARAMETER_NOT_PASSED_FOR_DEFAULT_NOTIFICATION);
warnSpy.mockClear();
});
@@ -36,7 +30,6 @@ describe("notify to rest", () => {
const notifyToRest = new NotifyToRest();
const warnSpy = jest.spyOn(global.console, "warn");
notifyToRest.notify({ title: 1, recipients: ["abc@abc.com"] });
- expect(warnSpy).toHaveBeenCalled();
expect(warnSpy).toHaveBeenCalledWith("[notifications] -", messages.TITLE_IS_NOT_STRING);
warnSpy.mockClear();
});
@@ -45,7 +38,6 @@ describe("notify to rest", () => {
const notifyToRest = new NotifyToRest();
const warnSpy = jest.spyOn(global.console, "warn");
notifyToRest.notify({ title: "abc", recipients: ["abc@abc.com"], priority: "abc" });
- expect(warnSpy).toHaveBeenCalled();
expect(warnSpy).toHaveBeenCalledWith("[notifications] -", "Invalid priority abc. Allowed priorities are LOW, NEUTRAL, MEDIUM, HIGH");
warnSpy.mockClear();
});
@@ -54,19 +46,16 @@ describe("notify to rest", () => {
const notifyToRest = new NotifyToRest();
const warnSpy = jest.spyOn(global.console, "warn");
notifyToRest.notify({ title: "abc", recipients: ["abc@abc.com"], priority: "low", description: true });
- expect(warnSpy).toHaveBeenCalled();
expect(warnSpy).toHaveBeenCalledWith("[notifications] -", messages.DESCRIPTION_IS_NOT_STRING);
warnSpy.mockClear();
});
it(`When correct body is send | Then notification is posted`, async () => {
- postNotification.mockImplementation(() => undefined);
const body = { title: "abc", recipients: ["abc@abc.com"], priority: "low" };
-
const notifyToRest = new NotifyToRest();
+ let notification; notifyToRest.postNotification = n => notification = n
await notifyToRest.init();
await notifyToRest.notify(body);
- expect(postNotification).toHaveBeenCalled();
- expect(postNotification.mock.calls[0][0]).toMatchObject(buildNotification(body));
+ expect(notification).toMatchObject(buildNotification(body));
});
});