From 9152ba8f364f97105aa1de12a155f48f21c9fb27 Mon Sep 17 00:00:00 2001 From: Frank Ye Date: Wed, 21 May 2025 11:28:08 -0400 Subject: [PATCH] add generic attribute field to NotificationPayload Signed-off-by: Frank Ye --- .changeset/wise-mails-agree.md | 6 ++++ .../migrations/20250521_addAttributes.js | 33 +++++++++++++++++++ plugins/notifications-backend/report.sql.md | 2 ++ .../DatabaseNotificationsStore.test.ts | 6 ++++ .../database/DatabaseNotificationsStore.ts | 10 +++++- .../src/service/router.test.ts | 12 +++++++ plugins/notifications-common/report.api.md | 11 +++++-- plugins/notifications-common/src/types.ts | 14 ++++++-- 8 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 .changeset/wise-mails-agree.md create mode 100644 plugins/notifications-backend/migrations/20250521_addAttributes.js diff --git a/.changeset/wise-mails-agree.md b/.changeset/wise-mails-agree.md new file mode 100644 index 0000000000..d6e96a3f20 --- /dev/null +++ b/.changeset/wise-mails-agree.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-notifications-backend': minor +'@backstage/plugin-notifications-common': minor +--- + +Add an optional generic object `attributes` field to `NotificationPayload`. Attributes can be used to store additional unstructured data for the notification, and are stored in the database. diff --git a/plugins/notifications-backend/migrations/20250521_addAttributes.js b/plugins/notifications-backend/migrations/20250521_addAttributes.js new file mode 100644 index 0000000000..e7519d2e29 --- /dev/null +++ b/plugins/notifications-backend/migrations/20250521_addAttributes.js @@ -0,0 +1,33 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +exports.up = async function up(knex) { + await knex.schema.alterTable('notification', table => { + table.text('attributes').nullable(); + }); + await knex.schema.alterTable('broadcast', table => { + table.text('attributes').nullable(); + }); +}; + +exports.down = async function down(knex) { + await knex.schema.alterTable('notification', table => { + table.dropColumn('attributes'); + }); + await knex.schema.alterTable('broadcast', table => { + table.dropColumn('attributes'); + }); +}; diff --git a/plugins/notifications-backend/report.sql.md b/plugins/notifications-backend/report.sql.md index aacb787a61..bb9612b357 100644 --- a/plugins/notifications-backend/report.sql.md +++ b/plugins/notifications-backend/report.sql.md @@ -6,6 +6,7 @@ | Column | Type | Nullable | Max Length | Default | | ------------- | -------------------------- | -------- | ---------- | ------------------- | +| `attributes` | `text` | true | - | - | | `created` | `timestamp with time zone` | false | - | `CURRENT_TIMESTAMP` | | `description` | `text` | true | - | - | | `icon` | `character varying` | true | 255 | - | @@ -40,6 +41,7 @@ | Column | Type | Nullable | Max Length | Default | | ------------- | -------------------------- | -------- | ---------- | ------------------- | +| `attributes` | `text` | true | - | - | | `created` | `timestamp with time zone` | false | - | `CURRENT_TIMESTAMP` | | `description` | `text` | true | - | - | | `icon` | `character varying` | true | 255 | - | diff --git a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts index d8ae3faf57..03c4428289 100644 --- a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts +++ b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts @@ -69,6 +69,9 @@ const testNotification1: Notification = { link: '/catalog', severity: 'critical', icon: 'docs', + attributes: { + attribute1: 'attributeValue', + }, }, }; const testNotification2: Notification = { @@ -209,6 +212,9 @@ describe.each(databases.eachSupportedId())( expect(notification?.payload?.link).toBe('/catalog'); expect(notification?.payload?.severity).toBe('critical'); expect(notification?.payload?.icon).toBe('docs'); + expect(notification?.payload?.attributes).toEqual({ + attribute1: 'attributeValue', + }); }); }); diff --git a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts index 94131ca257..5f68aa0e39 100644 --- a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts +++ b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts @@ -53,6 +53,7 @@ const NOTIFICATION_COLUMNS = [ 'user', 'read', 'saved', + 'attributes', ]; type NotificationRowType = { @@ -164,6 +165,7 @@ export class DatabaseNotificationsStore implements NotificationsStore { severity: row.severity, scope: row.scope, icon: row.icon, + attributes: row.attributes ? JSON.parse(row.attributes) : undefined, }, })); }; @@ -224,9 +226,12 @@ export class DatabaseNotificationsStore implements NotificationsStore { description: notification.payload?.description, severity: normalizeSeverity(notification.payload?.severity), scope: notification.payload?.scope, - icon: notification.payload.icon, + icon: notification.payload?.icon, saved: notification.saved, read: notification.read, + attributes: notification.payload?.attributes + ? JSON.stringify(notification.payload.attributes) + : undefined, }; }; @@ -242,6 +247,9 @@ export class DatabaseNotificationsStore implements NotificationsStore { severity: normalizeSeverity(notification.payload?.severity), icon: notification.payload.icon, scope: notification.payload?.scope, + attributes: notification.payload?.attributes + ? JSON.stringify(notification.payload.attributes) + : undefined, }; }; diff --git a/plugins/notifications-backend/src/service/router.test.ts b/plugins/notifications-backend/src/service/router.test.ts index 6c74fed41c..b745754ba6 100644 --- a/plugins/notifications-backend/src/service/router.test.ts +++ b/plugins/notifications-backend/src/service/router.test.ts @@ -529,6 +529,9 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { title: 'Test broadcast notification', created: new Date(), severity: 'high', + attributes: { + attr1: 'attrValue', + }, }); await client('notification').insert({ id: uuid(), @@ -537,6 +540,9 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { title: 'Test notification', created: new Date(), severity: 'normal', + attributes: { + attr1: 'attrValue', + }, }); const response = await request(app).get('/'); @@ -555,6 +561,9 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { severity: 'normal', title: 'Test notification', topic: null, + attributes: { + attr1: 'attrValue', + }, }, read: null, saved: null, @@ -573,6 +582,9 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { severity: 'high', title: 'Test broadcast notification', topic: null, + attributes: { + attr1: 'attrValue', + }, }, read: null, saved: null, diff --git a/plugins/notifications-common/report.api.md b/plugins/notifications-common/report.api.md index 93889b206b..f619ab677d 100644 --- a/plugins/notifications-common/report.api.md +++ b/plugins/notifications-common/report.api.md @@ -31,7 +31,9 @@ export type NewNotificationSignal = { }; // @public (undocumented) -type Notification_2 = { +type Notification_2< + T extends Record = Record, +> = { id: string; user: string | null; created: Date; @@ -39,12 +41,14 @@ type Notification_2 = { read?: Date; updated?: Date; origin: string; - payload: NotificationPayload; + payload: NotificationPayload; }; export { Notification_2 as Notification }; // @public (undocumented) -export type NotificationPayload = { +export type NotificationPayload< + T extends Record = Record, +> = { title: string; description?: string; link?: string; @@ -52,6 +56,7 @@ export type NotificationPayload = { topic?: string; scope?: string; icon?: string; + attributes?: T; }; // @public (undocumented) diff --git a/plugins/notifications-common/src/types.ts b/plugins/notifications-common/src/types.ts index 48f89cbbb4..2583d44dc1 100644 --- a/plugins/notifications-common/src/types.ts +++ b/plugins/notifications-common/src/types.ts @@ -18,7 +18,9 @@ export type NotificationSeverity = 'critical' | 'high' | 'normal' | 'low'; /** @public */ -export type NotificationPayload = { +export type NotificationPayload< + T extends Record = Record, +> = { /** * Notification title */ @@ -50,10 +52,16 @@ export type NotificationPayload = { * Optional notification icon */ icon?: string; + /** + * Optional additional customizable attributes. + */ + attributes?: T; }; /** @public */ -export type Notification = { +export type Notification< + T extends Record = Record, +> = { /** * Unique identifier for the notification */ @@ -87,7 +95,7 @@ export type Notification = { /** * Actual notification payload */ - payload: NotificationPayload; + payload: NotificationPayload; }; /** @public */