@rwdocs/backstage-plugin-rw-backend 0.1.4 → 0.1.6

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.
Files changed (59) hide show
  1. package/dist/comments/CommentEventPublisher.cjs.js +119 -0
  2. package/dist/comments/CommentEventPublisher.cjs.js.map +1 -0
  3. package/dist/comments/CommentStore.cjs.js +160 -0
  4. package/dist/comments/CommentStore.cjs.js.map +1 -0
  5. package/dist/comments/author.cjs.js +38 -0
  6. package/dist/comments/author.cjs.js.map +1 -0
  7. package/dist/comments/logging.cjs.js +50 -0
  8. package/dist/comments/logging.cjs.js.map +1 -0
  9. package/dist/comments/mapping.cjs.js +32 -0
  10. package/dist/comments/mapping.cjs.js.map +1 -0
  11. package/dist/comments/permissions.cjs.js +24 -0
  12. package/dist/comments/permissions.cjs.js.map +1 -0
  13. package/dist/comments/router.cjs.js +314 -0
  14. package/dist/comments/router.cjs.js.map +1 -0
  15. package/dist/comments/timestamps.cjs.js +23 -0
  16. package/dist/comments/timestamps.cjs.js.map +1 -0
  17. package/dist/comments/types.cjs.js +14 -0
  18. package/dist/comments/types.cjs.js.map +1 -0
  19. package/dist/hub.cjs.js +2 -2
  20. package/dist/hub.cjs.js.map +1 -1
  21. package/dist/inbox/InboxStore.cjs.js +96 -0
  22. package/dist/inbox/InboxStore.cjs.js.map +1 -0
  23. package/dist/inbox/cursor.cjs.js +35 -0
  24. package/dist/inbox/cursor.cjs.js.map +1 -0
  25. package/dist/inbox/inboxRouter.cjs.js +90 -0
  26. package/dist/inbox/inboxRouter.cjs.js.map +1 -0
  27. package/dist/inbox/mapping.cjs.js +35 -0
  28. package/dist/inbox/mapping.cjs.js.map +1 -0
  29. package/dist/inbox/snippet.cjs.js +44 -0
  30. package/dist/inbox/snippet.cjs.js.map +1 -0
  31. package/dist/plugin.cjs.js +129 -34
  32. package/dist/plugin.cjs.js.map +1 -1
  33. package/dist/router.cjs.js +9 -8
  34. package/dist/router.cjs.js.map +1 -1
  35. package/dist/siteIndex/PagesReader.cjs.js +15 -0
  36. package/dist/siteIndex/PagesReader.cjs.js.map +1 -0
  37. package/dist/siteIndex/RegistryStore.cjs.js +20 -0
  38. package/dist/siteIndex/RegistryStore.cjs.js.map +1 -0
  39. package/dist/siteIndex/SectionOwnershipStore.cjs.js +21 -0
  40. package/dist/siteIndex/SectionOwnershipStore.cjs.js.map +1 -0
  41. package/dist/siteIndex/SectionsReader.cjs.js +14 -0
  42. package/dist/siteIndex/SectionsReader.cjs.js.map +1 -0
  43. package/dist/siteIndex/SiteRefreshStore.cjs.js +73 -0
  44. package/dist/siteIndex/SiteRefreshStore.cjs.js.map +1 -0
  45. package/dist/siteIndex/effectiveOwnership.cjs.js +34 -0
  46. package/dist/siteIndex/effectiveOwnership.cjs.js.map +1 -0
  47. package/dist/siteIndex/registryHash.cjs.js +10 -0
  48. package/dist/siteIndex/registryHash.cjs.js.map +1 -0
  49. package/dist/siteIndex/runScan.cjs.js +75 -0
  50. package/dist/siteIndex/runScan.cjs.js.map +1 -0
  51. package/dist/siteIndex/runWorker.cjs.js +81 -0
  52. package/dist/siteIndex/runWorker.cjs.js.map +1 -0
  53. package/dist/siteIndex/schedule.cjs.js +25 -0
  54. package/dist/siteIndex/schedule.cjs.js.map +1 -0
  55. package/migrations/20260621000000_init.js +81 -0
  56. package/package.json +16 -9
  57. package/config.d.ts +0 -56
  58. package/dist/entityPath.cjs.js +0 -12
  59. package/dist/entityPath.cjs.js.map +0 -1
