@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/dist/index.cjs.js CHANGED
@@ -2,848 +2,9 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var backendPluginApi = require('@backstage/backend-plugin-api');
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
- const migrationsDir = backendPluginApi.resolvePackagePath(
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