feat: notification processor improvements

Notification processor functions are now renamed to `preProcess` and `postProcess`.
Additionally, processor name is now required to be returned by `getName`.
A new processor functionality `processOptions` was added to process options before sending the notification.

Signed-off-by: Heikki Hellgren <heikki.hellgren@op.fi>
This commit is contained in:
Heikki Hellgren
2024-04-08 08:15:13 +03:00
parent 0d60f65572
commit 0d99528300
6 changed files with 178 additions and 75 deletions
+9
View File
@@ -0,0 +1,9 @@
---
'@backstage/plugin-notifications-backend': patch
'@backstage/plugin-notifications-common': patch
'@backstage/plugin-notifications-node': patch
---
Notification processor functions are now renamed to `preProcess` and `postProcess`.
Additionally, processor name is now required to be returned by `getName`.
A new processor functionality `processOptions` was added to process options before sending the notification.
@@ -31,7 +31,10 @@ import {
RELATION_PARENT_OF,
stringifyEntityRef,
} from '@backstage/catalog-model';
import { NotificationProcessor } from '@backstage/plugin-notifications-node';
import {
NotificationProcessor,
NotificationSendOptions,
} from '@backstage/plugin-notifications-node';
import { InputError } from '@backstage/errors';
import {
AuthService,
@@ -45,6 +48,7 @@ import {
NewNotificationSignal,
Notification,
NotificationReadSignal,
NotificationStatus,
} from '@backstage/plugin-notifications-common';
import { parseEntityOrderFieldParams } from './parseEntityOrderFieldParams';
@@ -73,7 +77,7 @@ export async function createRouter(
userInfo,
discovery,
catalog,
processors,
processors = [],
signals,
} = options;
@@ -173,18 +177,54 @@ export async function createRouter(
return users;
};
const decorateNotification = async (notification: Notification) => {
let ret = notification;
for (const processor of processors ?? []) {
ret = processor.decorate ? await processor.decorate(ret) : ret;
const processOptions = async (opts: NotificationSendOptions) => {
let ret = opts;
for (const processor of processors) {
try {
ret = processor.processOptions
? await processor.processOptions(ret)
: ret;
} catch (e) {
logger.error(
`Error while processing notification options with ${processor.getName()}: ${e}`,
);
}
}
return ret;
};
const processorSendNotification = async (notification: Notification) => {
for (const processor of processors ?? []) {
if (processor.send) {
processor.send(notification);
const preProcessNotification = async (
notification: Notification,
opts: NotificationSendOptions,
) => {
let ret = notification;
for (const processor of processors) {
try {
ret = processor.preProcess
? await processor.preProcess(ret, opts)
: ret;
} catch (e) {
logger.error(
`Error while pre processing notification with ${processor.getName()}: ${e}`,
);
}
}
return ret;
};
const postProcessNotification = async (
notification: Notification,
opts: NotificationSendOptions,
) => {
for (const processor of processors) {
if (processor.postProcess) {
try {
await processor.postProcess(notification, opts);
} catch (e) {
logger.error(
`Error while post processing notification with ${processor.getName()}: ${e}`,
);
}
}
}
};
@@ -250,7 +290,7 @@ export async function createRouter(
});
});
router.get('/status', async (req, res) => {
router.get('/status', async (req: Request<any, NotificationStatus>, res) => {
const user = await getUser(req);
const status = await store.getStatus({ user });
res.send(status);
@@ -313,17 +353,19 @@ export async function createRouter(
const sendBroadcastNotification = async (
baseNotification: Omit<Notification, 'user' | 'id'>,
opts: { scope?: string; origin: string },
opts: NotificationSendOptions,
origin: string,
) => {
const { scope, origin } = opts;
const { scope } = opts.payload;
const broadcastNotification = {
...baseNotification,
user: null,
id: uuid(),
};
const notification = await decorateNotification({
...broadcastNotification,
user: '',
});
const notification = await preProcessNotification(
broadcastNotification,
opts,
);
let existingNotification;
if (scope) {
existingNotification = await store.getExistingScopeBroadcast({
@@ -342,7 +384,6 @@ export async function createRouter(
} else {
await store.saveBroadcast(notification);
}
processorSendNotification(ret);
if (signals) {
await signals.publish<NewNotificationSignal>({
@@ -353,6 +394,7 @@ export async function createRouter(
},
channel: 'notifications',
});
postProcessNotification(ret, opts);
}
return notification;
};
@@ -360,10 +402,11 @@ export async function createRouter(
const sendUserNotifications = async (
baseNotification: Omit<Notification, 'user' | 'id'>,
users: string[],
opts: { scope?: string; origin: string },
opts: NotificationSendOptions,
origin: string,
) => {
const notifications = [];
const { scope, origin } = opts;
const { scope } = opts.payload;
const uniqueUsers = [...new Set(users)];
for (const user of uniqueUsers) {
const userNotification = {
@@ -371,7 +414,7 @@ export async function createRouter(
id: uuid(),
user,
};
const notification = await decorateNotification(userNotification);
const notification = await preProcessNotification(userNotification, opts);
let existingNotification;
if (scope) {
@@ -393,7 +436,6 @@ export async function createRouter(
await store.saveNotification(notification);
}
processorSendNotification(ret);
notifications.push(ret);
if (signals) {
@@ -406,59 +448,68 @@ export async function createRouter(
channel: 'notifications',
});
}
postProcessNotification(ret, opts);
}
return notifications;
};
// Add new notification
router.post('/', async (req, res) => {
const { recipients, payload } = req.body;
const notifications = [];
let users = [];
router.post(
'/',
async (req: Request<any, Notification[], NotificationSendOptions>, res) => {
const opts = await processOptions(req.body);
const { recipients, payload } = opts;
const notifications: Notification[] = [];
let users = [];
const credentials = await httpAuth.credentials(req, { allow: ['service'] });
const { title, scope } = payload;
if (!recipients || !title) {
logger.error(`Invalid notification request received`);
throw new InputError();
}
const origin = credentials.principal.subject;
const baseNotification = {
payload: {
...payload,
severity: payload.severity ?? 'normal',
},
origin,
created: new Date(),
};
if (recipients.type === 'broadcast') {
const broadcast = await sendBroadcastNotification(baseNotification, {
scope,
origin,
const credentials = await httpAuth.credentials(req, {
allow: ['service'],
});
notifications.push(broadcast);
} else {
const entityRef = recipients.entityRef;
try {
users = await getUsersForEntityRef(entityRef);
} catch (e) {
const { title } = payload;
if (!recipients || !title) {
logger.error(`Invalid notification request received`);
throw new InputError();
}
const userNotifications = await sendUserNotifications(
baseNotification,
users,
{ scope, origin },
);
notifications.push(...userNotifications);
}
res.json(notifications);
});
const origin = credentials.principal.subject;
const baseNotification = {
payload: {
...payload,
severity: payload.severity ?? 'normal',
},
origin,
created: new Date(),
};
if (recipients.type === 'broadcast') {
const broadcast = await sendBroadcastNotification(
baseNotification,
opts,
origin,
);
notifications.push(broadcast);
} else {
const entityRef = recipients.entityRef;
try {
users = await getUsersForEntityRef(entityRef);
} catch (e) {
throw new InputError();
}
const userNotifications = await sendUserNotifications(
baseNotification,
users,
opts,
origin,
);
notifications.push(...userNotifications);
}
res.json(notifications);
},
);
router.use(errorHandler());
return router;
+1 -1
View File
@@ -12,7 +12,7 @@ export type NewNotificationSignal = {
// @public (undocumented)
type Notification_2 = {
id: string;
user: string;
user: string | null;
created: Date;
saved?: Date;
read?: Date;
+1 -1
View File
@@ -33,7 +33,7 @@ export type NotificationPayload = {
/** @public */
export type Notification = {
id: string;
user: string;
user: string | null;
created: Date;
saved?: Date;
read?: Date;
+12 -2
View File
@@ -22,8 +22,18 @@ export class DefaultNotificationService implements NotificationService {
// @public (undocumented)
export interface NotificationProcessor {
decorate?(notification: Notification_2): Promise<Notification_2>;
send?(notification: Notification_2): Promise<void>;
getName(): string;
postProcess?(
notification: Notification_2,
options: NotificationSendOptions,
): Promise<void>;
preProcess?(
notification: Notification_2,
options: NotificationSendOptions,
): Promise<Notification_2>;
processOptions?(
options: NotificationSendOptions,
): Promise<NotificationSendOptions>;
}
// @public (undocumented)
+40 -7
View File
@@ -15,25 +15,58 @@
*/
import { createExtensionPoint } from '@backstage/backend-plugin-api';
import { Notification } from '@backstage/plugin-notifications-common';
import { NotificationSendOptions } from './service';
/**
* @public
*/
export interface NotificationProcessor {
/**
* Decorate notification before sending it
*
* @param notification - The notification to decorate
* @returns The same notification or a modified version of it
* Human-readable name of this processor like Email, Slack, etc.
*/
decorate?(notification: Notification): Promise<Notification>;
getName(): string;
/**
* Send notification using this processor.
* Process the notification options.
*
* This can be used to modify the options before sending the notification or even sending the notification to
* external services. This function is called only once for each notification before processing it.
*
* @param options - The original options to send the notification
*/
processOptions?(
options: NotificationSendOptions,
): Promise<NotificationSendOptions>;
/**
* Pre-process notification before sending it to Backstage UI.
*
* Can be used to send the notification to external services or to decorate the notification with additional
* information. This function is called for each notification recipient individually or once for broadcast
* notification.
*
* @param notification - The notification to decorate
* @param options - The options to send the notification
* @returns The same notification or a modified version of it
*/
preProcess?(
notification: Notification,
options: NotificationSendOptions,
): Promise<Notification>;
/**
* Post process notification after sending it to Backstage UI.
*
* Can be used to send the notification to external services. This function is called for each notification
* recipient individually or once for broadcast notification.
*
* @param notification - The notification to send
* @param options - The options to send the notification
*/
send?(notification: Notification): Promise<void>;
postProcess?(
notification: Notification,
options: NotificationSendOptions,
): Promise<void>;
}
/**