@@ -0,0 +1,119 @@
1
+ 'use strict';
2
+
3
+ var catalogModel = require('@backstage/catalog-model');
4
+ var backstagePluginRwCommon = require('@rwdocs/backstage-plugin-rw-common');
5
+ var snippet = require('../inbox/snippet.cjs.js');
6
+ var mapping = require('../inbox/mapping.cjs.js');
7
+ var author = require('./author.cjs.js');
8
+ var types = require('./types.cjs.js');
9
+
10
+ class CommentEventPublisher {
11
+ events;
12
+ sections;
13
+ comments;
14
+ logger;
15
+ pages;
16
+ constructor(deps) {
17
+ this.events = deps.events;
18
+ this.sections = deps.sections;
19
+ this.comments = deps.comments;
20
+ this.logger = deps.logger;
21
+ this.pages = deps.pages;
22
+ }
23
+ async onCommentCreated(row, actorRef) {
24
+ try {
25
+ if (row.parent_id === null) {
26
+ await this.publishOwnerSide(row, actorRef);
27
+ } else {
28
+ await this.publishParticipantSide("created", row, row.parent_id, actorRef);
29
+ }
30
+ } catch (error) {
31
+ this.logger.warn(`rw.comments publish (created) failed: ${error}`);
32
+ }
33
+ }
34
+ async onCommentResolved(row, actorRef, actorName) {
35
+ try {
36
+ await this.publishParticipantSide("resolved", row, row.id, actorRef, actorName);
37
+ } catch (error) {
38
+ this.logger.warn(`rw.comments publish (resolved) failed: ${error}`);
39
+ }
40
+ }
41
+ async resolvePageTitle(siteRef, sectionRef, subpath) {
42
+ try {
43
+ return await this.pages.getTitle(siteRef, sectionRef, subpath);
44
+ } catch (err) {
45
+ this.logger.warn(`rw.comments: could not resolve page title: ${err}`);
46
+ return null;
47
+ }
48
+ }
49
+ async publishOwnerSide(row, actorRef) {
50
+ const section = await this.sections.getSection(row.site_ref, row.section_ref);
51
+ if (!section || !section.entity_owner_ref) return;
52
+ const recipients = [section.entity_owner_ref].filter((ref) => ref !== actorRef);
53
+ if (recipients.length === 0) return;
54
+ const subpath = types.subpathOf(row.page_ref);
55
+ const viewerPath = mapping.joinNonEmpty([section.section_path, subpath], "/");
56
+ const actorName = author.authorFromRow(row).name;
57
+ const [pageTitle, sectionTitle] = await Promise.all([
58
+ this.resolvePageTitle(row.site_ref, row.section_ref, subpath),
59
+ this.resolvePageTitle(row.site_ref, row.section_ref, "")
60
+ ]);
61
+ await this.publish("created", "owner", row, row.id, recipients, actorRef, {
62
+ entityRef: section.entity_ref,
63
+ viewerPath,
64
+ actorName,
65
+ pageTitle,
66
+ sectionTitle
67
+ });
68
+ }
69
+ async publishParticipantSide(kind, row, rootId, actorRef, resolvedActorName) {
70
+ const participants = await this.comments.participantsOf(rootId);
71
+ const recipients = participants.filter((ref) => ref !== actorRef);
72
+ if (recipients.length === 0) return;
73
+ const section = await this.sections.getSection(row.site_ref, row.section_ref);
74
+ const subpath = types.subpathOf(row.page_ref);
75
+ const viewerPath = section ? mapping.joinNonEmpty([section.section_path, subpath], "/") : subpath;
76
+ const actorName = kind === "resolved" ? resolvedActorName ?? catalogModel.parseEntityRef(actorRef).name : author.authorFromRow(row).name;
77
+ const [pageTitle, sectionTitle] = await Promise.all([
78
+ this.resolvePageTitle(row.site_ref, row.section_ref, subpath),
79
+ this.resolvePageTitle(row.site_ref, row.section_ref, "")
80
+ ]);
81
+ await this.publish(kind, "participants", row, rootId, recipients, actorRef, {
82
+ entityRef: section?.entity_ref ?? null,
83
+ viewerPath,
84
+ actorName,
85
+ pageTitle,
86
+ sectionTitle
87
+ });
88
+ }
89
+ async publish(kind, audience, row, rootId, recipients, actorRef, link) {
90
+ const eventPayload = {
91
+ kind,
92
+ audience,
93
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
94
+ commentId: row.id,
95
+ rootId,
96
+ parentId: row.parent_id,
97
+ siteRef: row.site_ref,
98
+ sectionRef: row.section_ref,
99
+ pageRef: row.page_ref,
100
+ actorRef,
101
+ recipients,
102
+ entityRef: link.entityRef,
103
+ // Anchor on the thread root, not the triggering row: a reply notification
104
+ // should open the whole thread (rootId === row.id for top-level + resolve events).
105
+ deepLinkSuffix: backstagePluginRwCommon.buildCommentDeepLinkSuffix({
106
+ viewerPath: link.viewerPath,
107
+ commentId: rootId
108
+ }),
109
+ bodySnippet: snippet.snippetFromHtml(row.body_html),
110
+ actorName: link.actorName,
111
+ pageTitle: link.pageTitle,
112
+ sectionTitle: link.sectionTitle
113
+ };
114
+ await this.events.publish({ topic: backstagePluginRwCommon.RW_COMMENTS_TOPIC, eventPayload });
115
+ }
116
+ }
117
+
118
+ exports.CommentEventPublisher = CommentEventPublisher;
119
+ //# sourceMappingURL=CommentEventPublisher.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CommentEventPublisher.cjs.js","sources":["../../src/comments/CommentEventPublisher.ts"],"sourcesContent":["import { LoggerService } from \"@backstage/backend-plugin-api\";\nimport { parseEntityRef } from \"@backstage/catalog-model\";\nimport { EventsService } from \"@backstage/plugin-events-node\";\nimport {\n RW_COMMENTS_TOPIC,\n CommentEventPayload,\n CommentEventAudience,\n buildCommentDeepLinkSuffix,\n} from \"@rwdocs/backstage-plugin-rw-common\";\nimport { SectionsReader } from \"../siteIndex/SectionsReader\";\nimport { PagesReader } from \"../siteIndex/PagesReader\";\nimport { snippetFromHtml } from \"../inbox/snippet\";\nimport { joinNonEmpty } from \"../inbox/mapping\";\nimport { CommentStore } from \"./CommentStore\";\nimport { authorFromRow } from \"./author\";\nimport { CommentRow, subpathOf } from \"./types\";\n\n/** Publishes self-contained `rw.comments` domain events after a comment write commits.\n * Owns recipient + deep-link resolution (it has the comments + sections tables) so the\n * notifications module can stay a thin sender. Best-effort: every method catches and\n * logs, and always resolves, so a publish failure can never affect the comment write.\n * Callers invoke fire-and-forget. */\nexport class CommentEventPublisher {\n private readonly events: EventsService;\n private readonly sections: SectionsReader;\n private readonly comments: CommentStore;\n private readonly logger: LoggerService;\n private readonly pages: PagesReader;\n\n constructor(deps: {\n events: EventsService;\n sections: SectionsReader;\n comments: CommentStore;\n logger: LoggerService;\n pages: PagesReader;\n }) {\n this.events = deps.events;\n this.sections = deps.sections;\n this.comments = deps.comments;\n this.logger = deps.logger;\n this.pages = deps.pages;\n }\n\n async onCommentCreated(row: CommentRow, actorRef: string): Promise<void> {\n try {\n if (row.parent_id === null) {\n await this.publishOwnerSide(row, actorRef);\n } else {\n await this.publishParticipantSide(\"created\", row, row.parent_id, actorRef);\n }\n } catch (error) {\n this.logger.warn(`rw.comments publish (created) failed: ${error}`);\n }\n }\n\n async onCommentResolved(row: CommentRow, actorRef: string, actorName?: string): Promise<void> {\n try {\n // resolve only happens on top-level rows, so the row IS the thread root.\n await this.publishParticipantSide(\"resolved\", row, row.id, actorRef, actorName);\n } catch (error) {\n this.logger.warn(`rw.comments publish (resolved) failed: ${error}`);\n }\n }\n\n private async resolvePageTitle(\n siteRef: string,\n sectionRef: string,\n subpath: string,\n ): Promise<string | null> {\n try {\n return await this.pages.getTitle(siteRef, sectionRef, subpath);\n } catch (err) {\n this.logger.warn(`rw.comments: could not resolve page title: ${err}`);\n return null;\n }\n }\n\n private async publishOwnerSide(row: CommentRow, actorRef: string): Promise<void> {\n const section = await this.sections.getSection(row.site_ref, row.section_ref);\n if (!section || !section.entity_owner_ref) return; // new/unowned section: inbox catches it\n const recipients = [section.entity_owner_ref].filter((ref) => ref !== actorRef);\n if (recipients.length === 0) return;\n const subpath = subpathOf(row.page_ref);\n const viewerPath = joinNonEmpty([section.section_path, subpath], \"/\");\n const actorName = authorFromRow(row).name;\n const [pageTitle, sectionTitle] = await Promise.all([\n this.resolvePageTitle(row.site_ref, row.section_ref, subpath),\n this.resolvePageTitle(row.site_ref, row.section_ref, \"\"),\n ]);\n await this.publish(\"created\", \"owner\", row, row.id, recipients, actorRef, {\n entityRef: section.entity_ref,\n viewerPath,\n actorName,\n pageTitle,\n sectionTitle,\n });\n }\n\n private async publishParticipantSide(\n kind: \"created\" | \"resolved\",\n row: CommentRow,\n rootId: string,\n actorRef: string,\n resolvedActorName?: string,\n ): Promise<void> {\n // participantsOf already returns distinct refs, so no extra dedup is needed here.\n const participants = await this.comments.participantsOf(rootId);\n const recipients = participants.filter((ref) => ref !== actorRef);\n if (recipients.length === 0) return;\n // The section row provides entityRef (link target) + section_path (path prefix); degrade gracefully if absent: entityRef -> null (module emits no link), viewerPath -> bare subpath.\n const section = await this.sections.getSection(row.site_ref, row.section_ref);\n const subpath = subpathOf(row.page_ref);\n const viewerPath = section ? joinNonEmpty([section.section_path, subpath], \"/\") : subpath;\n const actorName =\n kind === \"resolved\"\n ? (resolvedActorName ?? parseEntityRef(actorRef).name)\n : authorFromRow(row).name;\n const [pageTitle, sectionTitle] = await Promise.all([\n this.resolvePageTitle(row.site_ref, row.section_ref, subpath),\n this.resolvePageTitle(row.site_ref, row.section_ref, \"\"),\n ]);\n await this.publish(kind, \"participants\", row, rootId, recipients, actorRef, {\n entityRef: section?.entity_ref ?? null,\n viewerPath,\n actorName,\n pageTitle,\n sectionTitle,\n });\n }\n\n private async publish(\n kind: \"created\" | \"resolved\",\n audience: CommentEventAudience,\n row: CommentRow,\n rootId: string,\n recipients: string[],\n actorRef: string,\n link: {\n entityRef: string | null;\n viewerPath: string;\n actorName: string;\n pageTitle: string | null;\n sectionTitle: string | null;\n },\n ): Promise<void> {\n const eventPayload: CommentEventPayload = {\n kind,\n audience,\n occurredAt: new Date().toISOString(),\n commentId: row.id,\n rootId,\n parentId: row.parent_id,\n siteRef: row.site_ref,\n sectionRef: row.section_ref,\n pageRef: row.page_ref,\n actorRef,\n recipients,\n entityRef: link.entityRef,\n // Anchor on the thread root, not the triggering row: a reply notification\n // should open the whole thread (rootId === row.id for top-level + resolve events).\n deepLinkSuffix: buildCommentDeepLinkSuffix({\n viewerPath: link.viewerPath,\n commentId: rootId,\n }),\n bodySnippet: snippetFromHtml(row.body_html),\n actorName: link.actorName,\n pageTitle: link.pageTitle,\n sectionTitle: link.sectionTitle,\n };\n await this.events.publish({ topic: RW_COMMENTS_TOPIC, eventPayload });\n }\n}\n"],"names":["subpathOf","joinNonEmpty","authorFromRow","parseEntityRef","buildCommentDeepLinkSuffix","snippetFromHtml","RW_COMMENTS_TOPIC"],"mappings":"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"}
@@ -0,0 +1,160 @@
1
+ 'use strict';
2
+
3
+ var uuid = require('uuid');
4
+ var core = require('@rwdocs/core');
5
+ var types = require('./types.cjs.js');
6
+
7
+ const TABLE = "comments";
8
+ class CommentStore {
9
+ constructor(knex) {
10
+ this.knex = knex;
11
+ }
12
+ async create(siteRef, input) {
13
+ const now = /* @__PURE__ */ new Date();
14
+ const row = {
15
+ id: uuid.v7(),
16
+ site_ref: siteRef,
17
+ page_ref: input.pageRef,
18
+ section_ref: types.sectionRefOf(input.pageRef),
19
+ parent_id: input.parentId ?? null,
20
+ author_ref: input.authorRef,
21
+ author_profile: input.authorProfile ? JSON.stringify(input.authorProfile) : null,
22
+ body: input.body,
23
+ body_html: await core.renderCommentBody(input.body),
24
+ selectors: JSON.stringify(input.selectors),
25
+ status: "open",
26
+ created_at: now,
27
+ updated_at: now,
28
+ resolved_at: null,
29
+ resolved_by: null,
30
+ deleted_at: null
31
+ };
32
+ await this.knex(TABLE).insert(row);
33
+ return row;
34
+ }
35
+ /**
36
+ * By global uuid; includes soft-deleted rows.
37
+ *
38
+ * Pass `opts.executor` (a transaction) to read inside an open transaction, and
39
+ * `opts.forUpdate` to lock the row for the rest of the transaction (real
40
+ * `SELECT ... FOR UPDATE` on Postgres; ignored/no-op on better-sqlite3, whose
41
+ * transactions already serialize writes).
42
+ */
43
+ async get(id, opts) {
44
+ const q = (opts?.executor ?? this.knex)(TABLE).where({ id });
45
+ if (opts?.forUpdate) q.forUpdate();
46
+ return q.first();
47
+ }
48
+ /** Run `fn` inside a single database transaction. */
49
+ async transaction(fn) {
50
+ return this.knex.transaction(fn);
51
+ }
52
+ /**
53
+ * Site-scoped; ALWAYS excludes soft-deleted rows; ORDER BY created_at ASC.
54
+ *
55
+ * Forward hook: the router currently reads only by `pageRef`. The additional
56
+ * `ListFilter` fields (`sectionRef`, `status`, `parentId`, `topLevelOnly`) and the
57
+ * corresponding `section_ref` column + `comments_section_idx` index are intentional
58
+ * forward hooks for the planned cross-section / entity-scoped querying direction (v1
59
+ * design). They are not dead code.
60
+ */
61
+ async list(siteRef, filter) {
62
+ const q = this.knex(TABLE).where({ site_ref: siteRef }).whereNull("deleted_at");
63
+ if (filter.pageRef !== void 0) q.andWhere({ page_ref: filter.pageRef });
64
+ if (filter.sectionRef !== void 0) q.andWhere({ section_ref: filter.sectionRef });
65
+ if (filter.status !== void 0) q.andWhere({ status: filter.status });
66
+ if (filter.parentId !== void 0) q.andWhere({ parent_id: filter.parentId });
67
+ if (filter.topLevelOnly) q.whereNull("parent_id");
68
+ return q.orderBy("created_at", "asc").orderBy("id", "asc");
69
+ }
70
+ async update(id, patch) {
71
+ const existing = await this.get(id);
72
+ if (!existing) return void 0;
73
+ const now = /* @__PURE__ */ new Date();
74
+ const changes = { updated_at: now };
75
+ if (patch.body !== void 0) {
76
+ changes.body = patch.body;
77
+ changes.body_html = await core.renderCommentBody(patch.body);
78
+ }
79
+ if (patch.selectors !== void 0) {
80
+ changes.selectors = JSON.stringify(patch.selectors);
81
+ }
82
+ if (patch.status !== void 0) {
83
+ changes.status = patch.status;
84
+ if (patch.status === "resolved") {
85
+ changes.resolved_at = this.knex.raw("COALESCE(resolved_at, ?)", [now]);
86
+ changes.resolved_by = this.knex.raw("COALESCE(resolved_by, ?)", [
87
+ patch.resolverRef ?? null
88
+ ]);
89
+ } else {
90
+ changes.resolved_at = null;
91
+ changes.resolved_by = null;
92
+ }
93
+ }
94
+ await this.knex(TABLE).where({ id }).update(changes);
95
+ return this.get(id);
96
+ }
97
+ /**
98
+ * Conditional soft-delete: only applies to a currently-live row (`deleted_at IS
99
+ * NULL`). Returns the fresh row when it applied, or `undefined` when 0 rows
100
+ * matched (the row was already deleted — a lost race). Pass `executor` to run
101
+ * inside an open transaction.
102
+ */
103
+ async softDelete(id, executor) {
104
+ const db = executor ?? this.knex;
105
+ const now = /* @__PURE__ */ new Date();
106
+ const count = await db(TABLE).where({ id }).whereNull("deleted_at").update({ deleted_at: now, updated_at: now });
107
+ if (count === 0) return void 0;
108
+ return this.get(id, { executor });
109
+ }
110
+ /**
111
+ * Conditional restore: only applies to a currently-deleted row (`deleted_at IS
112
+ * NOT NULL`). Returns the fresh row when it applied, or `undefined` when 0 rows
113
+ * matched (the row was already live — a lost race). Pass `executor` to run
114
+ * inside an open transaction.
115
+ */
116
+ async restore(id, executor) {
117
+ const db = executor ?? this.knex;
118
+ const count = await db(TABLE).where({ id }).whereNotNull("deleted_at").update({ deleted_at: null, updated_at: /* @__PURE__ */ new Date() });
119
+ if (count === 0) return void 0;
120
+ return this.get(id, { executor });
121
+ }
122
+ /**
123
+ * Distinct author refs participating in a thread: the root comment plus its direct,
124
+ * non-deleted replies (threads are one level deep, so `rootId` is the parent of every
125
+ * reply). Used by the comment-event publisher to address commenter-side notifications.
126
+ * Order is stable (creation order) so notification recipient lists are deterministic.
127
+ */
128
+ async participantsOf(rootId) {
129
+ const rows = await this.knex(TABLE).where(function whereInThread() {
130
+ this.where("id", rootId).orWhere("parent_id", rootId);
131
+ }).whereNull("deleted_at").orderBy("created_at", "asc").orderBy("id", "asc").select("author_ref");
132
+ const seen = /* @__PURE__ */ new Set();
133
+ const out = [];
134
+ for (const row of rows) {
135
+ const ref = row.author_ref;
136
+ if (!seen.has(ref)) {
137
+ seen.add(ref);
138
+ out.push(ref);
139
+ }
140
+ }
141
+ return out;
142
+ }
143
+ /**
144
+ * Returns a map from parent_id → open reply count for a given set of
145
+ * top-level comment ids. Uses a single grouped query (no N+1).
146
+ * Only counts open, non-deleted replies.
147
+ */
148
+ async replyCountsFor(parentIds) {
149
+ const map = /* @__PURE__ */ new Map();
150
+ if (!parentIds.length) return map;
151
+ const rows = await this.knex(TABLE).whereIn("parent_id", parentIds).andWhere("status", "open").whereNull("deleted_at").groupBy("parent_id").select("parent_id").count("id as cnt");
152
+ for (const row of rows) {
153
+ map.set(row.parent_id, Number(row.cnt));
154
+ }
155
+ return map;
156
+ }
157
+ }
158
+
159
+ exports.CommentStore = CommentStore;
160
+ //# sourceMappingURL=CommentStore.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CommentStore.cjs.js","sources":["../../src/comments/CommentStore.ts"],"sourcesContent":["import type { Knex } from \"knex\";\nimport { v7 as uuidv7 } from \"uuid\";\nimport { renderCommentBody } from \"@rwdocs/core\";\nimport { CommentRow, CommentStatus, CreateCommentInput, ListFilter, sectionRefOf } from \"./types\";\n\nconst TABLE = \"comments\";\n\nexport class CommentStore {\n constructor(private readonly knex: Knex) {}\n\n async create(siteRef: string, input: CreateCommentInput): Promise<CommentRow> {\n const now = new Date();\n const row: CommentRow = {\n id: uuidv7(),\n site_ref: siteRef,\n page_ref: input.pageRef,\n section_ref: sectionRefOf(input.pageRef),\n parent_id: input.parentId ?? null,\n author_ref: input.authorRef,\n author_profile: input.authorProfile ? JSON.stringify(input.authorProfile) : null,\n body: input.body,\n body_html: await renderCommentBody(input.body),\n selectors: JSON.stringify(input.selectors),\n status: \"open\",\n created_at: now,\n updated_at: now,\n resolved_at: null,\n resolved_by: null,\n deleted_at: null,\n };\n await this.knex(TABLE).insert(row);\n return row;\n }\n\n /**\n * By global uuid; includes soft-deleted rows.\n *\n * Pass `opts.executor` (a transaction) to read inside an open transaction, and\n * `opts.forUpdate` to lock the row for the rest of the transaction (real\n * `SELECT ... FOR UPDATE` on Postgres; ignored/no-op on better-sqlite3, whose\n * transactions already serialize writes).\n */\n async get(\n id: string,\n opts?: { executor?: Knex.Transaction; forUpdate?: boolean },\n ): Promise<CommentRow | undefined> {\n const q = (opts?.executor ?? this.knex)<CommentRow>(TABLE).where({ id });\n if (opts?.forUpdate) q.forUpdate();\n return q.first();\n }\n\n /** Run `fn` inside a single database transaction. */\n async transaction<T>(fn: (tx: Knex.Transaction) => Promise<T>): Promise<T> {\n return this.knex.transaction(fn);\n }\n\n /**\n * Site-scoped; ALWAYS excludes soft-deleted rows; ORDER BY created_at ASC.\n *\n * Forward hook: the router currently reads only by `pageRef`. The additional\n * `ListFilter` fields (`sectionRef`, `status`, `parentId`, `topLevelOnly`) and the\n * corresponding `section_ref` column + `comments_section_idx` index are intentional\n * forward hooks for the planned cross-section / entity-scoped querying direction (v1\n * design). They are not dead code.\n */\n async list(siteRef: string, filter: ListFilter): Promise<CommentRow[]> {\n const q = this.knex<CommentRow>(TABLE).where({ site_ref: siteRef }).whereNull(\"deleted_at\");\n if (filter.pageRef !== undefined) q.andWhere({ page_ref: filter.pageRef });\n if (filter.sectionRef !== undefined) q.andWhere({ section_ref: filter.sectionRef });\n if (filter.status !== undefined) q.andWhere({ status: filter.status });\n if (filter.parentId !== undefined) q.andWhere({ parent_id: filter.parentId });\n if (filter.topLevelOnly) q.whereNull(\"parent_id\");\n return q.orderBy(\"created_at\", \"asc\").orderBy(\"id\", \"asc\");\n }\n\n async update(\n id: string,\n patch: { body?: string; status?: CommentStatus; selectors?: unknown[]; resolverRef?: string },\n ): Promise<CommentRow | undefined> {\n const existing = await this.get(id);\n if (!existing) return undefined;\n\n const now = new Date();\n const changes: Partial<CommentRow> = { updated_at: now };\n if (patch.body !== undefined) {\n changes.body = patch.body;\n changes.body_html = await renderCommentBody(patch.body);\n }\n if (patch.selectors !== undefined) {\n changes.selectors = JSON.stringify(patch.selectors);\n }\n if (patch.status !== undefined) {\n changes.status = patch.status;\n if (patch.status === \"resolved\") {\n // Push the idempotency guard into SQL — concurrent resolves can't double-stamp.\n // COALESCE leaves existing stamps intact; works on both better-sqlite3 and Postgres.\n (changes as any).resolved_at = this.knex.raw(\"COALESCE(resolved_at, ?)\", [now]);\n (changes as any).resolved_by = this.knex.raw(\"COALESCE(resolved_by, ?)\", [\n patch.resolverRef ?? null,\n ]);\n } else {\n changes.resolved_at = null;\n changes.resolved_by = null;\n }\n }\n await this.knex(TABLE).where({ id }).update(changes);\n return this.get(id);\n }\n\n /**\n * Conditional soft-delete: only applies to a currently-live row (`deleted_at IS\n * NULL`). Returns the fresh row when it applied, or `undefined` when 0 rows\n * matched (the row was already deleted — a lost race). Pass `executor` to run\n * inside an open transaction.\n */\n async softDelete(id: string, executor?: Knex.Transaction): Promise<CommentRow | undefined> {\n const db = executor ?? this.knex;\n const now = new Date();\n const count = await db(TABLE)\n .where({ id })\n .whereNull(\"deleted_at\")\n .update({ deleted_at: now, updated_at: now });\n if (count === 0) return undefined;\n return this.get(id, { executor });\n }\n\n /**\n * Conditional restore: only applies to a currently-deleted row (`deleted_at IS\n * NOT NULL`). Returns the fresh row when it applied, or `undefined` when 0 rows\n * matched (the row was already live — a lost race). Pass `executor` to run\n * inside an open transaction.\n */\n async restore(id: string, executor?: Knex.Transaction): Promise<CommentRow | undefined> {\n const db = executor ?? this.knex;\n const count = await db(TABLE)\n .where({ id })\n .whereNotNull(\"deleted_at\")\n .update({ deleted_at: null, updated_at: new Date() });\n if (count === 0) return undefined;\n return this.get(id, { executor });\n }\n\n /**\n * Distinct author refs participating in a thread: the root comment plus its direct,\n * non-deleted replies (threads are one level deep, so `rootId` is the parent of every\n * reply). Used by the comment-event publisher to address commenter-side notifications.\n * Order is stable (creation order) so notification recipient lists are deterministic.\n */\n async participantsOf(rootId: string): Promise<string[]> {\n const rows = await this.knex(TABLE)\n .where(function whereInThread(this: Knex.QueryBuilder) {\n this.where(\"id\", rootId).orWhere(\"parent_id\", rootId);\n })\n .whereNull(\"deleted_at\")\n .orderBy(\"created_at\", \"asc\")\n .orderBy(\"id\", \"asc\")\n .select(\"author_ref\");\n const seen = new Set<string>();\n const out: string[] = [];\n for (const row of rows) {\n const ref = row.author_ref as string;\n if (!seen.has(ref)) {\n seen.add(ref);\n out.push(ref);\n }\n }\n return out;\n }\n\n /**\n * Returns a map from parent_id → open reply count for a given set of\n * top-level comment ids. Uses a single grouped query (no N+1).\n * Only counts open, non-deleted replies.\n */\n async replyCountsFor(parentIds: string[]): Promise<Map<string, number>> {\n const map = new Map<string, number>();\n if (!parentIds.length) return map;\n const rows = await this.knex(TABLE)\n .whereIn(\"parent_id\", parentIds)\n .andWhere(\"status\", \"open\")\n .whereNull(\"deleted_at\")\n .groupBy(\"parent_id\")\n .select(\"parent_id\")\n .count(\"id as cnt\");\n for (const row of rows) {\n map.set(row.parent_id as string, Number(row.cnt));\n }\n return map;\n }\n}\n"],"names":["uuidv7","sectionRefOf","renderCommentBody"],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ var catalogModel = require('@backstage/catalog-model');
4
+
5
+ function authorFromRow(row) {
6
+ const profile = row.author_profile ? JSON.parse(row.author_profile) : null;
7
+ return {
8
+ id: row.author_ref,
9
+ name: profile?.displayName ?? catalogModel.parseEntityRef(row.author_ref).name,
10
+ ...profile?.picture ? { avatarUrl: profile.picture } : {}
11
+ };
12
+ }
13
+ async function resolveAuthor(deps) {
14
+ const { userInfo, auth, catalog, credentials } = deps;
15
+ const { userEntityRef } = await userInfo.getUserInfo(credentials);
16
+ const serviceCreds = await auth.getOwnServiceCredentials();
17
+ let entity;
18
+ try {
19
+ entity = await catalog.getEntityByRef(userEntityRef, { credentials: serviceCreds });
20
+ } catch {
21
+ return { authorRef: userEntityRef };
22
+ }
23
+ const profile = entity?.spec?.profile;
24
+ if (profile?.displayName || profile?.picture) {
25
+ return {
26
+ authorRef: userEntityRef,
27
+ authorProfile: {
28
+ ...profile.displayName ? { displayName: profile.displayName } : {},
29
+ ...profile.picture ? { picture: profile.picture } : {}
30
+ }
31
+ };
32
+ }
33
+ return { authorRef: userEntityRef };
34
+ }
35
+
36
+ exports.authorFromRow = authorFromRow;
37
+ exports.resolveAuthor = resolveAuthor;
38
+ //# sourceMappingURL=author.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"author.cjs.js","sources":["../../src/comments/author.ts"],"sourcesContent":["import type {\n AuthService,\n BackstageCredentials,\n UserInfoService,\n} from \"@backstage/backend-plugin-api\";\nimport type { CatalogService } from \"@backstage/plugin-catalog-node\";\nimport { parseEntityRef } from \"@backstage/catalog-model\";\nimport { AuthorProfile, CommentRow } from \"./types\";\n\n/**\n * Shapes the display author ({ id, name, avatarUrl? }) from a comment row's\n * stored author_ref + author_profile snapshot. Shared by the thread view\n * (toCommentResponse) and the inbox (toInboxItem) so both render an author\n * identically; the name falls back to the humanized entity name when there's\n * no profile snapshot.\n */\nexport function authorFromRow(row: Pick<CommentRow, \"author_ref\" | \"author_profile\">): {\n id: string;\n name: string;\n avatarUrl?: string;\n} {\n const profile: AuthorProfile | null = row.author_profile ? JSON.parse(row.author_profile) : null;\n return {\n id: row.author_ref,\n name: profile?.displayName ?? parseEntityRef(row.author_ref).name,\n ...(profile?.picture ? { avatarUrl: profile.picture } : {}),\n };\n}\n\nexport async function resolveAuthor(deps: {\n userInfo: UserInfoService;\n auth: AuthService;\n catalog: CatalogService;\n credentials: BackstageCredentials;\n}): Promise<{ authorRef: string; authorProfile?: AuthorProfile }> {\n const { userInfo, auth, catalog, credentials } = deps;\n const { userEntityRef } = await userInfo.getUserInfo(credentials);\n\n const serviceCreds = await auth.getOwnServiceCredentials();\n let entity: Awaited<ReturnType<typeof catalog.getEntityByRef>>;\n try {\n entity = await catalog.getEntityByRef(userEntityRef, { credentials: serviceCreds });\n } catch {\n return { authorRef: userEntityRef };\n }\n\n const profile = entity?.spec?.profile as { displayName?: string; picture?: string } | undefined;\n if (profile?.displayName || profile?.picture) {\n return {\n authorRef: userEntityRef,\n authorProfile: {\n ...(profile.displayName ? { displayName: profile.displayName } : {}),\n ...(profile.picture ? { picture: profile.picture } : {}),\n },\n };\n }\n return { authorRef: userEntityRef };\n}\n"],"names":["parseEntityRef"],"mappings":";;;;AAgBO,SAAS,cAAc,GAAA,EAI5B;AACA,EAAA,MAAM,UAAgC,GAAA,CAAI,cAAA,GAAiB,KAAK,KAAA,CAAM,GAAA,CAAI,cAAc,CAAA,GAAI,IAAA;AAC5F,EAAA,OAAO;AAAA,IACL,IAAI,GAAA,CAAI,UAAA;AAAA,IACR,MAAM,OAAA,EAAS,WAAA,IAAeA,2BAAA,CAAe,GAAA,CAAI,UAAU,CAAA,CAAE,IAAA;AAAA,IAC7D,GAAI,SAAS,OAAA,GAAU,EAAE,WAAW,OAAA,CAAQ,OAAA,KAAY;AAAC,GAC3D;AACF;AAEA,eAAsB,cAAc,IAAA,EAK8B;AAChE,EAAA,MAAM,EAAE,QAAA,EAAU,IAAA,EAAM,OAAA,EAAS,aAAY,GAAI,IAAA;AACjD,EAAA,MAAM,EAAE,aAAA,EAAc,GAAI,MAAM,QAAA,CAAS,YAAY,WAAW,CAAA;AAEhE,EAAA,MAAM,YAAA,GAAe,MAAM,IAAA,CAAK,wBAAA,EAAyB;AACzD,EAAA,IAAI,MAAA;AACJ,EAAA,IAAI;AACF,IAAA,MAAA,GAAS,MAAM,OAAA,CAAQ,cAAA,CAAe,eAAe,EAAE,WAAA,EAAa,cAAc,CAAA;AAAA,EACpF,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,EAAE,WAAW,aAAA,EAAc;AAAA,EACpC;AAEA,EAAA,MAAM,OAAA,GAAU,QAAQ,IAAA,EAAM,OAAA;AAC9B,EAAA,IAAI,OAAA,EAAS,WAAA,IAAe,OAAA,EAAS,OAAA,EAAS;AAC5C,IAAA,OAAO;AAAA,MACL,SAAA,EAAW,aAAA;AAAA,MACX,aAAA,EAAe;AAAA,QACb,GAAI,QAAQ,WAAA,GAAc,EAAE,aAAa,OAAA,CAAQ,WAAA,KAAgB,EAAC;AAAA,QAClE,GAAI,QAAQ,OAAA,GAAU,EAAE,SAAS,OAAA,CAAQ,OAAA,KAAY;AAAC;AACxD,KACF;AAAA,EACF;AACA,EAAA,OAAO,EAAE,WAAW,aAAA,EAAc;AACpC;;;;;"}
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ function logCommentOp(logger, event) {
4
+ switch (event.kind) {
5
+ case "mutation":
6
+ logger.info(`comment op ${event.op}`, {
7
+ op: event.op,
8
+ siteRef: event.siteRef,
9
+ commentId: event.commentId,
10
+ ...event.parentId ? { parentId: event.parentId } : {},
11
+ outcome: "ok"
12
+ });
13
+ return;
14
+ case "denied":
15
+ logger.warn(`comment op ${event.op} denied`, {
16
+ op: event.op,
17
+ permission: event.permission,
18
+ userEntityRef: event.userEntityRef,
19
+ outcome: "denied"
20
+ });
21
+ return;
22
+ case "entity-not-visible":
23
+ logger.warn(`comment op ${event.op} entity not visible`, {
24
+ op: event.op,
25
+ siteRef: event.siteRef,
26
+ userEntityRef: event.userEntityRef,
27
+ outcome: "entity-not-visible"
28
+ });
29
+ return;
30
+ case "oversized-list":
31
+ logger.warn("comment list oversized", {
32
+ op: "list",
33
+ siteRef: event.siteRef,
34
+ pageRef: event.pageRef,
35
+ count: event.count,
36
+ bytes: event.bytes
37
+ });
38
+ return;
39
+ case "error":
40
+ logger.error(`comment op ${event.op} failed`, {
41
+ op: event.op,
42
+ outcome: "error",
43
+ err: event.err instanceof Error ? event.err.message : String(event.err)
44
+ });
45
+ return;
46
+ }
47
+ }
48
+
49
+ exports.logCommentOp = logCommentOp;
50
+ //# sourceMappingURL=logging.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logging.cjs.js","sources":["../../src/comments/logging.ts"],"sourcesContent":["import type { LoggerService } from \"@backstage/backend-plugin-api\";\n\n/**\n * `mutation` and `denied` are the only variants emitted today. `entity-not-visible`,\n * `oversized-list`, and `error` are defined ahead of their emitters as intentional\n * forward hooks — the switch in `logCommentOp` already handles them so adding an\n * emitter later requires no structural change here.\n */\nexport type CommentLogEvent =\n | { kind: \"mutation\"; op: string; siteRef: string; commentId: string; parentId?: string }\n | { kind: \"denied\"; op: string; permission: string; userEntityRef: string }\n | { kind: \"entity-not-visible\"; op: string; siteRef: string; userEntityRef: string }\n | { kind: \"oversized-list\"; siteRef: string; pageRef: string; count: number; bytes: number }\n | { kind: \"error\"; op: string; err: unknown };\n\n/** Single funnel for comment-op logging. PII (body/html/profile/selectors/tokens) is\n * not representable in CommentLogEvent, so it cannot be logged here. */\nexport function logCommentOp(logger: LoggerService, event: CommentLogEvent): void {\n switch (event.kind) {\n case \"mutation\":\n logger.info(`comment op ${event.op}`, {\n op: event.op,\n siteRef: event.siteRef,\n commentId: event.commentId,\n ...(event.parentId ? { parentId: event.parentId } : {}),\n outcome: \"ok\",\n });\n return;\n case \"denied\":\n logger.warn(`comment op ${event.op} denied`, {\n op: event.op,\n permission: event.permission,\n userEntityRef: event.userEntityRef,\n outcome: \"denied\",\n });\n return;\n case \"entity-not-visible\":\n logger.warn(`comment op ${event.op} entity not visible`, {\n op: event.op,\n siteRef: event.siteRef,\n userEntityRef: event.userEntityRef,\n outcome: \"entity-not-visible\",\n });\n return;\n case \"oversized-list\":\n logger.warn(\"comment list oversized\", {\n op: \"list\",\n siteRef: event.siteRef,\n pageRef: event.pageRef,\n count: event.count,\n bytes: event.bytes,\n });\n return;\n case \"error\":\n logger.error(`comment op ${event.op} failed`, {\n op: event.op,\n outcome: \"error\",\n err: event.err instanceof Error ? event.err.message : String(event.err),\n });\n return;\n default: {\n // Exhaustiveness check — TypeScript ensures this is unreachable.\n const _: never = event;\n void _;\n }\n }\n}\n"],"names":[],"mappings":";;AAiBO,SAAS,YAAA,CAAa,QAAuB,KAAA,EAA8B;AAChF,EAAA,QAAQ,MAAM,IAAA;AAAM,IAClB,KAAK,UAAA;AACH,MAAA,MAAA,CAAO,IAAA,CAAK,CAAA,WAAA,EAAc,KAAA,CAAM,EAAE,CAAA,CAAA,EAAI;AAAA,QACpC,IAAI,KAAA,CAAM,EAAA;AAAA,QACV,SAAS,KAAA,CAAM,OAAA;AAAA,QACf,WAAW,KAAA,CAAM,SAAA;AAAA,QACjB,GAAI,MAAM,QAAA,GAAW,EAAE,UAAU,KAAA,CAAM,QAAA,KAAa,EAAC;AAAA,QACrD,OAAA,EAAS;AAAA,OACV,CAAA;AACD,MAAA;AAAA,IACF,KAAK,QAAA;AACH,MAAA,MAAA,CAAO,IAAA,CAAK,CAAA,WAAA,EAAc,KAAA,CAAM,EAAE,CAAA,OAAA,CAAA,EAAW;AAAA,QAC3C,IAAI,KAAA,CAAM,EAAA;AAAA,QACV,YAAY,KAAA,CAAM,UAAA;AAAA,QAClB,eAAe,KAAA,CAAM,aAAA;AAAA,QACrB,OAAA,EAAS;AAAA,OACV,CAAA;AACD,MAAA;AAAA,IACF,KAAK,oBAAA;AACH,MAAA,MAAA,CAAO,IAAA,CAAK,CAAA,WAAA,EAAc,KAAA,CAAM,EAAE,CAAA,mBAAA,CAAA,EAAuB;AAAA,QACvD,IAAI,KAAA,CAAM,EAAA;AAAA,QACV,SAAS,KAAA,CAAM,OAAA;AAAA,QACf,eAAe,KAAA,CAAM,aAAA;AAAA,QACrB,OAAA,EAAS;AAAA,OACV,CAAA;AACD,MAAA;AAAA,IACF,KAAK,gBAAA;AACH,MAAA,MAAA,CAAO,KAAK,wBAAA,EAA0B;AAAA,QACpC,EAAA,EAAI,MAAA;AAAA,QACJ,SAAS,KAAA,CAAM,OAAA;AAAA,QACf,SAAS,KAAA,CAAM,OAAA;AAAA,QACf,OAAO,KAAA,CAAM,KAAA;AAAA,QACb,OAAO,KAAA,CAAM;AAAA,OACd,CAAA;AACD,MAAA;AAAA,IACF,KAAK,OAAA;AACH,MAAA,MAAA,CAAO,KAAA,CAAM,CAAA,WAAA,EAAc,KAAA,CAAM,EAAE,CAAA,OAAA,CAAA,EAAW;AAAA,QAC5C,IAAI,KAAA,CAAM,EAAA;AAAA,QACV,OAAA,EAAS,OAAA;AAAA,QACT,GAAA,EAAK,MAAM,GAAA,YAAe,KAAA,GAAQ,MAAM,GAAA,CAAI,OAAA,GAAU,MAAA,CAAO,KAAA,CAAM,GAAG;AAAA,OACvE,CAAA;AACD,MAAA;AAKF;AAEJ;;;;"}
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+
3
+ var timestamps = require('./timestamps.cjs.js');
4
+ var author = require('./author.cjs.js');
5
+
6
+ function toCommentResponse(row, callerRef) {
7
+ const isReply = row.parent_id !== null;
8
+ const deleted = row.deleted_at !== null;
9
+ const isAuthor = callerRef !== void 0 && callerRef === row.author_ref;
10
+ return {
11
+ id: row.id,
12
+ documentId: row.page_ref,
13
+ // viewer wire field — preserved until @rwdocs/viewer is updated
14
+ ...row.parent_id !== null ? { parentId: row.parent_id } : {},
15
+ author: author.authorFromRow(row),
16
+ body: row.body,
17
+ bodyHtml: row.body_html,
18
+ selectors: JSON.parse(row.selectors),
19
+ status: row.status,
20
+ createdAt: timestamps.toIso(row.created_at),
21
+ updatedAt: timestamps.toIso(row.updated_at),
22
+ ...deleted ? { deletedAt: timestamps.toIso(row.deleted_at) } : {},
23
+ canDelete: isReply && !deleted && isAuthor,
24
+ canRestore: deleted && isAuthor,
25
+ // Resolve is collaborative — any authenticated user may resolve/reopen a
26
+ // thread, unlike canDelete/canRestore which are author-only.
27
+ canResolve: !isReply && !deleted
28
+ };
29
+ }
30
+
31
+ exports.toCommentResponse = toCommentResponse;
32
+ //# sourceMappingURL=mapping.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mapping.cjs.js","sources":["../../src/comments/mapping.ts"],"sourcesContent":["import { CommentRow } from \"./types\";\nimport { toIso } from \"./timestamps\";\nimport { authorFromRow } from \"./author\";\n\nexport interface CommentResponse {\n id: string;\n documentId: string;\n parentId?: string;\n author: { id: string; name: string; avatarUrl?: string };\n body: string;\n bodyHtml: string;\n selectors: unknown[];\n status: \"open\" | \"resolved\";\n createdAt: string;\n updatedAt: string;\n deletedAt?: string;\n canDelete: boolean;\n canRestore: boolean;\n canResolve: boolean;\n}\n\nexport function toCommentResponse(row: CommentRow, callerRef: string | undefined): CommentResponse {\n const isReply = row.parent_id !== null;\n const deleted = row.deleted_at !== null;\n const isAuthor = callerRef !== undefined && callerRef === row.author_ref;\n\n return {\n id: row.id,\n documentId: row.page_ref, // viewer wire field — preserved until @rwdocs/viewer is updated\n ...(row.parent_id !== null ? { parentId: row.parent_id } : {}),\n author: authorFromRow(row),\n body: row.body,\n bodyHtml: row.body_html,\n selectors: JSON.parse(row.selectors),\n status: row.status,\n createdAt: toIso(row.created_at)!,\n updatedAt: toIso(row.updated_at)!,\n ...(deleted ? { deletedAt: toIso(row.deleted_at) } : {}),\n canDelete: isReply && !deleted && isAuthor,\n canRestore: deleted && isAuthor,\n // Resolve is collaborative — any authenticated user may resolve/reopen a\n // thread, unlike canDelete/canRestore which are author-only.\n canResolve: !isReply && !deleted,\n };\n}\n"],"names":["authorFromRow","toIso"],"mappings":";;;;;AAqBO,SAAS,iBAAA,CAAkB,KAAiB,SAAA,EAAgD;AACjG,EAAA,MAAM,OAAA,GAAU,IAAI,SAAA,KAAc,IAAA;AAClC,EAAA,MAAM,OAAA,GAAU,IAAI,UAAA,KAAe,IAAA;AACnC,EAAA,MAAM,QAAA,GAAW,SAAA,KAAc,MAAA,IAAa,SAAA,KAAc,GAAA,CAAI,UAAA;AAE9D,EAAA,OAAO;AAAA,IACL,IAAI,GAAA,CAAI,EAAA;AAAA,IACR,YAAY,GAAA,CAAI,QAAA;AAAA;AAAA,IAChB,GAAI,IAAI,SAAA,KAAc,IAAA,GAAO,EAAE,QAAA,EAAU,GAAA,CAAI,SAAA,EAAU,GAAI,EAAC;AAAA,IAC5D,MAAA,EAAQA,qBAAc,GAAG,CAAA;AAAA,IACzB,MAAM,GAAA,CAAI,IAAA;AAAA,IACV,UAAU,GAAA,CAAI,SAAA;AAAA,IACd,SAAA,EAAW,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,SAAS,CAAA;AAAA,IACnC,QAAQ,GAAA,CAAI,MAAA;AAAA,IACZ,SAAA,EAAWC,gBAAA,CAAM,GAAA,CAAI,UAAU,CAAA;AAAA,IAC/B,SAAA,EAAWA,gBAAA,CAAM,GAAA,CAAI,UAAU,CAAA;AAAA,IAC/B,GAAI,UAAU,EAAE,SAAA,EAAWA,iBAAM,GAAA,CAAI,UAAU,CAAA,EAAE,GAAI,EAAC;AAAA,IACtD,SAAA,EAAW,OAAA,IAAW,CAAC,OAAA,IAAW,QAAA;AAAA,IAClC,YAAY,OAAA,IAAW,QAAA;AAAA;AAAA;AAAA,IAGvB,UAAA,EAAY,CAAC,OAAA,IAAW,CAAC;AAAA,GAC3B;AACF;;;;"}
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ var v3 = require('zod/v3');
4
+ var pluginPermissionNode = require('@backstage/plugin-permission-node');
5
+ var backstagePluginRwCommon = require('@rwdocs/backstage-plugin-rw-common');
6
+
7
+ const commentResourceRef = pluginPermissionNode.createPermissionResourceRef().with({
8
+ pluginId: "rw",
9
+ resourceType: backstagePluginRwCommon.RESOURCE_TYPE_RW_COMMENT
10
+ });
11
+ const isCommentAuthor = pluginPermissionNode.createPermissionRule(
12
+ {
13
+ name: "IS_COMMENT_AUTHOR",
14
+ description: "Allow only the comment author",
15
+ resourceRef: commentResourceRef,
16
+ paramsSchema: v3.z.object({ userRef: v3.z.string() }),
17
+ apply: (comment, { userRef }) => comment.author.id === userRef,
18
+ toQuery: () => ({})
19
+ }
20
+ );
21
+
22
+ exports.commentResourceRef = commentResourceRef;
23
+ exports.isCommentAuthor = isCommentAuthor;
24
+ //# sourceMappingURL=permissions.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permissions.cjs.js","sources":["../../src/comments/permissions.ts"],"sourcesContent":["import { z } from \"zod/v3\";\nimport {\n createPermissionResourceRef,\n createPermissionRule,\n} from \"@backstage/plugin-permission-node\";\nimport { RESOURCE_TYPE_RW_COMMENT } from \"@rwdocs/backstage-plugin-rw-common\";\nimport type { CommentResponse } from \"./mapping\";\n\n/**\n * Resource ref for RW comments. TQuery is {} because all authorization\n * is applied in-memory (no DB query translation is needed).\n */\nexport const commentResourceRef = createPermissionResourceRef<CommentResponse, {}>().with({\n pluginId: \"rw\",\n resourceType: RESOURCE_TYPE_RW_COMMENT,\n});\n\n/**\n * Permission rule that returns true when the caller's userRef matches the comment author.\n * The caller ref is passed as a `userRef` param for testability.\n */\nexport const isCommentAuthor = createPermissionRule<typeof commentResourceRef, { userRef: string }>(\n {\n name: \"IS_COMMENT_AUTHOR\",\n description: \"Allow only the comment author\",\n resourceRef: commentResourceRef,\n paramsSchema: z.object({ userRef: z.string() }),\n apply: (comment, { userRef }) => comment.author.id === userRef,\n toQuery: () => ({}),\n },\n);\n"],"names":["createPermissionResourceRef","RESOURCE_TYPE_RW_COMMENT","createPermissionRule","z"],"mappings":";;;;;;AAYO,MAAM,kBAAA,GAAqBA,gDAAA,EAAiD,CAAE,IAAA,CAAK;AAAA,EACxF,QAAA,EAAU,IAAA;AAAA,EACV,YAAA,EAAcC;AAChB,CAAC;AAMM,MAAM,eAAA,GAAkBC,yCAAA;AAAA,EAC7B;AAAA,IACE,IAAA,EAAM,mBAAA;AAAA,IACN,WAAA,EAAa,+BAAA;AAAA,IACb,WAAA,EAAa,kBAAA;AAAA,IACb,YAAA,EAAcC,KAAE,MAAA,CAAO,EAAE,SAASA,IAAA,CAAE,MAAA,IAAU,CAAA;AAAA,IAC9C,KAAA,EAAO,CAAC,OAAA,EAAS,EAAE,SAAQ,KAAM,OAAA,CAAQ,OAAO,EAAA,KAAO,OAAA;AAAA,IACvD,OAAA,EAAS,OAAO,EAAC;AAAA;AAErB;;;;;"}