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:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -33,7 +33,7 @@ export type NotificationPayload = {
|
||||
/** @public */
|
||||
export type Notification = {
|
||||
id: string;
|
||||
user: string;
|
||||
user: string | null;
|
||||
created: Date;
|
||||
saved?: Date;
|
||||
read?: Date;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user