Skip to content

Commit ced278a

Browse files
Merge branch 'MVP' into simeonPetkov96-patch-1
2 parents a8b7061 + 67e3c9a commit ced278a

15 files changed

+6533
-16
lines changed

.prettierrc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"tabWidth": 2,
3+
"useTabs": false,
4+
"printWidth": 150
5+
}

cds-plugin.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const cds = require("@sap/cds");
2+
const { validateNotificationTypes, readFile } = require("./lib/utils");
3+
const { createNotificationTypesMap } = require("./lib/notificationTypes");
4+
const { setGlobalLogLevel } = require("@sap-cloud-sdk/util");
5+
6+
cds.once("served", async () => {
7+
setGlobalLogLevel("error");
8+
const profiles = cds.env.profiles ?? [];
9+
const production = profiles.includes("production");
10+
11+
// read notification types
12+
const notificationTypes = readFile(cds.env.requires?.notifications?.types);
13+
14+
if (validateNotificationTypes(notificationTypes)) {
15+
if (!production) {
16+
const notificationTypesMap = createNotificationTypesMap(notificationTypes, true);
17+
cds.notifications = { local: { types: notificationTypesMap } };
18+
}
19+
}
20+
});

index.cds

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace sap.notifications;
2+
3+
service AlertNotificationService {
4+
/**
5+
* TODO : connect action to notify api.
6+
*/
7+
action notify();
8+
}

jest.config.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// FIXME: should not be necessary
2+
process.env.CDS_ENV = 'better-sqlite'
3+
4+
const config = {
5+
testTimeout: 42222,
6+
testMatch: ['**/*.test.js']
7+
}
8+
9+
module.exports = config

lib/content-deployment.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const cds = require("@sap/cds");
2+
const { validateNotificationTypes, readFile } = require("./utils");
3+
const { createNotificationTypesMap, processNotificationTypes } = require("./notificationTypes");
4+
const { setGlobalLogLevel } = require("@sap-cloud-sdk/util");
5+
6+
async function deployNotificationTypes() {
7+
setGlobalLogLevel("error");
8+
9+
// read notification types
10+
const notificationTypes = readFile(cds.env.requires?.notifications?.types);
11+
12+
if (validateNotificationTypes(notificationTypes)) {
13+
await processNotificationTypes(notificationTypes);
14+
}
15+
}
16+
17+
deployNotificationTypes();

