@backstage/plugin-notifications-backend 0.4.1-next.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/dist/database/DatabaseNotificationsStore.cjs.js +298 -0
- package/dist/database/DatabaseNotificationsStore.cjs.js.map +1 -0
- package/dist/index.cjs.js +2 -841
- package/dist/index.cjs.js.map +1 -1
- package/dist/plugin.cjs.js +72 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/service/getUsersForEntityRef.cjs.js +98 -0
- package/dist/service/getUsersForEntityRef.cjs.js.map +1 -0
- package/dist/service/parseEntityOrderFieldParams.cjs.js +35 -0
- package/dist/service/parseEntityOrderFieldParams.cjs.js.map +1 -0
- package/dist/service/router.cjs.js +374 -0
- package/dist/service/router.cjs.js.map +1 -0
- package/package.json +15 -15
package/dist/index.cjs.js
CHANGED
|
@@ -2,848 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
-
var
|
|
6
|
-
var backendCommon = require('@backstage/backend-common');
|
|
7
|
-
var express = require('express');
|
|
8
|
-
var Router = require('express-promise-router');
|
|
9
|
-
var pluginNotificationsCommon = require('@backstage/plugin-notifications-common');
|
|
10
|
-
var uuid = require('uuid');
|
|
11
|
-
var errors = require('@backstage/errors');
|
|
12
|
-
var catalogModel = require('@backstage/catalog-model');
|
|
13
|
-
var pluginSignalsNode = require('@backstage/plugin-signals-node');
|
|
14
|
-
var pluginNotificationsNode = require('@backstage/plugin-notifications-node');
|
|
15
|
-
var alpha = require('@backstage/plugin-catalog-node/alpha');
|
|
5
|
+
var plugin = require('./plugin.cjs.js');
|
|
16
6
|
|
|
17
|
-
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
18
7
|
|
|
19
|
-
var express__default = /*#__PURE__*/_interopDefaultCompat(express);
|
|
20
|
-
var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
|
|
21
8
|
|
|
22
|
-
|
|
23
|
-
"@backstage/plugin-notifications-backend",
|
|
24
|
-
"migrations"
|
|
25
|
-
);
|
|
26
|
-
const NOTIFICATION_COLUMNS = [
|
|
27
|
-
"id",
|
|
28
|
-
"title",
|
|
29
|
-
"description",
|
|
30
|
-
"severity",
|
|
31
|
-
"link",
|
|
32
|
-
"origin",
|
|
33
|
-
"scope",
|
|
34
|
-
"topic",
|
|
35
|
-
"icon",
|
|
36
|
-
"created",
|
|
37
|
-
"updated",
|
|
38
|
-
"user",
|
|
39
|
-
"read",
|
|
40
|
-
"saved"
|
|
41
|
-
];
|
|
42
|
-
const normalizeSeverity = (input) => {
|
|
43
|
-
let lower = (input ?? "normal").toLowerCase();
|
|
44
|
-
if (pluginNotificationsCommon.notificationSeverities.indexOf(lower) < 0) {
|
|
45
|
-
lower = "normal";
|
|
46
|
-
}
|
|
47
|
-
return lower;
|
|
48
|
-
};
|
|
49
|
-
class DatabaseNotificationsStore {
|
|
50
|
-
constructor(db) {
|
|
51
|
-
this.db = db;
|
|
52
|
-
this.isSQLite = this.db.client.config.client.includes("sqlite3");
|
|
53
|
-
}
|
|
54
|
-
isSQLite = false;
|
|
55
|
-
static async create({
|
|
56
|
-
database,
|
|
57
|
-
skipMigrations
|
|
58
|
-
}) {
|
|
59
|
-
const client = await database.getClient();
|
|
60
|
-
if (!database.migrations?.skip && !skipMigrations) {
|
|
61
|
-
await client.migrate.latest({
|
|
62
|
-
directory: migrationsDir
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
return new DatabaseNotificationsStore(client);
|
|
66
|
-
}
|
|
67
|
-
mapToInteger = (val) => {
|
|
68
|
-
return typeof val === "string" ? Number.parseInt(val, 10) : val ?? 0;
|
|
69
|
-
};
|
|
70
|
-
mapToNotifications = (rows) => {
|
|
71
|
-
return rows.map((row) => ({
|
|
72
|
-
id: row.id,
|
|
73
|
-
user: row.user,
|
|
74
|
-
created: new Date(row.created),
|
|
75
|
-
saved: row.saved,
|
|
76
|
-
read: row.read,
|
|
77
|
-
updated: row.updated,
|
|
78
|
-
origin: row.origin,
|
|
79
|
-
payload: {
|
|
80
|
-
title: row.title,
|
|
81
|
-
description: row.description,
|
|
82
|
-
link: row.link,
|
|
83
|
-
topic: row.topic,
|
|
84
|
-
severity: row.severity,
|
|
85
|
-
scope: row.scope,
|
|
86
|
-
icon: row.icon
|
|
87
|
-
}
|
|
88
|
-
}));
|
|
89
|
-
};
|
|
90
|
-
mapNotificationToDbRow = (notification) => {
|
|
91
|
-
return {
|
|
92
|
-
id: notification.id,
|
|
93
|
-
user: notification.user,
|
|
94
|
-
origin: notification.origin,
|
|
95
|
-
created: notification.created,
|
|
96
|
-
topic: notification.payload?.topic,
|
|
97
|
-
link: notification.payload?.link,
|
|
98
|
-
title: notification.payload?.title,
|
|
99
|
-
description: notification.payload?.description,
|
|
100
|
-
severity: normalizeSeverity(notification.payload?.severity),
|
|
101
|
-
scope: notification.payload?.scope,
|
|
102
|
-
icon: notification.payload.icon,
|
|
103
|
-
saved: notification.saved,
|
|
104
|
-
read: notification.read
|
|
105
|
-
};
|
|
106
|
-
};
|
|
107
|
-
mapBroadcastToDbRow = (notification) => {
|
|
108
|
-
return {
|
|
109
|
-
id: notification.id,
|
|
110
|
-
origin: notification.origin,
|
|
111
|
-
created: notification.created,
|
|
112
|
-
topic: notification.payload?.topic,
|
|
113
|
-
link: notification.payload?.link,
|
|
114
|
-
title: notification.payload?.title,
|
|
115
|
-
description: notification.payload?.description,
|
|
116
|
-
severity: normalizeSeverity(notification.payload?.severity),
|
|
117
|
-
icon: notification.payload.icon,
|
|
118
|
-
scope: notification.payload?.scope
|
|
119
|
-
};
|
|
120
|
-
};
|
|
121
|
-
getBroadcastUnion = (user) => {
|
|
122
|
-
return this.db("broadcast").leftJoin("broadcast_user_status", function clause() {
|
|
123
|
-
const join = this.on("id", "=", "broadcast_user_status.broadcast_id");
|
|
124
|
-
if (user !== null && user !== void 0) {
|
|
125
|
-
join.andOnVal("user", "=", user);
|
|
126
|
-
}
|
|
127
|
-
}).select(NOTIFICATION_COLUMNS);
|
|
128
|
-
};
|
|
129
|
-
getNotificationsBaseQuery = (options) => {
|
|
130
|
-
const { user, orderField } = options;
|
|
131
|
-
const subQuery = this.db("notification").select(NOTIFICATION_COLUMNS).unionAll([this.getBroadcastUnion(user)]).as("notifications");
|
|
132
|
-
const query = this.db.from(subQuery).where((q) => {
|
|
133
|
-
q.where("user", user).orWhereNull("user");
|
|
134
|
-
});
|
|
135
|
-
if (orderField && orderField.length > 0) {
|
|
136
|
-
orderField.forEach((orderBy) => {
|
|
137
|
-
query.orderBy(orderBy.field, orderBy.order);
|
|
138
|
-
});
|
|
139
|
-
} else if (!orderField) {
|
|
140
|
-
query.orderBy("created", "desc");
|
|
141
|
-
}
|
|
142
|
-
if (options.createdAfter) {
|
|
143
|
-
if (this.isSQLite) {
|
|
144
|
-
query.where("created", ">=", options.createdAfter.valueOf());
|
|
145
|
-
} else {
|
|
146
|
-
query.where("created", ">=", options.createdAfter.toISOString());
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
if (options.limit) {
|
|
150
|
-
query.limit(options.limit);
|
|
151
|
-
}
|
|
152
|
-
if (options.offset) {
|
|
153
|
-
query.offset(options.offset);
|
|
154
|
-
}
|
|
155
|
-
if (options.search) {
|
|
156
|
-
query.whereRaw(
|
|
157
|
-
`(LOWER(title) LIKE LOWER(?) OR LOWER(description) LIKE LOWER(?))`,
|
|
158
|
-
[`%${options.search}%`, `%${options.search}%`]
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
if (options.ids) {
|
|
162
|
-
query.whereIn("id", options.ids);
|
|
163
|
-
}
|
|
164
|
-
if (options.read) {
|
|
165
|
-
query.whereNotNull("read");
|
|
166
|
-
} else if (options.read === false) {
|
|
167
|
-
query.whereNull("read");
|
|
168
|
-
}
|
|
169
|
-
if (options.topic) {
|
|
170
|
-
query.where("topic", "=", options.topic);
|
|
171
|
-
}
|
|
172
|
-
if (options.saved) {
|
|
173
|
-
query.whereNotNull("saved");
|
|
174
|
-
} else if (options.saved === false) {
|
|
175
|
-
query.whereNull("saved");
|
|
176
|
-
}
|
|
177
|
-
if (options.minimumSeverity !== void 0) {
|
|
178
|
-
const idx = pluginNotificationsCommon.notificationSeverities.indexOf(options.minimumSeverity);
|
|
179
|
-
const equalOrHigher = pluginNotificationsCommon.notificationSeverities.slice(0, idx + 1);
|
|
180
|
-
query.whereIn("severity", equalOrHigher);
|
|
181
|
-
}
|
|
182
|
-
return query;
|
|
183
|
-
};
|
|
184
|
-
async getNotifications(options) {
|
|
185
|
-
const notificationQuery = this.getNotificationsBaseQuery(options);
|
|
186
|
-
const notifications = await notificationQuery.select(NOTIFICATION_COLUMNS);
|
|
187
|
-
return this.mapToNotifications(notifications);
|
|
188
|
-
}
|
|
189
|
-
async getNotificationsCount(options) {
|
|
190
|
-
const countOptions = { ...options };
|
|
191
|
-
countOptions.limit = void 0;
|
|
192
|
-
countOptions.offset = void 0;
|
|
193
|
-
countOptions.orderField = [];
|
|
194
|
-
const notificationQuery = this.getNotificationsBaseQuery(countOptions);
|
|
195
|
-
const response = await notificationQuery.count("id as CNT");
|
|
196
|
-
return Number(response[0].CNT);
|
|
197
|
-
}
|
|
198
|
-
async saveNotification(notification) {
|
|
199
|
-
await this.db.insert(this.mapNotificationToDbRow(notification)).into("notification");
|
|
200
|
-
}
|
|
201
|
-
async saveBroadcast(notification) {
|
|
202
|
-
await this.db.insert(this.mapBroadcastToDbRow(notification)).into("broadcast");
|
|
203
|
-
if (notification.saved || notification.read) {
|
|
204
|
-
await this.db.insert({
|
|
205
|
-
user: notification.user,
|
|
206
|
-
broadcast_id: notification.id,
|
|
207
|
-
saved: notification.saved,
|
|
208
|
-
read: notification.read
|
|
209
|
-
}).into("broadcast_user_status");
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
async getStatus(options) {
|
|
213
|
-
const notificationQuery = this.getNotificationsBaseQuery({
|
|
214
|
-
...options,
|
|
215
|
-
orderField: []
|
|
216
|
-
});
|
|
217
|
-
const readSubQuery = notificationQuery.clone().count("id").whereNotNull("read").as("READ");
|
|
218
|
-
const unreadSubQuery = notificationQuery.clone().count("id").whereNull("read").as("UNREAD");
|
|
219
|
-
const query = await notificationQuery.select(readSubQuery, unreadSubQuery).first();
|
|
220
|
-
return {
|
|
221
|
-
unread: this.mapToInteger(query?.UNREAD),
|
|
222
|
-
read: this.mapToInteger(query?.READ)
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
async getExistingScopeNotification(options) {
|
|
226
|
-
const query = this.db("notification").where("user", options.user).where("scope", options.scope).where("origin", options.origin).limit(1);
|
|
227
|
-
const rows = await query;
|
|
228
|
-
if (!rows || rows.length === 0) {
|
|
229
|
-
return null;
|
|
230
|
-
}
|
|
231
|
-
return rows[0];
|
|
232
|
-
}
|
|
233
|
-
async getExistingScopeBroadcast(options) {
|
|
234
|
-
const query = this.db("broadcast").where("scope", options.scope).where("origin", options.origin).limit(1);
|
|
235
|
-
const rows = await query;
|
|
236
|
-
if (!rows || rows.length === 0) {
|
|
237
|
-
return null;
|
|
238
|
-
}
|
|
239
|
-
return rows[0];
|
|
240
|
-
}
|
|
241
|
-
async restoreExistingNotification({
|
|
242
|
-
id,
|
|
243
|
-
notification
|
|
244
|
-
}) {
|
|
245
|
-
const updateColumns = {
|
|
246
|
-
title: notification.payload.title,
|
|
247
|
-
description: notification.payload.description,
|
|
248
|
-
link: notification.payload.link,
|
|
249
|
-
topic: notification.payload.topic,
|
|
250
|
-
updated: /* @__PURE__ */ new Date(),
|
|
251
|
-
severity: normalizeSeverity(notification.payload?.severity),
|
|
252
|
-
read: null
|
|
253
|
-
};
|
|
254
|
-
const notificationQuery = this.db("notification").where("id", id).where("user", notification.user);
|
|
255
|
-
const broadcastQuery = this.db("broadcast").where("id", id);
|
|
256
|
-
await Promise.all([
|
|
257
|
-
notificationQuery.update(updateColumns),
|
|
258
|
-
broadcastQuery.update({ ...updateColumns, read: void 0 })
|
|
259
|
-
]);
|
|
260
|
-
return await this.getNotification({ id, user: notification.user });
|
|
261
|
-
}
|
|
262
|
-
async getNotification(options) {
|
|
263
|
-
const rows = await this.db.select("*").from(
|
|
264
|
-
this.db("notification").select(NOTIFICATION_COLUMNS).unionAll([this.getBroadcastUnion(options.user)]).as("notifications")
|
|
265
|
-
).where("id", options.id).limit(1);
|
|
266
|
-
if (!rows || rows.length === 0) {
|
|
267
|
-
return null;
|
|
268
|
-
}
|
|
269
|
-
return this.mapToNotifications(rows)[0];
|
|
270
|
-
}
|
|
271
|
-
markReadSaved = async (ids, user, read, saved) => {
|
|
272
|
-
await this.db("notification").whereIn("id", ids).where("user", user).update({ read, saved });
|
|
273
|
-
const broadcasts = this.mapToNotifications(
|
|
274
|
-
await this.db("broadcast").whereIn("id", ids).select()
|
|
275
|
-
);
|
|
276
|
-
if (broadcasts.length > 0)
|
|
277
|
-
if (!this.isSQLite) {
|
|
278
|
-
await this.db("broadcast_user_status").insert(
|
|
279
|
-
broadcasts.map((b) => ({
|
|
280
|
-
broadcast_id: b.id,
|
|
281
|
-
user,
|
|
282
|
-
read,
|
|
283
|
-
saved
|
|
284
|
-
}))
|
|
285
|
-
).onConflict(["broadcast_id", "user"]).merge(["read", "saved"]);
|
|
286
|
-
} else {
|
|
287
|
-
for (const b of broadcasts) {
|
|
288
|
-
const baseQuery = this.db("broadcast_user_status").where("broadcast_id", b.id).where("user", user);
|
|
289
|
-
const exists = await baseQuery.clone().limit(1).select().first();
|
|
290
|
-
if (exists) {
|
|
291
|
-
await baseQuery.clone().update({ read, saved });
|
|
292
|
-
} else {
|
|
293
|
-
await baseQuery.clone().insert({ broadcast_id: b.id, user, read, saved });
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
};
|
|
298
|
-
async markRead(options) {
|
|
299
|
-
await this.markReadSaved(options.ids, options.user, /* @__PURE__ */ new Date(), void 0);
|
|
300
|
-
}
|
|
301
|
-
async markUnread(options) {
|
|
302
|
-
await this.markReadSaved(options.ids, options.user, null, void 0);
|
|
303
|
-
}
|
|
304
|
-
async markSaved(options) {
|
|
305
|
-
await this.markReadSaved(options.ids, options.user, void 0, /* @__PURE__ */ new Date());
|
|
306
|
-
}
|
|
307
|
-
async markUnsaved(options) {
|
|
308
|
-
await this.markReadSaved(options.ids, options.user, void 0, null);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function parseStringsParam(param, ctx) {
|
|
313
|
-
if (param === void 0) {
|
|
314
|
-
return void 0;
|
|
315
|
-
}
|
|
316
|
-
const array = [param].flat();
|
|
317
|
-
if (array.some((p) => typeof p !== "string")) {
|
|
318
|
-
throw new errors.InputError(`Invalid ${ctx}, not a string`);
|
|
319
|
-
}
|
|
320
|
-
return array;
|
|
321
|
-
}
|
|
322
|
-
function parseEntityOrderFieldParams(params) {
|
|
323
|
-
const orderFieldStrings = parseStringsParam(params.orderField, "orderField");
|
|
324
|
-
if (!orderFieldStrings) {
|
|
325
|
-
return void 0;
|
|
326
|
-
}
|
|
327
|
-
return orderFieldStrings.map((orderFieldString) => {
|
|
328
|
-
const [field, order] = orderFieldString.split(",");
|
|
329
|
-
if (order !== void 0 && !isOrder(order)) {
|
|
330
|
-
throw new errors.InputError("Invalid order field order, must be asc or desc");
|
|
331
|
-
}
|
|
332
|
-
return { field, order };
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
function isOrder(order) {
|
|
336
|
-
return ["asc", "desc"].includes(order);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const isUserEntityRef = (ref) => catalogModel.parseEntityRef(ref).kind.toLocaleLowerCase() === "user";
|
|
340
|
-
const partitionEntityRefs = (refs) => refs.reduce(
|
|
341
|
-
([userEntityRefs, otherEntityRefs], ref) => {
|
|
342
|
-
return isUserEntityRef(ref) ? [[...userEntityRefs, ref], otherEntityRefs] : [userEntityRefs, [...otherEntityRefs, ref]];
|
|
343
|
-
},
|
|
344
|
-
[[], []]
|
|
345
|
-
);
|
|
346
|
-
const getUsersForEntityRef = async (entityRef, excludeEntityRefs, options) => {
|
|
347
|
-
const { auth, catalogClient } = options;
|
|
348
|
-
if (entityRef === null) {
|
|
349
|
-
return [];
|
|
350
|
-
}
|
|
351
|
-
const { token } = await auth.getPluginRequestToken({
|
|
352
|
-
onBehalfOf: await auth.getOwnServiceCredentials(),
|
|
353
|
-
targetPluginId: "catalog"
|
|
354
|
-
});
|
|
355
|
-
const excluded = Array.isArray(excludeEntityRefs) ? excludeEntityRefs : [excludeEntityRefs];
|
|
356
|
-
const refsArr = Array.isArray(entityRef) ? entityRef : [entityRef];
|
|
357
|
-
const [userEntityRefs, otherEntityRefs] = partitionEntityRefs(refsArr);
|
|
358
|
-
const users = userEntityRefs.filter((ref) => !excluded.includes(ref));
|
|
359
|
-
const entityRefs = otherEntityRefs.filter((ref) => !excluded.includes(ref));
|
|
360
|
-
const fields = ["kind", "metadata.name", "metadata.namespace", "relations"];
|
|
361
|
-
let entities = [];
|
|
362
|
-
if (entityRefs.length > 0) {
|
|
363
|
-
const fetchedEntities = await catalogClient.getEntitiesByRefs(
|
|
364
|
-
{
|
|
365
|
-
entityRefs,
|
|
366
|
-
fields
|
|
367
|
-
},
|
|
368
|
-
{ token }
|
|
369
|
-
);
|
|
370
|
-
entities = fetchedEntities.items;
|
|
371
|
-
}
|
|
372
|
-
const mapEntity = async (entity) => {
|
|
373
|
-
if (!entity) {
|
|
374
|
-
return [];
|
|
375
|
-
}
|
|
376
|
-
const currentEntityRef = catalogModel.stringifyEntityRef(entity);
|
|
377
|
-
if (excluded.includes(currentEntityRef)) {
|
|
378
|
-
return [];
|
|
379
|
-
}
|
|
380
|
-
if (catalogModel.isUserEntity(entity)) {
|
|
381
|
-
return [currentEntityRef];
|
|
382
|
-
}
|
|
383
|
-
if (catalogModel.isGroupEntity(entity)) {
|
|
384
|
-
if (!entity.relations?.length) {
|
|
385
|
-
return [];
|
|
386
|
-
}
|
|
387
|
-
const groupUsers = entity.relations.filter(
|
|
388
|
-
(relation) => relation.type === catalogModel.RELATION_HAS_MEMBER && isUserEntityRef(relation.targetRef)
|
|
389
|
-
).map((r) => r.targetRef);
|
|
390
|
-
const childGroupRefs = entity.relations.filter((relation) => relation.type === catalogModel.RELATION_PARENT_OF).map((r) => r.targetRef);
|
|
391
|
-
let childGroupUsers = [];
|
|
392
|
-
if (childGroupRefs.length > 0) {
|
|
393
|
-
const childGroups = await catalogClient.getEntitiesByRefs(
|
|
394
|
-
{
|
|
395
|
-
entityRefs: childGroupRefs,
|
|
396
|
-
fields
|
|
397
|
-
},
|
|
398
|
-
{ token }
|
|
399
|
-
);
|
|
400
|
-
childGroupUsers = await Promise.all(childGroups.items.map(mapEntity));
|
|
401
|
-
}
|
|
402
|
-
return [...groupUsers, ...childGroupUsers.flat(2)].filter(
|
|
403
|
-
(ref) => !excluded.includes(ref)
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
if (entity.relations?.length) {
|
|
407
|
-
const ownerRef = entity.relations.find(
|
|
408
|
-
(relation) => relation.type === catalogModel.RELATION_OWNED_BY
|
|
409
|
-
)?.targetRef;
|
|
410
|
-
if (!ownerRef) {
|
|
411
|
-
return [];
|
|
412
|
-
}
|
|
413
|
-
if (isUserEntityRef(ownerRef)) {
|
|
414
|
-
if (excluded.includes(ownerRef)) {
|
|
415
|
-
return [];
|
|
416
|
-
}
|
|
417
|
-
return [ownerRef];
|
|
418
|
-
}
|
|
419
|
-
const owner = await catalogClient.getEntityByRef(ownerRef, { token });
|
|
420
|
-
return mapEntity(owner);
|
|
421
|
-
}
|
|
422
|
-
return [];
|
|
423
|
-
};
|
|
424
|
-
for (const entity of entities) {
|
|
425
|
-
const u = await mapEntity(entity);
|
|
426
|
-
users.push(...u);
|
|
427
|
-
}
|
|
428
|
-
return [...new Set(users)].filter(Boolean);
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
async function createRouter(options) {
|
|
432
|
-
const {
|
|
433
|
-
config,
|
|
434
|
-
logger,
|
|
435
|
-
database,
|
|
436
|
-
auth,
|
|
437
|
-
httpAuth,
|
|
438
|
-
userInfo,
|
|
439
|
-
catalog,
|
|
440
|
-
processors = [],
|
|
441
|
-
signals
|
|
442
|
-
} = options;
|
|
443
|
-
const store = await DatabaseNotificationsStore.create({ database });
|
|
444
|
-
const frontendBaseUrl = config.getString("app.baseUrl");
|
|
445
|
-
const getUser = async (req) => {
|
|
446
|
-
const credentials = await httpAuth.credentials(req, { allow: ["user"] });
|
|
447
|
-
const info = await userInfo.getUserInfo(credentials);
|
|
448
|
-
return info.userEntityRef;
|
|
449
|
-
};
|
|
450
|
-
const filterProcessors = (payload) => {
|
|
451
|
-
const result = [];
|
|
452
|
-
for (const processor of processors) {
|
|
453
|
-
if (processor.getNotificationFilters) {
|
|
454
|
-
const filters = processor.getNotificationFilters();
|
|
455
|
-
if (filters.minSeverity) {
|
|
456
|
-
if (pluginNotificationsCommon.notificationSeverities.indexOf(payload.severity ?? "normal") > pluginNotificationsCommon.notificationSeverities.indexOf(filters.minSeverity)) {
|
|
457
|
-
continue;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
if (filters.maxSeverity) {
|
|
461
|
-
if (pluginNotificationsCommon.notificationSeverities.indexOf(payload.severity ?? "normal") < pluginNotificationsCommon.notificationSeverities.indexOf(filters.maxSeverity)) {
|
|
462
|
-
continue;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
if (filters.excludedTopics && payload.topic) {
|
|
466
|
-
if (filters.excludedTopics.includes(payload.topic)) {
|
|
467
|
-
continue;
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
result.push(processor);
|
|
472
|
-
}
|
|
473
|
-
return result;
|
|
474
|
-
};
|
|
475
|
-
const processOptions = async (opts) => {
|
|
476
|
-
const filtered = filterProcessors(opts.payload);
|
|
477
|
-
let ret = opts;
|
|
478
|
-
for (const processor of filtered) {
|
|
479
|
-
try {
|
|
480
|
-
ret = processor.processOptions ? await processor.processOptions(ret) : ret;
|
|
481
|
-
} catch (e) {
|
|
482
|
-
logger.error(
|
|
483
|
-
`Error while processing notification options with ${processor.getName()}: ${e}`
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
return ret;
|
|
488
|
-
};
|
|
489
|
-
const preProcessNotification = async (notification, opts) => {
|
|
490
|
-
const filtered = filterProcessors(notification.payload);
|
|
491
|
-
let ret = notification;
|
|
492
|
-
for (const processor of filtered) {
|
|
493
|
-
try {
|
|
494
|
-
ret = processor.preProcess ? await processor.preProcess(ret, opts) : ret;
|
|
495
|
-
} catch (e) {
|
|
496
|
-
logger.error(
|
|
497
|
-
`Error while pre processing notification with ${processor.getName()}: ${e}`
|
|
498
|
-
);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
return ret;
|
|
502
|
-
};
|
|
503
|
-
const postProcessNotification = async (notification, opts) => {
|
|
504
|
-
const filtered = filterProcessors(notification.payload);
|
|
505
|
-
for (const processor of filtered) {
|
|
506
|
-
if (processor.postProcess) {
|
|
507
|
-
try {
|
|
508
|
-
await processor.postProcess(notification, opts);
|
|
509
|
-
} catch (e) {
|
|
510
|
-
logger.error(
|
|
511
|
-
`Error while post processing notification with ${processor.getName()}: ${e}`
|
|
512
|
-
);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
};
|
|
517
|
-
const validateLink = (link) => {
|
|
518
|
-
const stripLeadingSlash = (s) => s.replace(/^\//, "");
|
|
519
|
-
const ensureTrailingSlash = (s) => s.replace(/\/?$/, "/");
|
|
520
|
-
const url = new URL(
|
|
521
|
-
stripLeadingSlash(link),
|
|
522
|
-
ensureTrailingSlash(frontendBaseUrl)
|
|
523
|
-
);
|
|
524
|
-
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
525
|
-
throw new Error("Only HTTP/HTTPS links are allowed");
|
|
526
|
-
}
|
|
527
|
-
};
|
|
528
|
-
const router = Router__default.default();
|
|
529
|
-
router.use(express__default.default.json());
|
|
530
|
-
router.get("/health", (_, response) => {
|
|
531
|
-
logger.info("PONG!");
|
|
532
|
-
response.json({ status: "ok" });
|
|
533
|
-
});
|
|
534
|
-
router.get("/", async (req, res) => {
|
|
535
|
-
const user = await getUser(req);
|
|
536
|
-
const opts = {
|
|
537
|
-
user
|
|
538
|
-
};
|
|
539
|
-
if (req.query.offset) {
|
|
540
|
-
opts.offset = Number.parseInt(req.query.offset.toString(), 10);
|
|
541
|
-
}
|
|
542
|
-
if (req.query.limit) {
|
|
543
|
-
opts.limit = Number.parseInt(req.query.limit.toString(), 10);
|
|
544
|
-
}
|
|
545
|
-
if (req.query.orderField) {
|
|
546
|
-
opts.orderField = parseEntityOrderFieldParams(req.query);
|
|
547
|
-
}
|
|
548
|
-
if (req.query.search) {
|
|
549
|
-
opts.search = req.query.search.toString();
|
|
550
|
-
}
|
|
551
|
-
if (req.query.read === "true") {
|
|
552
|
-
opts.read = true;
|
|
553
|
-
} else if (req.query.read === "false") {
|
|
554
|
-
opts.read = false;
|
|
555
|
-
}
|
|
556
|
-
if (req.query.topic) {
|
|
557
|
-
opts.topic = req.query.topic.toString();
|
|
558
|
-
}
|
|
559
|
-
if (req.query.saved === "true") {
|
|
560
|
-
opts.saved = true;
|
|
561
|
-
} else if (req.query.saved === "false") {
|
|
562
|
-
opts.saved = false;
|
|
563
|
-
}
|
|
564
|
-
if (req.query.createdAfter) {
|
|
565
|
-
const sinceEpoch = Date.parse(String(req.query.createdAfter));
|
|
566
|
-
if (isNaN(sinceEpoch)) {
|
|
567
|
-
throw new errors.InputError("Unexpected date format");
|
|
568
|
-
}
|
|
569
|
-
opts.createdAfter = new Date(sinceEpoch);
|
|
570
|
-
}
|
|
571
|
-
if (req.query.minimumSeverity) {
|
|
572
|
-
opts.minimumSeverity = normalizeSeverity(
|
|
573
|
-
req.query.minimumSeverity.toString()
|
|
574
|
-
);
|
|
575
|
-
}
|
|
576
|
-
const [notifications, totalCount] = await Promise.all([
|
|
577
|
-
store.getNotifications(opts),
|
|
578
|
-
store.getNotificationsCount(opts)
|
|
579
|
-
]);
|
|
580
|
-
res.json({
|
|
581
|
-
totalCount,
|
|
582
|
-
notifications
|
|
583
|
-
});
|
|
584
|
-
});
|
|
585
|
-
router.get("/status", async (req, res) => {
|
|
586
|
-
const user = await getUser(req);
|
|
587
|
-
const status = await store.getStatus({ user });
|
|
588
|
-
res.json(status);
|
|
589
|
-
});
|
|
590
|
-
router.get("/:id", async (req, res) => {
|
|
591
|
-
const user = await getUser(req);
|
|
592
|
-
const opts = {
|
|
593
|
-
user,
|
|
594
|
-
limit: 1,
|
|
595
|
-
ids: [req.params.id]
|
|
596
|
-
};
|
|
597
|
-
const notifications = await store.getNotifications(opts);
|
|
598
|
-
if (notifications.length !== 1) {
|
|
599
|
-
throw new errors.NotFoundError("Not found");
|
|
600
|
-
}
|
|
601
|
-
res.json(notifications[0]);
|
|
602
|
-
});
|
|
603
|
-
router.post("/update", async (req, res) => {
|
|
604
|
-
const user = await getUser(req);
|
|
605
|
-
const { ids, read, saved } = req.body;
|
|
606
|
-
if (!ids || !Array.isArray(ids)) {
|
|
607
|
-
throw new errors.InputError();
|
|
608
|
-
}
|
|
609
|
-
if (read === true) {
|
|
610
|
-
await store.markRead({ user, ids });
|
|
611
|
-
if (signals) {
|
|
612
|
-
await signals.publish({
|
|
613
|
-
recipients: { type: "user", entityRef: [user] },
|
|
614
|
-
message: { action: "notification_read", notification_ids: ids },
|
|
615
|
-
channel: "notifications"
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
|
-
} else if (read === false) {
|
|
619
|
-
await store.markUnread({ user, ids });
|
|
620
|
-
if (signals) {
|
|
621
|
-
await signals.publish({
|
|
622
|
-
recipients: { type: "user", entityRef: [user] },
|
|
623
|
-
message: { action: "notification_unread", notification_ids: ids },
|
|
624
|
-
channel: "notifications"
|
|
625
|
-
});
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
if (saved === true) {
|
|
629
|
-
await store.markSaved({ user, ids });
|
|
630
|
-
} else if (saved === false) {
|
|
631
|
-
await store.markUnsaved({ user, ids });
|
|
632
|
-
}
|
|
633
|
-
const notifications = await store.getNotifications({ ids, user });
|
|
634
|
-
res.json(notifications);
|
|
635
|
-
});
|
|
636
|
-
const sendBroadcastNotification = async (baseNotification, opts, origin) => {
|
|
637
|
-
const { scope } = opts.payload;
|
|
638
|
-
const broadcastNotification = {
|
|
639
|
-
...baseNotification,
|
|
640
|
-
user: null,
|
|
641
|
-
id: uuid.v4()
|
|
642
|
-
};
|
|
643
|
-
const notification = await preProcessNotification(
|
|
644
|
-
broadcastNotification,
|
|
645
|
-
opts
|
|
646
|
-
);
|
|
647
|
-
let existingNotification;
|
|
648
|
-
if (scope) {
|
|
649
|
-
existingNotification = await store.getExistingScopeBroadcast({
|
|
650
|
-
scope,
|
|
651
|
-
origin
|
|
652
|
-
});
|
|
653
|
-
}
|
|
654
|
-
let ret = notification;
|
|
655
|
-
if (existingNotification) {
|
|
656
|
-
const restored = await store.restoreExistingNotification({
|
|
657
|
-
id: existingNotification.id,
|
|
658
|
-
notification: { ...notification, user: "" }
|
|
659
|
-
});
|
|
660
|
-
ret = restored ?? notification;
|
|
661
|
-
} else {
|
|
662
|
-
await store.saveBroadcast(notification);
|
|
663
|
-
}
|
|
664
|
-
if (signals) {
|
|
665
|
-
await signals.publish({
|
|
666
|
-
recipients: { type: "broadcast" },
|
|
667
|
-
message: {
|
|
668
|
-
action: "new_notification",
|
|
669
|
-
notification_id: ret.id
|
|
670
|
-
},
|
|
671
|
-
channel: "notifications"
|
|
672
|
-
});
|
|
673
|
-
postProcessNotification(ret, opts);
|
|
674
|
-
}
|
|
675
|
-
return notification;
|
|
676
|
-
};
|
|
677
|
-
const sendUserNotifications = async (baseNotification, users, opts, origin) => {
|
|
678
|
-
const notifications = [];
|
|
679
|
-
const { scope } = opts.payload;
|
|
680
|
-
const uniqueUsers = [...new Set(users)];
|
|
681
|
-
for (const user of uniqueUsers) {
|
|
682
|
-
const userNotification = {
|
|
683
|
-
...baseNotification,
|
|
684
|
-
id: uuid.v4(),
|
|
685
|
-
user
|
|
686
|
-
};
|
|
687
|
-
const notification = await preProcessNotification(userNotification, opts);
|
|
688
|
-
let existingNotification;
|
|
689
|
-
if (scope) {
|
|
690
|
-
existingNotification = await store.getExistingScopeNotification({
|
|
691
|
-
user,
|
|
692
|
-
scope,
|
|
693
|
-
origin
|
|
694
|
-
});
|
|
695
|
-
}
|
|
696
|
-
let ret = notification;
|
|
697
|
-
if (existingNotification) {
|
|
698
|
-
const restored = await store.restoreExistingNotification({
|
|
699
|
-
id: existingNotification.id,
|
|
700
|
-
notification
|
|
701
|
-
});
|
|
702
|
-
ret = restored ?? notification;
|
|
703
|
-
} else {
|
|
704
|
-
await store.saveNotification(notification);
|
|
705
|
-
}
|
|
706
|
-
notifications.push(ret);
|
|
707
|
-
if (signals) {
|
|
708
|
-
await signals.publish({
|
|
709
|
-
recipients: { type: "user", entityRef: [user] },
|
|
710
|
-
message: {
|
|
711
|
-
action: "new_notification",
|
|
712
|
-
notification_id: ret.id
|
|
713
|
-
},
|
|
714
|
-
channel: "notifications"
|
|
715
|
-
});
|
|
716
|
-
}
|
|
717
|
-
postProcessNotification(ret, opts);
|
|
718
|
-
}
|
|
719
|
-
return notifications;
|
|
720
|
-
};
|
|
721
|
-
router.post(
|
|
722
|
-
"/",
|
|
723
|
-
async (req, res) => {
|
|
724
|
-
const opts = await processOptions(req.body);
|
|
725
|
-
const { recipients, payload } = opts;
|
|
726
|
-
const notifications = [];
|
|
727
|
-
let users = [];
|
|
728
|
-
const credentials = await httpAuth.credentials(req, {
|
|
729
|
-
allow: ["service"]
|
|
730
|
-
});
|
|
731
|
-
const { title, link } = payload;
|
|
732
|
-
if (!recipients || !title) {
|
|
733
|
-
logger.error(`Invalid notification request received`);
|
|
734
|
-
throw new errors.InputError(`Invalid notification request received`);
|
|
735
|
-
}
|
|
736
|
-
if (link) {
|
|
737
|
-
try {
|
|
738
|
-
validateLink(link);
|
|
739
|
-
} catch (e) {
|
|
740
|
-
throw new errors.InputError("Invalid link provided", e);
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
const origin = credentials.principal.subject;
|
|
744
|
-
const baseNotification = {
|
|
745
|
-
payload: {
|
|
746
|
-
...payload,
|
|
747
|
-
severity: payload.severity ?? "normal"
|
|
748
|
-
},
|
|
749
|
-
origin,
|
|
750
|
-
created: /* @__PURE__ */ new Date()
|
|
751
|
-
};
|
|
752
|
-
if (recipients.type === "broadcast") {
|
|
753
|
-
const broadcast = await sendBroadcastNotification(
|
|
754
|
-
baseNotification,
|
|
755
|
-
opts,
|
|
756
|
-
origin
|
|
757
|
-
);
|
|
758
|
-
notifications.push(broadcast);
|
|
759
|
-
} else {
|
|
760
|
-
const entityRef = recipients.entityRef;
|
|
761
|
-
try {
|
|
762
|
-
users = await getUsersForEntityRef(
|
|
763
|
-
entityRef,
|
|
764
|
-
recipients.excludeEntityRef ?? [],
|
|
765
|
-
{ auth, catalogClient: catalog }
|
|
766
|
-
);
|
|
767
|
-
} catch (e) {
|
|
768
|
-
logger.error(`Failed to resolve notification receivers: ${e}`);
|
|
769
|
-
throw new errors.InputError("Failed to resolve notification receivers", e);
|
|
770
|
-
}
|
|
771
|
-
const userNotifications = await sendUserNotifications(
|
|
772
|
-
baseNotification,
|
|
773
|
-
users,
|
|
774
|
-
opts,
|
|
775
|
-
origin
|
|
776
|
-
);
|
|
777
|
-
notifications.push(...userNotifications);
|
|
778
|
-
}
|
|
779
|
-
res.json(notifications);
|
|
780
|
-
}
|
|
781
|
-
);
|
|
782
|
-
router.use(backendCommon.errorHandler());
|
|
783
|
-
return router;
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
class NotificationsProcessingExtensionPointImpl {
|
|
787
|
-
#processors = new Array();
|
|
788
|
-
addProcessor(...processors) {
|
|
789
|
-
this.#processors.push(...processors.flat());
|
|
790
|
-
}
|
|
791
|
-
get processors() {
|
|
792
|
-
return this.#processors;
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
const notificationsPlugin = backendPluginApi.createBackendPlugin({
|
|
796
|
-
pluginId: "notifications",
|
|
797
|
-
register(env) {
|
|
798
|
-
const processingExtensions = new NotificationsProcessingExtensionPointImpl();
|
|
799
|
-
env.registerExtensionPoint(
|
|
800
|
-
pluginNotificationsNode.notificationsProcessingExtensionPoint,
|
|
801
|
-
processingExtensions
|
|
802
|
-
);
|
|
803
|
-
env.registerInit({
|
|
804
|
-
deps: {
|
|
805
|
-
auth: backendPluginApi.coreServices.auth,
|
|
806
|
-
httpAuth: backendPluginApi.coreServices.httpAuth,
|
|
807
|
-
userInfo: backendPluginApi.coreServices.userInfo,
|
|
808
|
-
httpRouter: backendPluginApi.coreServices.httpRouter,
|
|
809
|
-
logger: backendPluginApi.coreServices.logger,
|
|
810
|
-
database: backendPluginApi.coreServices.database,
|
|
811
|
-
signals: pluginSignalsNode.signalsServiceRef,
|
|
812
|
-
config: backendPluginApi.coreServices.rootConfig,
|
|
813
|
-
catalog: alpha.catalogServiceRef
|
|
814
|
-
},
|
|
815
|
-
async init({
|
|
816
|
-
auth,
|
|
817
|
-
httpAuth,
|
|
818
|
-
userInfo,
|
|
819
|
-
httpRouter,
|
|
820
|
-
logger,
|
|
821
|
-
database,
|
|
822
|
-
signals,
|
|
823
|
-
config,
|
|
824
|
-
catalog
|
|
825
|
-
}) {
|
|
826
|
-
httpRouter.use(
|
|
827
|
-
await createRouter({
|
|
828
|
-
auth,
|
|
829
|
-
httpAuth,
|
|
830
|
-
userInfo,
|
|
831
|
-
logger,
|
|
832
|
-
config,
|
|
833
|
-
database,
|
|
834
|
-
catalog,
|
|
835
|
-
signals,
|
|
836
|
-
processors: processingExtensions.processors
|
|
837
|
-
})
|
|
838
|
-
);
|
|
839
|
-
httpRouter.addAuthPolicy({
|
|
840
|
-
path: "/health",
|
|
841
|
-
allow: "unauthenticated"
|
|
842
|
-
});
|
|
843
|
-
}
|
|
844
|
-
});
|
|
845
|
-
}
|
|
846
|
-
});
|
|
847
|
-
|
|
848
|
-
exports.default = notificationsPlugin;
|
|
9
|
+
exports.default = plugin.notificationsPlugin;
|
|
849
10
|
//# sourceMappingURL=index.cjs.js.map
|