diff --git a/.changeset/happy-scissors-breathe.md b/.changeset/happy-scissors-breathe.md new file mode 100644 index 0000000000..e066bef771 --- /dev/null +++ b/.changeset/happy-scissors-breathe.md @@ -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. diff --git a/plugins/notifications-backend/src/service/router.ts b/plugins/notifications-backend/src/service/router.ts index ea988d851b..0bce3d4bb3 100644 --- a/plugins/notifications-backend/src/service/router.ts +++ b/plugins/notifications-backend/src/service/router.ts @@ -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, 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, - 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({ @@ -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, 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, 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; diff --git a/plugins/notifications-common/api-report.md b/plugins/notifications-common/api-report.md index bd01aa7f5a..f7fed1a5d0 100644 --- a/plugins/notifications-common/api-report.md +++ b/plugins/notifications-common/api-report.md @@ -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; diff --git a/plugins/notifications-common/src/types.ts b/plugins/notifications-common/src/types.ts index 2a5f803652..30bb00b727 100644 --- a/plugins/notifications-common/src/types.ts +++ b/plugins/notifications-common/src/types.ts @@ -33,7 +33,7 @@ export type NotificationPayload = { /** @public */ export type Notification = { id: string; - user: string; + user: string | null; created: Date; saved?: Date; read?: Date; diff --git a/plugins/notifications-node/api-report.md b/plugins/notifications-node/api-report.md index 46dcf88fe9..ff995e9258 100644 --- a/plugins/notifications-node/api-report.md +++ b/plugins/notifications-node/api-report.md @@ -22,8 +22,18 @@ export class DefaultNotificationService implements NotificationService { // @public (undocumented) export interface NotificationProcessor { - decorate?(notification: Notification_2): Promise; - send?(notification: Notification_2): Promise; + getName(): string; + postProcess?( + notification: Notification_2, + options: NotificationSendOptions, + ): Promise; + preProcess?( + notification: Notification_2, + options: NotificationSendOptions, + ): Promise; + processOptions?( + options: NotificationSendOptions, + ): Promise; } // @public (undocumented) diff --git a/plugins/notifications-node/src/extensions.ts b/plugins/notifications-node/src/extensions.ts index f8ab26462d..0a7aa0b932 100644 --- a/plugins/notifications-node/src/extensions.ts +++ b/plugins/notifications-node/src/extensions.ts @@ -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; + 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; + + /** + * 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; + + /** + * 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; + postProcess?( + notification: Notification, + options: NotificationSendOptions, + ): Promise; } /**