diff --git a/.changeset/wise-mails-agree.md b/.changeset/wise-mails-agree.md index 301c941339..1ad08c2ef6 100644 --- a/.changeset/wise-mails-agree.md +++ b/.changeset/wise-mails-agree.md @@ -1,6 +1,6 @@ --- -'@backstage/plugin-notifications-backend': minor +'@backstage/plugin-notifications-backend': patch '@backstage/plugin-notifications-common': minor --- -Add an optional generic object `metadata` field to `NotificationPayload`. Metadata can be used to store additional unstructured data for the notification, and are stored in the database. +Add an optional generic object `metadata` field to `NotificationPayload`. Metadata can be used to store additional unstructured data for the notification and are available to use by processors. diff --git a/docs/notifications/usage.md b/docs/notifications/usage.md index 614a5f8bfe..94de585d21 100644 --- a/docs/notifications/usage.md +++ b/docs/notifications/usage.md @@ -215,7 +215,32 @@ notificationsApi.getNotification(lastSignal.notification_id); The metadata field is a freeform object that is designed to be used by processors. -For example, for a custom processor that supports markdown format the metadata field can be used to pass an optional `markdown` field. +### Well-known Notification Metadata Fields + +Below are metadata fields that will be commonly used between processors and have defined schematics. + +#### backstage.io/body.markdown + +```ts +# Example: +const payload = { + title: 'Entities Require Attention', + description: 'Entities: Service A, Service B' + metadata: { + 'backstage.io/body.markdown': ` + # Entities + - Service A + - System B + ` + } +} +``` + +This value of this metadata field should be the notification message in markdown format. This allows additional formatting options for processors that support markdown. + +### Usage + +Below is an example of using the `backstage.io/body.markdown` metadata field in a custom processor. When sending a notification: @@ -226,7 +251,7 @@ notificationService.send({ title: 'Notification', description: 'Description' metadata: { - markdown: ` + 'backstage.io/body.markdown': ` ### Notification Description `, @@ -235,14 +260,18 @@ notificationService.send({ }); ``` -In your processor, you can then use the metadata field accordingly: +In the processor, you can then use the metadata field accordingly: ```ts async postProcess(notification: Notification): Promise { + // We suggest you parse the metadata field with a schema, i.e. Zod + const parseResult = CustomProcessorMetadataSchema.safeParse(notification.payload.metadata ?? {}); + const metadata = parseResult.success ? parseResult.data : {}; + customNotificationSender.send({ to: getUsers(notification.recipients), subject: notification.payload.title, - markdownText: notification.payload.metadata?.markdown ?? notification.payload.description, + markdownText: metadata['backstage.io/body.markdown'] ?? notification.payload.description, }); } ``` diff --git a/plugins/notifications-backend/migrations/20250521_addMetadata.js b/plugins/notifications-backend/migrations/20250521_addMetadata.js deleted file mode 100644 index c298ed49e1..0000000000 --- a/plugins/notifications-backend/migrations/20250521_addMetadata.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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('metadata').nullable(); - }); - await knex.schema.alterTable('broadcast', table => { - table.text('metadata').nullable(); - }); -}; - -exports.down = async function down(knex) { - await knex.schema.alterTable('notification', table => { - table.dropColumn('metadata'); - }); - await knex.schema.alterTable('broadcast', table => { - table.dropColumn('metadata'); - }); -}; diff --git a/plugins/notifications-backend/report.sql.md b/plugins/notifications-backend/report.sql.md index ab491dc791..aacb787a61 100644 --- a/plugins/notifications-backend/report.sql.md +++ b/plugins/notifications-backend/report.sql.md @@ -11,7 +11,6 @@ | `icon` | `character varying` | true | 255 | - | | `id` | `uuid` | false | - | - | | `link` | `text` | true | - | - | -| `metadata` | `text` | true | - | - | | `origin` | `character varying` | false | 255 | - | | `scope` | `character varying` | true | 255 | - | | `severity` | `character varying` | false | 8 | - | @@ -46,7 +45,6 @@ | `icon` | `character varying` | true | 255 | - | | `id` | `uuid` | false | - | - | | `link` | `text` | true | - | - | -| `metadata` | `text` | true | - | - | | `origin` | `character varying` | false | 255 | - | | `read` | `timestamp with time zone` | true | - | - | | `saved` | `timestamp with time zone` | true | - | - | diff --git a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts index 9d8898f0e8..d8ae3faf57 100644 --- a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts +++ b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts @@ -69,9 +69,6 @@ const testNotification1: Notification = { link: '/catalog', severity: 'critical', icon: 'docs', - metadata: { - attribute1: 'value1', - }, }, }; const testNotification2: Notification = { @@ -212,9 +209,6 @@ 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?.metadata).toEqual({ - attribute1: 'value1', - }); }); }); diff --git a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts index 6a2f3f80b8..94131ca257 100644 --- a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts +++ b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts @@ -53,7 +53,6 @@ const NOTIFICATION_COLUMNS = [ 'user', 'read', 'saved', - 'metadata', ]; type NotificationRowType = { @@ -165,7 +164,6 @@ export class DatabaseNotificationsStore implements NotificationsStore { severity: row.severity, scope: row.scope, icon: row.icon, - metadata: row.metadata ? JSON.parse(row.metadata) : undefined, }, })); }; @@ -226,12 +224,9 @@ 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, - metadata: notification.payload?.metadata - ? JSON.stringify(notification.payload.metadata) - : undefined, }; }; @@ -247,9 +242,6 @@ export class DatabaseNotificationsStore implements NotificationsStore { severity: normalizeSeverity(notification.payload?.severity), icon: notification.payload.icon, scope: notification.payload?.scope, - metadata: notification.payload?.metadata - ? JSON.stringify(notification.payload.metadata) - : undefined, }; }; diff --git a/plugins/notifications-backend/src/service/router.test.ts b/plugins/notifications-backend/src/service/router.test.ts index b19a78c3d9..980762edbc 100644 --- a/plugins/notifications-backend/src/service/router.test.ts +++ b/plugins/notifications-backend/src/service/router.test.ts @@ -241,6 +241,9 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { }, payload: { title: 'test notification', + metadata: { + attr: 1, + }, }, }); @@ -253,6 +256,9 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { payload: { severity: 'normal', title: 'test notification', + metadata: { + attr: 1, + }, }, user: 'user:default/mock', }, @@ -529,9 +535,6 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { title: 'Test broadcast notification', created: new Date(), severity: 'high', - metadata: { - attr1: 'value1', - }, }); await client('notification').insert({ id: uuid(), @@ -540,9 +543,6 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { title: 'Test notification', created: new Date(), severity: 'normal', - metadata: { - attr1: 'value1', - }, }); const response = await request(app).get('/'); @@ -561,9 +561,6 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { severity: 'normal', title: 'Test notification', topic: null, - metadata: { - attr1: 'value1', - }, }, read: null, saved: null, @@ -582,9 +579,6 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { severity: 'high', title: 'Test broadcast notification', topic: null, - metadata: { - attr1: 'value1', - }, }, read: null, saved: null,