From 9152ba8f364f97105aa1de12a155f48f21c9fb27 Mon Sep 17 00:00:00 2001 From: Frank Ye Date: Wed, 21 May 2025 11:28:08 -0400 Subject: [PATCH 1/5] 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 */ From b345e53dd30d644a573a00614a3a630e473a2dc6 Mon Sep 17 00:00:00 2001 From: Frank Ye Date: Fri, 30 May 2025 14:05:19 -0400 Subject: [PATCH 2/5] rename to metadata Signed-off-by: Frank Ye --- .changeset/wise-mails-agree.md | 2 +- ..._addAttributes.js => 20250521_addMetadata.js} | 8 ++++---- plugins/notifications-backend/report.sql.md | 4 ++-- .../database/DatabaseNotificationsStore.test.ts | 8 ++++---- .../src/database/DatabaseNotificationsStore.ts | 12 ++++++------ .../src/service/router.test.ts | 16 ++++++++-------- plugins/notifications-common/package.json | 1 + plugins/notifications-common/report.api.md | 15 +++++++-------- plugins/notifications-common/src/types.ts | 16 +++++++--------- yarn.lock | 1 + 10 files changed, 41 insertions(+), 42 deletions(-) rename plugins/notifications-backend/migrations/{20250521_addAttributes.js => 20250521_addMetadata.js} (85%) diff --git a/.changeset/wise-mails-agree.md b/.changeset/wise-mails-agree.md index d6e96a3f20..301c941339 100644 --- a/.changeset/wise-mails-agree.md +++ b/.changeset/wise-mails-agree.md @@ -3,4 +3,4 @@ '@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. +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. diff --git a/plugins/notifications-backend/migrations/20250521_addAttributes.js b/plugins/notifications-backend/migrations/20250521_addMetadata.js similarity index 85% rename from plugins/notifications-backend/migrations/20250521_addAttributes.js rename to plugins/notifications-backend/migrations/20250521_addMetadata.js index e7519d2e29..c298ed49e1 100644 --- a/plugins/notifications-backend/migrations/20250521_addAttributes.js +++ b/plugins/notifications-backend/migrations/20250521_addMetadata.js @@ -16,18 +16,18 @@ exports.up = async function up(knex) { await knex.schema.alterTable('notification', table => { - table.text('attributes').nullable(); + table.text('metadata').nullable(); }); await knex.schema.alterTable('broadcast', table => { - table.text('attributes').nullable(); + table.text('metadata').nullable(); }); }; exports.down = async function down(knex) { await knex.schema.alterTable('notification', table => { - table.dropColumn('attributes'); + table.dropColumn('metadata'); }); await knex.schema.alterTable('broadcast', table => { - table.dropColumn('attributes'); + table.dropColumn('metadata'); }); }; diff --git a/plugins/notifications-backend/report.sql.md b/plugins/notifications-backend/report.sql.md index bb9612b357..ab491dc791 100644 --- a/plugins/notifications-backend/report.sql.md +++ b/plugins/notifications-backend/report.sql.md @@ -6,12 +6,12 @@ | 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 | - | | `id` | `uuid` | false | - | - | | `link` | `text` | true | - | - | +| `metadata` | `text` | true | - | - | | `origin` | `character varying` | false | 255 | - | | `scope` | `character varying` | true | 255 | - | | `severity` | `character varying` | false | 8 | - | @@ -41,12 +41,12 @@ | 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 | - | | `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 03c4428289..9d8898f0e8 100644 --- a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts +++ b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts @@ -69,8 +69,8 @@ const testNotification1: Notification = { link: '/catalog', severity: 'critical', icon: 'docs', - attributes: { - attribute1: 'attributeValue', + metadata: { + attribute1: 'value1', }, }, }; @@ -212,8 +212,8 @@ 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', + 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 5f68aa0e39..6a2f3f80b8 100644 --- a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts +++ b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts @@ -53,7 +53,7 @@ const NOTIFICATION_COLUMNS = [ 'user', 'read', 'saved', - 'attributes', + 'metadata', ]; type NotificationRowType = { @@ -165,7 +165,7 @@ export class DatabaseNotificationsStore implements NotificationsStore { severity: row.severity, scope: row.scope, icon: row.icon, - attributes: row.attributes ? JSON.parse(row.attributes) : undefined, + metadata: row.metadata ? JSON.parse(row.metadata) : undefined, }, })); }; @@ -229,8 +229,8 @@ export class DatabaseNotificationsStore implements NotificationsStore { icon: notification.payload?.icon, saved: notification.saved, read: notification.read, - attributes: notification.payload?.attributes - ? JSON.stringify(notification.payload.attributes) + metadata: notification.payload?.metadata + ? JSON.stringify(notification.payload.metadata) : undefined, }; }; @@ -247,8 +247,8 @@ 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) + 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 b745754ba6..b19a78c3d9 100644 --- a/plugins/notifications-backend/src/service/router.test.ts +++ b/plugins/notifications-backend/src/service/router.test.ts @@ -529,8 +529,8 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { title: 'Test broadcast notification', created: new Date(), severity: 'high', - attributes: { - attr1: 'attrValue', + metadata: { + attr1: 'value1', }, }); await client('notification').insert({ @@ -540,8 +540,8 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { title: 'Test notification', created: new Date(), severity: 'normal', - attributes: { - attr1: 'attrValue', + metadata: { + attr1: 'value1', }, }); @@ -561,8 +561,8 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { severity: 'normal', title: 'Test notification', topic: null, - attributes: { - attr1: 'attrValue', + metadata: { + attr1: 'value1', }, }, read: null, @@ -582,8 +582,8 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { severity: 'high', title: 'Test broadcast notification', topic: null, - attributes: { - attr1: 'attrValue', + metadata: { + attr1: 'value1', }, }, read: null, diff --git a/plugins/notifications-common/package.json b/plugins/notifications-common/package.json index 66473d0237..9e1c544ba1 100644 --- a/plugins/notifications-common/package.json +++ b/plugins/notifications-common/package.json @@ -40,6 +40,7 @@ }, "dependencies": { "@backstage/config": "workspace:^", + "@backstage/types": "workspace:^", "@material-ui/icons": "^4.9.1" }, "devDependencies": { diff --git a/plugins/notifications-common/report.api.md b/plugins/notifications-common/report.api.md index f619ab677d..337c41b7b5 100644 --- a/plugins/notifications-common/report.api.md +++ b/plugins/notifications-common/report.api.md @@ -4,6 +4,7 @@ ```ts import { Config } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; // @public (undocumented) export type ChannelSetting = { @@ -31,9 +32,7 @@ export type NewNotificationSignal = { }; // @public (undocumented) -type Notification_2< - T extends Record = Record, -> = { +type Notification_2 = { id: string; user: string | null; created: Date; @@ -41,14 +40,12 @@ type Notification_2< read?: Date; updated?: Date; origin: string; - payload: NotificationPayload; + payload: NotificationPayload; }; export { Notification_2 as Notification }; // @public (undocumented) -export type NotificationPayload< - T extends Record = Record, -> = { +export type NotificationPayload = { title: string; description?: string; link?: string; @@ -56,7 +53,9 @@ export type NotificationPayload< topic?: string; scope?: string; icon?: string; - attributes?: T; + metadata?: { + [KMetadataKey in string]?: JsonValue; + }; }; // @public (undocumented) diff --git a/plugins/notifications-common/src/types.ts b/plugins/notifications-common/src/types.ts index 2583d44dc1..4e49de79de 100644 --- a/plugins/notifications-common/src/types.ts +++ b/plugins/notifications-common/src/types.ts @@ -14,13 +14,13 @@ * limitations under the License. */ +import { JsonValue } from '@backstage/types'; + /** @public */ export type NotificationSeverity = 'critical' | 'high' | 'normal' | 'low'; /** @public */ -export type NotificationPayload< - T extends Record = Record, -> = { +export type NotificationPayload = { /** * Notification title */ @@ -53,15 +53,13 @@ export type NotificationPayload< */ icon?: string; /** - * Optional additional customizable attributes. + * Optional additional customizable metadata. */ - attributes?: T; + metadata?: { [KMetadataKey in string]?: JsonValue }; }; /** @public */ -export type Notification< - T extends Record = Record, -> = { +export type Notification = { /** * Unique identifier for the notification */ @@ -95,7 +93,7 @@ export type Notification< /** * Actual notification payload */ - payload: NotificationPayload; + payload: NotificationPayload; }; /** @public */ diff --git a/yarn.lock b/yarn.lock index a3334417ab..c3cdc943dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5751,6 +5751,7 @@ __metadata: dependencies: "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" + "@backstage/types": "workspace:^" "@material-ui/icons": "npm:^4.9.1" languageName: unknown linkType: soft From 1506df32f1247f1460e8072b827a2308cad7659d Mon Sep 17 00:00:00 2001 From: Frank Ye Date: Wed, 16 Jul 2025 17:21:21 -0400 Subject: [PATCH 3/5] add usage docs Signed-off-by: Frank Ye --- docs/notifications/usage.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/docs/notifications/usage.md b/docs/notifications/usage.md index 358426dc21..614a5f8bfe 100644 --- a/docs/notifications/usage.md +++ b/docs/notifications/usage.md @@ -210,3 +210,39 @@ notificationsApi.getNotification(yourId); // or with connection to signals: notificationsApi.getNotification(lastSignal.notification_id); ``` + +## Metadata Field + +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. + +When sending a notification: + +```ts +notificationService.send({ + recipients: { type: 'entity', entityRef: 'group/default:team-a' }, + payload: { + title: 'Notification', + description: 'Description' + metadata: { + markdown: ` + ### Notification + Description + `, + }, + }, +}); +``` + +In your processor, you can then use the metadata field accordingly: + +```ts +async postProcess(notification: Notification): Promise { + customNotificationSender.send({ + to: getUsers(notification.recipients), + subject: notification.payload.title, + markdownText: notification.payload.metadata?.markdown ?? notification.payload.description, + }); +} +``` From 50319c04bb417458a03d307cf2bd99733611c544 Mon Sep 17 00:00:00 2001 From: Frank Ye Date: Thu, 7 Aug 2025 17:33:21 -0400 Subject: [PATCH 4/5] change metadata field to be processor only and update docs Signed-off-by: Frank Ye --- .changeset/wise-mails-agree.md | 4 +- docs/notifications/usage.md | 37 +++++++++++++++++-- .../migrations/20250521_addMetadata.js | 33 ----------------- plugins/notifications-backend/report.sql.md | 2 - .../DatabaseNotificationsStore.test.ts | 6 --- .../database/DatabaseNotificationsStore.ts | 10 +---- .../src/service/router.test.ts | 18 +++------ 7 files changed, 42 insertions(+), 68 deletions(-) delete mode 100644 plugins/notifications-backend/migrations/20250521_addMetadata.js 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, From 17b34e585f454334f0943d919f07f2c6c94035e7 Mon Sep 17 00:00:00 2001 From: Frank Ye Date: Thu, 14 Aug 2025 16:26:29 -0400 Subject: [PATCH 5/5] update BEP typing for metadata Signed-off-by: Frank Ye --- beps/0001-notifications-system/README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/beps/0001-notifications-system/README.md b/beps/0001-notifications-system/README.md index 03fb224e1c..bfea3115f9 100644 --- a/beps/0001-notifications-system/README.md +++ b/beps/0001-notifications-system/README.md @@ -211,10 +211,7 @@ export type NotificationPayload = { topic?: string; scope?: string; icon?: string; - metadata?: Array<{ - type: string; - value: JsonValue; - }>; + metadata?: { [KMetadataKey in string]?: JsonValue }; }; export type Notification = {