From 50c7dbde2f8da23389fe6ac0f37d3e403c848ef9 Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Fri, 26 Jan 2024 14:44:30 +0200 Subject: [PATCH] feat: support for topic in notification using same topic twice will restore existing notification in that topic if any. this can be useful if you want to remind users about some specific thing over time Signed-off-by: Heikki Hellgren --- packages/backend/src/plugins/notifications.ts | 2 + .../migrations/20231215_init.js | 6 +- .../database/DatabaseNotificationsStore.ts | 52 +++++++++++++++++ .../src/database/NotificationsStore.ts | 12 ++++ .../src/service/router.ts | 56 +++++++++++++++---- plugins/notifications-common/api-report.md | 3 + plugins/notifications-common/src/types.ts | 3 + plugins/notifications-node/README.md | 3 + plugins/notifications-node/api-report.md | 1 + .../src/service/DefaultNotificationService.ts | 1 + 10 files changed, 124 insertions(+), 15 deletions(-) diff --git a/packages/backend/src/plugins/notifications.ts b/packages/backend/src/plugins/notifications.ts index 5926c292ff..8bfbb593d8 100644 --- a/packages/backend/src/plugins/notifications.ts +++ b/packages/backend/src/plugins/notifications.ts @@ -21,12 +21,14 @@ import { PluginEnvironment } from '../types'; export default async function createPlugin( env: PluginEnvironment, ): Promise { + // TODO: Remove this test code setInterval(() => { env.notificationService.send({ receivers: { type: 'broadcast' }, title: 'Test', description: 'Test', link: '/catalog', + topic: 'topic', }); }, 60000); diff --git a/plugins/notifications-backend/migrations/20231215_init.js b/plugins/notifications-backend/migrations/20231215_init.js index 08758d06f3..329906ca99 100644 --- a/plugins/notifications-backend/migrations/20231215_init.js +++ b/plugins/notifications-backend/migrations/20231215_init.js @@ -21,9 +21,9 @@ exports.up = async function up(knex) { table.string('title').notNullable(); table.text('description').notNullable(); table.text('link').notNullable(); - table.text('icon').nullable(); - table.text('image').nullable(); - table.datetime('created').notNullable(); + table.text('topic').nullable(); + table.datetime('created').defaultTo(knex.fn.now()).notNullable(); + table.datetime('updated').nullable(); table.datetime('read').nullable(); table.datetime('done').nullable(); table.boolean('saved').defaultTo(false).notNullable(); diff --git a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts index 533a5b8627..f1915d233b 100644 --- a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts +++ b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts @@ -108,6 +108,58 @@ export class DatabaseNotificationsStore implements NotificationsStore { }; } + async getExistingTopicNotification(options: { + user_ref: string; + topic: string; + }) { + const query = this.db('notifications') + .where('userRef', options.user_ref) + .where('topic', options.topic) + .select('*') + .limit(1); + + const rows = await query; + if (!rows || rows.length === 0) { + return null; + } + return rows[0] as Notification; + } + + async restoreExistingNotification(options: { + id: string; + notification: Notification; + }) { + const query = this.db('notifications') + .where('id', options.id) + .where('userRef', options.notification.userRef); + const rows = await query.update({ + title: options.notification.title, + description: options.notification.description, + link: options.notification.link, + topic: options.notification.topic, + updated: options.notification.created, + read: null, + done: null, + }); + + if (!rows) { + return null; + } + + return await this.getNotification(options); + } + + async getNotification(options: { id: string }) { + const rows = await this.db('notifications') + .where('id', options.id) + .select('*') + .limit(1); + if (!rows || rows.length === 0) { + return null; + } + return rows[0] as Notification; + } + async markRead(options: NotificationModifyOptions): Promise { const notificationQuery = this.getNotificationsBaseQuery(options); await notificationQuery.update({ read: new Date() }); diff --git a/plugins/notifications-backend/src/database/NotificationsStore.ts b/plugins/notifications-backend/src/database/NotificationsStore.ts index c63de65dc5..457b08489a 100644 --- a/plugins/notifications-backend/src/database/NotificationsStore.ts +++ b/plugins/notifications-backend/src/database/NotificationsStore.ts @@ -37,6 +37,18 @@ export interface NotificationsStore { saveNotification(notification: Notification): Promise; + getExistingTopicNotification(options: { + user_ref: string; + topic: string; + }): Promise; + + restoreExistingNotification(options: { + id: string; + notification: Notification; + }): Promise; + + getNotification(options: { id: string }): Promise; + getStatus(options: NotificationGetOptions): Promise; markRead(options: NotificationModifyOptions): Promise; diff --git a/plugins/notifications-backend/src/service/router.ts b/plugins/notifications-backend/src/service/router.ts index f5299931e0..5dd7350c33 100644 --- a/plugins/notifications-backend/src/service/router.ts +++ b/plugins/notifications-backend/src/service/router.ts @@ -41,6 +41,7 @@ import { NotificationProcessor } from '../types'; import { AuthenticationError } from '@backstage/errors'; import { DiscoveryService, LoggerService } from '@backstage/backend-plugin-api'; import { SignalService } from '@backstage/plugin-signals-node'; +import { Notification } from '@backstage/plugin-notifications-common'; /** @public */ export interface RouterOptions { @@ -145,6 +146,22 @@ export async function createRouter( return users; }; + const decorateNotification = async (notification: Notification) => { + let ret: Notification = notification; + for (const processor of processors ?? []) { + ret = processor.decorate ? await processor.decorate(ret) : ret; + } + return ret; + }; + + const processorSendNotification = async (notification: Notification) => { + for (const processor of processors ?? []) { + if (processor.send) { + processor.send(notification); + } + } + }; + const router = Router(); router.use(express.json()); @@ -269,7 +286,7 @@ export async function createRouter( }); router.post('/notifications', async (req, res) => { - const { receivers, title, description, link } = req.body; + const { receivers, title, description, link, topic } = req.body; const notifications = []; let users = []; @@ -298,25 +315,40 @@ export async function createRouter( title, description, link, + topic, created: new Date(), saved: false, }; for (const user of users) { - let notification = { ...baseNotification, id: uuid(), userRef: user }; - for (const processor of processors ?? []) { - notification = processor.decorate - ? await processor.decorate(notification) - : notification; + const userNotification = { + ...baseNotification, + id: uuid(), + userRef: user, + }; + const notification = await decorateNotification(userNotification); + + let existingNotification; + if (topic) { + existingNotification = await store.getExistingTopicNotification({ + user_ref: user, + topic, + }); } - await store.saveNotification(notification); - for (const processor of processors ?? []) { - if (processor.send) { - processor.send(notification); - } + let ret = notification; + if (existingNotification) { + const restored = await store.restoreExistingNotification({ + id: existingNotification.id, + notification, + }); + ret = restored ?? notification; + } else { + await store.saveNotification(notification); } - notifications.push(notification); + + processorSendNotification(ret); + notifications.push(ret); } if (signalService) { diff --git a/plugins/notifications-common/api-report.md b/plugins/notifications-common/api-report.md index 4e34ae2afc..0770c94fae 100644 --- a/plugins/notifications-common/api-report.md +++ b/plugins/notifications-common/api-report.md @@ -10,8 +10,11 @@ type Notification_2 = { title: string; description: string; link: string; + topic?: string; created: Date; + updated?: Date; read?: Date; + done?: Date; saved: boolean; }; export { Notification_2 as Notification }; diff --git a/plugins/notifications-common/src/types.ts b/plugins/notifications-common/src/types.ts index c19edb9b24..43de9c725f 100644 --- a/plugins/notifications-common/src/types.ts +++ b/plugins/notifications-common/src/types.ts @@ -24,8 +24,11 @@ export type Notification = { title: string; description: string; link: string; + topic?: string; created: Date; + updated?: Date; read?: Date; + done?: Date; saved: boolean; }; diff --git a/plugins/notifications-node/README.md b/plugins/notifications-node/README.md index 31fe22baf9..c3d8e93fe7 100644 --- a/plugins/notifications-node/README.md +++ b/plugins/notifications-node/README.md @@ -53,3 +53,6 @@ save the notification and optionally signal the frontend to show the latest stat When sending notifications, you can specify the entity reference of the notification. If the entity reference is a user, the notification will be sent to only that user. If it's a group, the notification will be sent to all members of the group. If it's some other entity, the notification will be sent to the owner of that entity. + +If the notification has `topic` set and user already has notification with that topic, the existing notification +will be updated with the new notification values and moved to inbox as unread. diff --git a/plugins/notifications-node/api-report.md b/plugins/notifications-node/api-report.md index 806b0400ac..58e0df5603 100644 --- a/plugins/notifications-node/api-report.md +++ b/plugins/notifications-node/api-report.md @@ -38,6 +38,7 @@ export type NotificationSendOptions = { title: string; description: string; link: string; + topic?: string; }; // @public (undocumented) diff --git a/plugins/notifications-node/src/service/DefaultNotificationService.ts b/plugins/notifications-node/src/service/DefaultNotificationService.ts index 16de07912a..bffac9e725 100644 --- a/plugins/notifications-node/src/service/DefaultNotificationService.ts +++ b/plugins/notifications-node/src/service/DefaultNotificationService.ts @@ -38,6 +38,7 @@ export type NotificationSendOptions = { title: string; description: string; link: string; + topic?: string; }; /** @public */