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 */