@rwdocs/backstage-plugin-rw-backend-module-notifications 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -18,7 +18,13 @@ class CommentNotifier {
|
|
|
18
18
|
}
|
|
19
19
|
try {
|
|
20
20
|
await this.notifications.send({
|
|
21
|
-
|
|
21
|
+
// Single point of actor exclusion (see class doc): the resolver drops the actor from
|
|
22
|
+
// the resolved recipients, including from an expanded group.
|
|
23
|
+
recipients: {
|
|
24
|
+
type: "entity",
|
|
25
|
+
entityRef: payload.recipients,
|
|
26
|
+
excludeEntityRef: payload.actorRef
|
|
27
|
+
},
|
|
22
28
|
payload: {
|
|
23
29
|
title: this.title(payload),
|
|
24
30
|
description: this.description(payload),
|
|
@@ -42,9 +48,11 @@ class CommentNotifier {
|
|
|
42
48
|
}
|
|
43
49
|
/** Defensive shape guard at the subscriber boundary: the module casts the raw event
|
|
44
50
|
* payload to `CommentEventPayload`, so a malformed or foreign event on the topic would
|
|
45
|
-
* otherwise flow straight into `send`. Validates the fields this notifier relies on
|
|
51
|
+
* otherwise flow straight into `send`. Validates the fields this notifier relies on —
|
|
52
|
+
* including `actorRef`, the sole basis for actor exclusion (passed as `excludeEntityRef`);
|
|
53
|
+
* without it the exclusion silently becomes a no-op and the actor self-notifies. */
|
|
46
54
|
isValid(payload) {
|
|
47
|
-
return !!payload && (payload.kind === "created" || payload.kind === "resolved") && typeof payload.rootId === "string" && Array.isArray(payload.recipients) && payload.recipients.length > 0;
|
|
55
|
+
return !!payload && (payload.kind === "created" || payload.kind === "resolved") && typeof payload.rootId === "string" && typeof payload.actorRef === "string" && payload.actorRef.length > 0 && Array.isArray(payload.recipients) && payload.recipients.length > 0;
|
|
48
56
|
}
|
|
49
57
|
subject(payload) {
|
|
50
58
|
const p = payload.pageTitle?.trim();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CommentNotifier.cjs.js","sources":["../src/CommentNotifier.ts"],"sourcesContent":["import { LoggerService } from \"@backstage/backend-plugin-api\";\nimport { parseEntityRef } from \"@backstage/catalog-model\";\nimport { NotificationService } from \"@backstage/plugin-notifications-node\";\nimport { CommentEventPayload } from \"@rwdocs/backstage-plugin-rw-common\";\n\n/** Subscriber-side: turns a self-contained `rw.comments` event into a native notification.\n *
|
|
1
|
+
{"version":3,"file":"CommentNotifier.cjs.js","sources":["../src/CommentNotifier.ts"],"sourcesContent":["import { LoggerService } from \"@backstage/backend-plugin-api\";\nimport { parseEntityRef } from \"@backstage/catalog-model\";\nimport { NotificationService } from \"@backstage/plugin-notifications-node\";\nimport { CommentEventPayload } from \"@rwdocs/backstage-plugin-rw-common\";\n\n/** Subscriber-side: turns a self-contained `rw.comments` event into a native notification.\n * Presentation + delivery — no DB. The publisher emits raw owner/participant recipients (which may\n * include the actor); this is the single point of actor exclusion: the actor is forwarded as\n * `excludeEntityRef` so Backstage's resolver drops them after expanding groups. The one catalog-path\n * convention (`/catalog/<ns>/<kind>/<name>`) is isolated here: the backend can't resolve the\n * frontend's catalog routeRef, so the app-relative link is composed from the entity ref by\n * convention. Best-effort: catches + logs, always resolves. */\nexport class CommentNotifier {\n private readonly notifications: NotificationService;\n private readonly logger: LoggerService;\n\n constructor(deps: { notifications: NotificationService; logger: LoggerService }) {\n this.notifications = deps.notifications;\n this.logger = deps.logger;\n }\n\n async handle(payload: CommentEventPayload): Promise<void> {\n if (!this.isValid(payload)) {\n this.logger.warn(\n `rw.comments: dropping malformed event payload (comment ${payload?.commentId ?? \"?\"})`,\n );\n return;\n }\n try {\n await this.notifications.send({\n // Single point of actor exclusion (see class doc): the resolver drops the actor from\n // the resolved recipients, including from an expanded group.\n recipients: {\n type: \"entity\",\n entityRef: payload.recipients,\n excludeEntityRef: payload.actorRef,\n },\n payload: {\n title: this.title(payload),\n description: this.description(payload),\n link: this.link(payload),\n severity: \"normal\",\n topic: this.topic(payload),\n // Per-thread collapse: events on one thread share this scope, and the\n // backend dedups on (user, scope, origin) — not topic — overwriting the\n // prior row's topic/title in place. So a recipient who is both the owner\n // and a resolve participant on one thread sees the latest event, not two\n // notifications. A disabled topic short-circuits the send before that\n // overwrite, so a recipient who muted the newer event keeps the prior row.\n scope: `rw:comment:${payload.rootId}`,\n },\n });\n } catch (error) {\n this.logger.warn(\n `rw.comments notification send failed (comment ${payload.commentId}): ${error}`,\n );\n }\n }\n\n /** Defensive shape guard at the subscriber boundary: the module casts the raw event\n * payload to `CommentEventPayload`, so a malformed or foreign event on the topic would\n * otherwise flow straight into `send`. Validates the fields this notifier relies on —\n * including `actorRef`, the sole basis for actor exclusion (passed as `excludeEntityRef`);\n * without it the exclusion silently becomes a no-op and the actor self-notifies. */\n private isValid(payload: CommentEventPayload): boolean {\n return (\n !!payload &&\n (payload.kind === \"created\" || payload.kind === \"resolved\") &&\n typeof payload.rootId === \"string\" &&\n typeof payload.actorRef === \"string\" &&\n payload.actorRef.length > 0 &&\n Array.isArray(payload.recipients) &&\n payload.recipients.length > 0\n );\n }\n\n private subject(payload: CommentEventPayload): string {\n const p = payload.pageTitle?.trim();\n const a = payload.sectionTitle?.trim();\n if (p && a && p !== a) return `${p} · ${a}`;\n return p || a || \"the docs\";\n }\n\n private actor(payload: CommentEventPayload): string {\n return payload.actorName?.trim() || \"Someone\";\n }\n\n private title(payload: CommentEventPayload): string {\n const s = this.subject(payload);\n const a = this.actor(payload);\n if (payload.kind === \"resolved\") return `${a} resolved a thread on ${s}`;\n if (payload.audience === \"owner\") return `${a} commented on ${s}`;\n return `${a} replied on ${s}`;\n }\n\n /** Stable, frozen notification topic id per event kind (see README \"Notification\n * topics\"). Colon-delimited <domain>:<object>:<verb>; lowercase and never renamed\n * (persisted in the settings key hash). Mirrors the title() branch. */\n private topic(payload: CommentEventPayload): string {\n if (payload.kind === \"resolved\") return \"comment:thread:resolved\";\n if (payload.audience === \"owner\") return \"comment:thread:created\";\n return \"comment:reply:created\";\n }\n\n private description(payload: CommentEventPayload): string {\n if (payload.kind === \"resolved\") return `Re: ${payload.bodySnippet}`;\n return payload.bodySnippet;\n }\n\n /** App-relative deep link, or undefined when the owning entity is unknown (degraded). */\n private link(payload: CommentEventPayload): string | undefined {\n if (!payload.entityRef) return undefined;\n const { kind, namespace, name } = parseEntityRef(payload.entityRef);\n const prefix = `/catalog/${namespace.toLowerCase()}/${kind.toLowerCase()}/${name}`;\n return `${prefix}${payload.deepLinkSuffix}`;\n }\n}\n"],"names":["parseEntityRef"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rwdocs/backstage-plugin-rw-backend-module-notifications",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"license": "MIT OR Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@backstage/catalog-model": "^1.7.6",
|
|
52
|
-
"@rwdocs/backstage-plugin-rw-common": "^0.1.
|
|
52
|
+
"@rwdocs/backstage-plugin-rw-common": "^0.1.7"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"@backstage/backend-plugin-api": "^1.0.0",
|