lib/notificationTypes.js

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
const { executeHttpRequest } = require("@sap-cloud-sdk/http-client");
2+
const { buildHeadersForDestination } = require("@sap-cloud-sdk/connectivity");
3+
const { getNotificationDestination, doesKeyExist, getPrefix, getNotificationTypesKeyWithPrefix, executeRequest } = require("./utils");
4+
const _ = require("lodash");
5+
6+
const NOTIFICATION_TYPES_API_ENDPOINT = "v2/NotificationType.svc";
7+
8+
const defaultTemplate = {
9+
"NotificationTypeKey": "Default",
10+
"NotificationTypeVersion": "1",
11+
"Templates": [
12+
{
13+
"Language": "en",
14+
"Description": "Other Notifications",
15+
"TemplatePublic": "{{title}}",
16+
"TemplateSensitive": "{{title}}",
17+
"TemplateGrouped": "Other Notifications",
18+
"TemplateLanguage": "mustache",
19+
"Subtitle": "{{description}}"
20+
}
21+
]
22+
}
23+
24+
function fromOdataArrayFormat(objectInArray) {
25+
if (objectInArray === undefined || objectInArray === null || Array.isArray(objectInArray)) {
26+
return objectInArray;
27+
} else {
28+
return objectInArray.results;
29+
}
30+
}
31+
32+
function createNotificationTypesMap(notificationTypesJSON, isLocal = false) {
33+
const types = {};
34+
35+
if(isLocal) {
36+
types["Default"] = { "1": defaultTemplate };
37+
}
38+
39+
// add user provided templates
40+
notificationTypesJSON.forEach((notificationType) => {
41+
// set default NotificationTypeVersion if required
42+
if(notificationType.NotificationTypeVersion === undefined) {
43+
notificationType.NotificationTypeVersion = "1";
44+
}
45+
46+
const notificationTypeKeyWithPrefix = getNotificationTypesKeyWithPrefix(notificationType.NotificationTypeKey);
47+
48+
// update the notification type key with prefix
49+
notificationType.NotificationTypeKey = notificationTypeKeyWithPrefix;
50+
51+
if (!doesKeyExist(types, notificationTypeKeyWithPrefix)) {
52+
types[notificationTypeKeyWithPrefix] = {};
53+
}
54+
55+
types[notificationTypeKeyWithPrefix][notificationType.NotificationTypeVersion] = notificationType;
56+
});
57+
58+
return types;
59+
}
60+
61+
async function getNotificationTypes() {
62+
const notificationDestination = await getNotificationDestination();
63+
const response = await executeHttpRequest(notificationDestination, {
64+
url: `${NOTIFICATION_TYPES_API_ENDPOINT}/NotificationTypes?$format=json&$expand=Templates,Actions,DeliveryChannels`,
65+
method: "get",
66+
});
67+
return response.data.d.results;
68+
}
69+
70+
async function createNotificationType(notificationType) {
71+
const notificationDestination = await getNotificationDestination();
72+
const csrfHeaders = await buildHeadersForDestination(notificationDestination, {
73+
url: NOTIFICATION_TYPES_API_ENDPOINT,
74+
});
75+
76+
console.log(
77+
`Notification Type of key ${notificationType.NotificationTypeKey} and version ${notificationType.NotificationTypeVersion} was not found. Creating it...`
78+
);
79+
80+
const response = await executeHttpRequest(notificationDestination, {
81+
url: `${NOTIFICATION_TYPES_API_ENDPOINT}/NotificationTypes`,
82+
method: "post",
83+
data: notificationType,
84+
headers: csrfHeaders,
85+
});
86+
return response.data.d;
87+
}
88+
89+
async function updateNotificationType(id, notificationType) {
90+
const notificationDestination = await getNotificationDestination();
91+
const csrfHeaders = await buildHeadersForDestination(notificationDestination, {
92+
url: NOTIFICATION_TYPES_API_ENDPOINT,
93+
});
94+
95+
console.log(
96+
`Detected change in notification type of key ${notificationType.NotificationTypeKey} and version ${notificationType.NotificationTypeVersion}. Updating it...`
97+
);
98+
99+
const response = await executeHttpRequest(notificationDestination, {
100+
url: `${NOTIFICATION_TYPES_API_ENDPOINT}/NotificationTypes(guid'${id}')`,
101+
method: "patch",
102+
data: notificationType,
103+
headers: csrfHeaders,
104+
});
105+
return response.status;
106+
}
107+
108+
async function deleteNotificationType(notificationType) {
109+
const notificationDestination = await getNotificationDestination();
110+
const csrfHeaders = await buildHeadersForDestination(notificationDestination, {
111+
url: NOTIFICATION_TYPES_API_ENDPOINT,
112+
});
113+
114+
console.log(
115+
`Notification Type of key ${notificationType.NotificationTypeKey} and version ${notificationType.NotificationTypeVersion} not present in the types file. Deleting it...`
116+
);
117+
118+
const response = await executeHttpRequest(notificationDestination, {
119+
url: `${NOTIFICATION_TYPES_API_ENDPOINT}/NotificationTypes(guid'${notificationType.NotificationTypeId}')`,
120+
method: "delete",
121+
headers: csrfHeaders,
122+
});
123+
return response.status;
124+
}
125+
126+
function _createChannelsMap(channels) {
127+
if(channels === null || channels === undefined) {
128+
return {};
129+
}
130+
131+
const channelMap = {};
132+
133+
channels.forEach((channel) => {
134+
channelMap[channel.Type] = channel;
135+
})
136+
137+
return channelMap;
138+
}
139+
140+
function areDeliveryChannelsEqual(oldChannels, newChannels) {
141+
if(_.size(oldChannels) !== _.size(newChannels)) {
142+
return false;
143+
}
144+
145+
const oldChannelsMap = _createChannelsMap(oldChannels);
146+
const newChannelsMap = _createChannelsMap(newChannels);
147+
148+
for(type of Object.keys(oldChannelsMap)) {
149+
if(!(type in newChannelsMap)) return false;
150+
151+
const oldChannel = oldChannelsMap[type];
152+
const newChannel = newChannelsMap[type];
153+
154+
// TODO: Check if language is not there
155+
const equal =
156+
oldChannel.Type == newChannel.Type.toUpperCase() &&
157+
oldChannel.Enabled == newChannel.Enabled &&
158+
oldChannel.DefaultPreference == newChannel.DefaultPreference &&
159+
oldChannel.EditablePreference == newChannel.EditablePreference;
160+
161+
if(!equal) return false;
162+
delete newChannelsMap[type];
163+
}
164+
165+
return Object.keys(newChannelsMap).length == 0;
166+
}
167+
168+
function isActionEqual(oldAction, newAction) {
169+
return (
170+
oldAction.Language == newAction.Language.toUpperCase() &&
171+
oldAction.ActionId == newAction.ActionId &&
172+
oldAction.ActionText == newAction.ActionText &&
173+
oldAction.GroupActionText == newAction.GroupActionText &&
174+
oldAction.Nature == newAction.Nature
175+
)
176+
}
177+
178+
function areActionsEqual(oldActions, newActions) {
179+
if(_.size(oldActions) !== _.size(newActions)) {
180+
return false;
181+
}
182+
183+
let matchFound = false;
184+
for (const oldAction of oldActions) {
185+
for (const newAction of newActions) {
186+
if (isActionEqual(oldAction, newAction)) {
187+
matchFound = true;
188+
break;
189+
}
190+
}
191+
if (!matchFound) {
192+
return false;
193+
}
194+
}
195+
196+
return true;
197+
}
198+
199+
function isTemplateEqual(oldTemplate, newTemplate) {
200+
return (
201+
oldTemplate.Language == newTemplate.Language.toUpperCase() &&
202+
oldTemplate.TemplatePublic == newTemplate.TemplatePublic &&
203+
oldTemplate.TemplateSensitive == newTemplate.TemplateSensitive &&
204+
oldTemplate.TemplateGrouped == newTemplate.TemplateGrouped &&
205+
oldTemplate.Description == newTemplate.Description &&
206+
oldTemplate.TemplateLanguage == newTemplate.TemplateLanguage.toUpperCase() &&
207+
oldTemplate.Subtitle == newTemplate.Subtitle &&
208+
oldTemplate.EmailSubject == newTemplate.EmailSubject &&
209+
oldTemplate.EmailText == newTemplate.EmailText &&
210+
oldTemplate.EmailHtml == newTemplate.EmailHtml
211+
)
212+
}
213+
214+
function areTemplatesEqual(oldTemplates, newTemplates) {
215+
if(_.size(oldTemplates) !== _.size(newTemplates)) {
216+
return false;
217+
}
218+
219+
let matchFound = false;
220+
for (const oldTemplate of oldTemplates) {
221+
for (const newTemplate of newTemplates) {
222+
if (isTemplateEqual(oldTemplate, newTemplate)) {
223+
matchFound = true;
224+
break;
225+
}
226+
}
227+
if (!matchFound) {
228+
return false;
229+
}
230+
}
231+
232+
return true;
233+
}
234+
235+
function isNotificationTypeEqual(oldNotificationType, newNotificationType) {
236+
if(newNotificationType.IsGroupable === undefined) {
237+
newNotificationType.IsGroupable = true;
238+
}
239+
240+
return (
241+
oldNotificationType.IsGroupable == newNotificationType.IsGroupable &&
242+
areTemplatesEqual(oldNotificationType.Templates.results, fromOdataArrayFormat(newNotificationType.Templates)) &&
243+
areActionsEqual(oldNotificationType.Actions.results, fromOdataArrayFormat(newNotificationType.Actions)) &&
244+
areDeliveryChannelsEqual(oldNotificationType.DeliveryChannels.results, fromOdataArrayFormat(newNotificationType.DeliveryChannels))
245+
)
246+
}
247+
248+
async function processNotificationTypes(notificationTypesJSON) {
249+
const notificationTypes = createNotificationTypesMap(notificationTypesJSON);
250+
const prefix = getPrefix();
251+
let defaultTemplateExists = false;
252+
253+
// get notficationTypes
254+
const existingTypes = await getNotificationTypes();
255+
256+
// iterate through notification types
257+
for(const existingType of existingTypes) {
258+
if(existingType.NotificationTypeKey == "Default") {
259+
defaultTemplateExists = true;
260+
continue;
261+
}
262+
263+
if(!existingType.NotificationTypeKey.startsWith(`${prefix}/`)) {
264+
console.log(
265+
`Skipping Notification Type of other application: ${existingType.NotificationTypeKey}.`
266+
);
267+
continue;
268+
}
269+
270+
// if the type isn't present in the JSON file, delete it
271+
if(notificationTypes[existingType.NotificationTypeKey] === undefined || notificationTypes[existingType.NotificationTypeKey][existingType.NotificationTypeVersion] === undefined) {
272+
await deleteNotificationType(existingType);
273+
continue;
274+
}
275+
276+
const newType = JSON.parse(JSON.stringify(notificationTypes[existingType.NotificationTypeKey][existingType.NotificationTypeVersion]));
277+
278+
// if the type is there then verify if everything is same or not
279+
if(!isNotificationTypeEqual(existingType, newType)) {
280+
await updateNotificationType(existingType.NotificationTypeId, notificationTypes[existingType.NotificationTypeKey][existingType.NotificationTypeVersion])
281+
} else {
282+
console.log(
283+
`Notification Type of key ${existingType.NotificationTypeKey} and version ${existingType.NotificationTypeVersion} unchanged.`
284+
);
285+
}
286+
287+
delete notificationTypes[existingType.NotificationTypeKey][existingType.NotificationTypeVersion];
288+
if(Object.keys(notificationTypes[existingType.NotificationTypeKey]).length == 0) {
289+
delete notificationTypes[existingType.NotificationTypeKey];
290+
}
291+
}
292+
293+
// create default template if required
294+
if(!defaultTemplateExists) {
295+
await createNotificationType(defaultTemplate);
296+
}
297+
298+
// create notification types that aren't there
299+
for(const notificationTypeKey in notificationTypes) {
300+
for(const notificationTypeVersion in notificationTypes[notificationTypeKey]) {
301+
await createNotificationType(notificationTypes[notificationTypeKey][notificationTypeVersion]);
302+
}
303+
}
304+
}
305+
306+
module.exports = {
307+
createNotificationTypesMap,
308+
processNotificationTypes
309+
}

lib/notifications.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
const { buildHeadersForDestination } = require("@sap-cloud-sdk/connectivity");
2+
const { getNotificationDestination, executeRequest, buildNotification } = require("./utils");
3+
const NOTIFICATIONS_API_ENDPOINT = "v2/Notification.svc";
4+
5+
async function postNotification(notificationData) {
6+
const notificationDestination = await getNotificationDestination();
7+
const csrfHeaders = await buildHeadersForDestination(notificationDestination, {
8+
url: NOTIFICATIONS_API_ENDPOINT,
9+
});
10+
11+
const notification = buildNotification(notificationData);
12+
13+
if (notification) {
14+
const response = await executeRequest("post", `${NOTIFICATIONS_API_ENDPOINT}/Notifications`, notification, notificationDestination, csrfHeaders);
15+
return response.data?.d ?? response;
16+
}
17+
}
18+
19+
module.exports = {
20+
postNotification
21+
};
22+

0 commit comments

Comments
 (0)