@rwdocs/backstage-plugin-rw-backend 0.1.5 → 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.
Files changed (54) hide show
  1. package/dist/comments/CommentEventPublisher.cjs.js +117 -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/inbox/InboxStore.cjs.js +96 -0
  20. package/dist/inbox/InboxStore.cjs.js.map +1 -0
  21. package/dist/inbox/cursor.cjs.js +35 -0
  22. package/dist/inbox/cursor.cjs.js.map +1 -0
  23. package/dist/inbox/inboxRouter.cjs.js +90 -0
  24. package/dist/inbox/inboxRouter.cjs.js.map +1 -0
  25. package/dist/inbox/mapping.cjs.js +35 -0
  26. package/dist/inbox/mapping.cjs.js.map +1 -0
  27. package/dist/inbox/snippet.cjs.js +44 -0
  28. package/dist/inbox/snippet.cjs.js.map +1 -0
  29. package/dist/plugin.cjs.js +121 -4
  30. package/dist/plugin.cjs.js.map +1 -1
  31. package/dist/router.cjs.js +4 -3
  32. package/dist/router.cjs.js.map +1 -1
  33. package/dist/siteIndex/PagesReader.cjs.js +15 -0
  34. package/dist/siteIndex/PagesReader.cjs.js.map +1 -0
  35. package/dist/siteIndex/RegistryStore.cjs.js +20 -0
  36. package/dist/siteIndex/RegistryStore.cjs.js.map +1 -0
  37. package/dist/siteIndex/SectionOwnershipStore.cjs.js +21 -0
  38. package/dist/siteIndex/SectionOwnershipStore.cjs.js.map +1 -0
  39. package/dist/siteIndex/SectionsReader.cjs.js +14 -0
  40. package/dist/siteIndex/SectionsReader.cjs.js.map +1 -0
  41. package/dist/siteIndex/SiteRefreshStore.cjs.js +73 -0
  42. package/dist/siteIndex/SiteRefreshStore.cjs.js.map +1 -0
  43. package/dist/siteIndex/effectiveOwnership.cjs.js +34 -0
  44. package/dist/siteIndex/effectiveOwnership.cjs.js.map +1 -0
  45. package/dist/siteIndex/registryHash.cjs.js +10 -0
  46. package/dist/siteIndex/registryHash.cjs.js.map +1 -0
  47. package/dist/siteIndex/runScan.cjs.js +75 -0
  48. package/dist/siteIndex/runScan.cjs.js.map +1 -0
  49. package/dist/siteIndex/runWorker.cjs.js +81 -0
  50. package/dist/siteIndex/runWorker.cjs.js.map +1 -0
  51. package/dist/siteIndex/schedule.cjs.js +25 -0
  52. package/dist/siteIndex/schedule.cjs.js.map +1 -0
  53. package/migrations/20260621000000_init.js +81 -0
  54. package/package.json +14 -7
@@ -0,0 +1,90 @@
1
+ 'use strict';
2
+
3
+ var PromiseRouter = require('express-promise-router');
4
+ var pluginPermissionCommon = require('@backstage/plugin-permission-common');
5
+ var backstagePluginRwCommon = require('@rwdocs/backstage-plugin-rw-common');
6
+ var mapping = require('./mapping.cjs.js');
7
+ var cursor = require('./cursor.cjs.js');
8
+
9
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
10
+
11
+ var PromiseRouter__default = /*#__PURE__*/_interopDefaultCompat(PromiseRouter);
12
+
13
+ const DEFAULT_LIMIT = 50;
14
+ const MAX_LIMIT = 100;
15
+ function parseLimit(raw) {
16
+ const n = Number(raw);
17
+ if (!Number.isFinite(n) || n <= 0) return DEFAULT_LIMIT;
18
+ return Math.min(Math.floor(n), MAX_LIMIT);
19
+ }
20
+ function createInboxRouter(deps) {
21
+ const { httpAuth, permissions, userInfo, store, commentStore, siteRefreshStore } = deps;
22
+ const router = PromiseRouter__default.default();
23
+ router.get("/comments/inbox", async (req, res) => {
24
+ const credentials = await httpAuth.credentials(req, { allow: ["user"] });
25
+ const decision = await permissions.authorize([{ permission: backstagePluginRwCommon.rwCommentReadPermission }], {
26
+ credentials
27
+ });
28
+ if (decision[0].result !== pluginPermissionCommon.AuthorizeResult.ALLOW) {
29
+ res.status(403).end();
30
+ return;
31
+ }
32
+ const { ownershipEntityRefs } = await userInfo.getUserInfo(credentials);
33
+ const limit = parseLimit(req.query.limit);
34
+ const cursorParam = typeof req.query.cursor === "string" ? req.query.cursor : void 0;
35
+ let filter;
36
+ let sort;
37
+ let lastKey;
38
+ let openCount;
39
+ let unansweredCount;
40
+ let built;
41
+ if (cursorParam) {
42
+ const cursor$1 = cursor.decodeCursor(cursorParam);
43
+ ({ filter, sort } = cursor$1);
44
+ lastKey = cursor$1.lastKey;
45
+ openCount = cursor$1.openCount;
46
+ unansweredCount = cursor$1.unansweredCount;
47
+ built = await siteRefreshStore.anyBuilt();
48
+ } else {
49
+ filter = req.query.filter === "unanswered" ? "unanswered" : "open";
50
+ sort = req.query.sort === "oldest" ? "oldest" : "newest";
51
+ const [b, c] = await Promise.all([
52
+ siteRefreshStore.anyBuilt(),
53
+ store.counts(ownershipEntityRefs)
54
+ ]);
55
+ built = b;
56
+ openCount = c.openCount;
57
+ unansweredCount = c.unansweredCount;
58
+ }
59
+ const page = await store.ownedOpenThreadsPage(ownershipEntityRefs, {
60
+ filter,
61
+ sort,
62
+ lastKey,
63
+ limit
64
+ });
65
+ const replyCountMap = await commentStore.replyCountsFor(page.rows.map((r) => r.id));
66
+ const items = page.rows.map((r) => mapping.toInboxItem(r, replyCountMap.get(r.id) ?? 0));
67
+ let nextCursor;
68
+ if (page.hasMore && page.rows.length > 0) {
69
+ const last = page.rows[page.rows.length - 1];
70
+ nextCursor = cursor.encodeCursor({
71
+ filter,
72
+ sort,
73
+ lastKey: [mapping.rawSortValue(last.updated_at), last.id],
74
+ openCount,
75
+ unansweredCount
76
+ });
77
+ }
78
+ res.json({
79
+ built,
80
+ items,
81
+ pageInfo: nextCursor ? { nextCursor } : {},
82
+ openCount,
83
+ unansweredCount
84
+ });
85
+ });
86
+ return router;
87
+ }
88
+
89
+ exports.createInboxRouter = createInboxRouter;
90
+ //# sourceMappingURL=inboxRouter.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inboxRouter.cjs.js","sources":["../../src/inbox/inboxRouter.ts"],"sourcesContent":["import type { Router } from \"express\";\nimport PromiseRouter from \"express-promise-router\";\nimport type {\n HttpAuthService,\n PermissionsService,\n UserInfoService,\n} from \"@backstage/backend-plugin-api\";\nimport { AuthorizeResult } from \"@backstage/plugin-permission-common\";\nimport { rwCommentReadPermission } from \"@rwdocs/backstage-plugin-rw-common\";\nimport type { CommentStore } from \"../comments/CommentStore\";\nimport type { SiteRefreshStore } from \"../siteIndex/SiteRefreshStore\";\nimport type { InboxStore } from \"./InboxStore\";\nimport { toInboxItem, rawSortValue } from \"./mapping\";\nimport { encodeCursor, decodeCursor } from \"./cursor\";\n\nexport interface InboxRouterDeps {\n httpAuth: HttpAuthService;\n permissions: PermissionsService;\n userInfo: UserInfoService;\n store: InboxStore;\n commentStore: CommentStore;\n siteRefreshStore: Pick<SiteRefreshStore, \"anyBuilt\">;\n}\n\nconst DEFAULT_LIMIT = 50;\nconst MAX_LIMIT = 100;\n\nfunction parseLimit(raw: unknown): number {\n const n = Number(raw);\n if (!Number.isFinite(n) || n <= 0) return DEFAULT_LIMIT;\n return Math.min(Math.floor(n), MAX_LIMIT);\n}\n\nexport function createInboxRouter(deps: InboxRouterDeps): Router {\n const { httpAuth, permissions, userInfo, store, commentStore, siteRefreshStore } = deps;\n const router = PromiseRouter();\n\n router.get(\"/comments/inbox\", async (req, res) => {\n const credentials = await httpAuth.credentials(req, { allow: [\"user\"] });\n const decision = await permissions.authorize([{ permission: rwCommentReadPermission }], {\n credentials,\n });\n if (decision[0].result !== AuthorizeResult.ALLOW) {\n res.status(403).end();\n return;\n }\n const { ownershipEntityRefs } = await userInfo.getUserInfo(credentials);\n const limit = parseLimit(req.query.limit);\n\n // Discriminate on cursor presence (mirrors catalog queryEntities). The cursor\n // carries filter/sort + memoised counts; the initial request reads them from\n // the query and computes the counts once. Subsequent pages reuse the counts\n // from the cursor — intentionally stale across a paginated fetch (saves a\n // count query per scroll; matches catalog queryEntities totalItems behaviour).\n const cursorParam = typeof req.query.cursor === \"string\" ? req.query.cursor : undefined;\n\n let filter: \"open\" | \"unanswered\";\n let sort: \"newest\" | \"oldest\";\n let lastKey: [string | number, string] | undefined;\n let openCount: number;\n let unansweredCount: number;\n let built: boolean;\n\n if (cursorParam) {\n const cursor = decodeCursor(cursorParam); // throws InputError → 400\n ({ filter, sort } = cursor);\n lastKey = cursor.lastKey;\n openCount = cursor.openCount;\n unansweredCount = cursor.unansweredCount;\n built = await siteRefreshStore.anyBuilt();\n } else {\n filter = req.query.filter === \"unanswered\" ? \"unanswered\" : \"open\";\n sort = req.query.sort === \"oldest\" ? \"oldest\" : \"newest\";\n const [b, c] = await Promise.all([\n siteRefreshStore.anyBuilt(),\n store.counts(ownershipEntityRefs),\n ]);\n built = b;\n openCount = c.openCount;\n unansweredCount = c.unansweredCount;\n }\n\n const page = await store.ownedOpenThreadsPage(ownershipEntityRefs, {\n filter,\n sort,\n lastKey,\n limit,\n });\n const replyCountMap = await commentStore.replyCountsFor(page.rows.map((r) => r.id));\n const items = page.rows.map((r) => toInboxItem(r, replyCountMap.get(r.id) ?? 0));\n\n let nextCursor: string | undefined;\n if (page.hasMore && page.rows.length > 0) {\n const last = page.rows[page.rows.length - 1];\n nextCursor = encodeCursor({\n filter,\n sort,\n lastKey: [rawSortValue(last.updated_at), last.id],\n openCount,\n unansweredCount,\n });\n }\n\n res.json({\n built,\n items,\n pageInfo: nextCursor ? { nextCursor } : {},\n openCount,\n unansweredCount,\n });\n });\n\n return router;\n}\n"],"names":["PromiseRouter","rwCommentReadPermission","AuthorizeResult","cursor","decodeCursor","toInboxItem","encodeCursor","rawSortValue"],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ var timestamps = require('../comments/timestamps.cjs.js');
4
+ var types = require('../comments/types.cjs.js');
5
+ var author = require('../comments/author.cjs.js');
6
+ var snippet = require('./snippet.cjs.js');
7
+
8
+ function joinNonEmpty(parts, sep) {
9
+ return parts.filter(Boolean).join(sep);
10
+ }
11
+ function rawSortValue(updatedAt) {
12
+ if (updatedAt instanceof Date) return updatedAt.toISOString();
13
+ return updatedAt;
14
+ }
15
+ function toInboxItem(row, replyCount) {
16
+ const viewerPath = joinNonEmpty([row.section_path, types.subpathOf(row.page_ref)], "/");
17
+ return {
18
+ commentId: row.id,
19
+ siteRef: row.site_ref,
20
+ pageRef: row.page_ref,
21
+ entityRef: row.entity_ref,
22
+ viewerPath,
23
+ pageTitle: row.page_title ?? viewerPath,
24
+ author: author.authorFromRow(row),
25
+ bodySnippet: snippet.snippetFromHtml(row.body_html),
26
+ createdAt: timestamps.toIso(row.created_at),
27
+ updatedAt: timestamps.toIso(row.updated_at),
28
+ replyCount
29
+ };
30
+ }
31
+
32
+ exports.joinNonEmpty = joinNonEmpty;
33
+ exports.rawSortValue = rawSortValue;
34
+ exports.toInboxItem = toInboxItem;
35
+ //# sourceMappingURL=mapping.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mapping.cjs.js","sources":["../../src/inbox/mapping.ts"],"sourcesContent":["export type { InboxItem, InboxResponse } from \"@rwdocs/backstage-plugin-rw-common\";\nimport type { InboxItem } from \"@rwdocs/backstage-plugin-rw-common\";\nimport { toIso } from \"../comments/timestamps\";\nimport { subpathOf } from \"../comments/types\";\nimport { authorFromRow } from \"../comments/author\";\nimport { snippetFromHtml } from \"./snippet\";\nimport type { OwnedThreadRow } from \"./InboxStore\";\n\nexport function joinNonEmpty(parts: string[], sep: string): string {\n return parts.filter(Boolean).join(sep);\n}\n\n/** The keyset sort value bound back into the seek. updated_at reads back as a\n * number (better-sqlite3), a string (sqlite3/pg-ISO), or a Date (pg's knex\n * driver returns Date objects for timestamp columns, unlike better-sqlite3\n * which returns a number). A Date is normalised to ISO so the cursor JSON\n * round-trip and the SQL seek comparison are both correct. Numbers/strings\n * pass through unchanged. */\nexport function rawSortValue(updatedAt: Date | string | number): string | number {\n if (updatedAt instanceof Date) return updatedAt.toISOString();\n return updatedAt;\n}\n\nexport function toInboxItem(row: OwnedThreadRow, replyCount: number): InboxItem {\n const viewerPath = joinNonEmpty([row.section_path, subpathOf(row.page_ref)], \"/\");\n return {\n commentId: row.id,\n siteRef: row.site_ref,\n pageRef: row.page_ref,\n entityRef: row.entity_ref,\n viewerPath,\n pageTitle: row.page_title ?? viewerPath,\n author: authorFromRow(row),\n bodySnippet: snippetFromHtml(row.body_html),\n createdAt: toIso(row.created_at)!,\n updatedAt: toIso(row.updated_at)!,\n replyCount,\n };\n}\n"],"names":["subpathOf","authorFromRow","snippetFromHtml","toIso"],"mappings":";;;;;;;AAQO,SAAS,YAAA,CAAa,OAAiB,GAAA,EAAqB;AACjE,EAAA,OAAO,KAAA,CAAM,MAAA,CAAO,OAAO,CAAA,CAAE,KAAK,GAAG,CAAA;AACvC;AAQO,SAAS,aAAa,SAAA,EAAoD;AAC/E,EAAA,IAAI,SAAA,YAAqB,IAAA,EAAM,OAAO,SAAA,CAAU,WAAA,EAAY;AAC5D,EAAA,OAAO,SAAA;AACT;AAEO,SAAS,WAAA,CAAY,KAAqB,UAAA,EAA+B;AAC9E,EAAA,MAAM,UAAA,GAAa,YAAA,CAAa,CAAC,GAAA,CAAI,YAAA,EAAcA,gBAAU,GAAA,CAAI,QAAQ,CAAC,CAAA,EAAG,GAAG,CAAA;AAChF,EAAA,OAAO;AAAA,IACL,WAAW,GAAA,CAAI,EAAA;AAAA,IACf,SAAS,GAAA,CAAI,QAAA;AAAA,IACb,SAAS,GAAA,CAAI,QAAA;AAAA,IACb,WAAW,GAAA,CAAI,UAAA;AAAA,IACf,UAAA;AAAA,IACA,SAAA,EAAW,IAAI,UAAA,IAAc,UAAA;AAAA,IAC7B,MAAA,EAAQC,qBAAc,GAAG,CAAA;AAAA,IACzB,WAAA,EAAaC,uBAAA,CAAgB,GAAA,CAAI,SAAS,CAAA;AAAA,IAC1C,SAAA,EAAWC,gBAAA,CAAM,GAAA,CAAI,UAAU,CAAA;AAAA,IAC/B,SAAA,EAAWA,gBAAA,CAAM,GAAA,CAAI,UAAU,CAAA;AAAA,IAC/B;AAAA,GACF;AACF;;;;;;"}
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ const SNIPPET_MAX = 200;
4
+ const NAMED_ENTITIES = {
5
+ amp: "&",
6
+ lt: "<",
7
+ gt: ">",
8
+ quot: '"',
9
+ apos: "'",
10
+ nbsp: " "
11
+ };
12
+ function stripTags(html) {
13
+ const blockTags = /(<\/?(?:p|div|blockquote|h[1-6]|ul|ol|li|pre|table|tr|td|th|thead|tbody|tfoot)[^>]*>)/gi;
14
+ return html.replace(blockTags, " ").replace(/<[^>]+>/g, "");
15
+ }
16
+ function decodeEntities(s) {
17
+ return s.replace(/&(#x[0-9a-f]+|#[0-9]+|[a-z]+);/gi, (match, code) => {
18
+ if (code[0] === "#") {
19
+ const cp = code[1] === "x" || code[1] === "X" ? parseInt(code.slice(2), 16) : parseInt(code.slice(1), 10);
20
+ return Number.isFinite(cp) && cp >= 1 && cp <= 1114111 ? String.fromCodePoint(cp) : match;
21
+ }
22
+ return NAMED_ENTITIES[code.toLowerCase()] ?? match;
23
+ });
24
+ }
25
+ function collapseWhitespace(s) {
26
+ return s.replace(/\s+/g, " ").trim();
27
+ }
28
+ const graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
29
+ function truncateGraphemes(text, max) {
30
+ const graphemes = [];
31
+ for (const { segment } of graphemeSegmenter.segment(text)) {
32
+ graphemes.push(segment);
33
+ if (graphemes.length > max) break;
34
+ }
35
+ if (graphemes.length <= max) return text;
36
+ return `${graphemes.slice(0, max).join("")}\u2026`;
37
+ }
38
+ function snippetFromHtml(html, max = SNIPPET_MAX) {
39
+ const text = collapseWhitespace(decodeEntities(stripTags(html)));
40
+ return truncateGraphemes(text, max);
41
+ }
42
+
43
+ exports.snippetFromHtml = snippetFromHtml;
44
+ //# sourceMappingURL=snippet.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snippet.cjs.js","sources":["../../src/inbox/snippet.ts"],"sourcesContent":["const SNIPPET_MAX = 200;\n\n/** Named entities the comment sanitizer (renderCommentBody) emits. Numeric\n * entities are decoded separately. */\nconst NAMED_ENTITIES: Record<string, string> = {\n amp: \"&\",\n lt: \"<\",\n gt: \">\",\n quot: '\"',\n apos: \"'\",\n nbsp: \" \",\n};\n\n/** Replace block tags with a space so block boundaries don't glue words together\n * (`<p>a</p><p>b</p>` → `a b`, not `ab`); inline tags are removed *without* a\n * space so inline formatting next to punctuation stays tight (`<strong>TLS</strong>?`\n * → `TLS?`, not `TLS ?` — collapseWhitespace can't undo a space before punctuation).\n * Whitespace is collapsed afterwards. */\nfunction stripTags(html: string): string {\n const blockTags =\n /(<\\/?(?:p|div|blockquote|h[1-6]|ul|ol|li|pre|table|tr|td|th|thead|tbody|tfoot)[^>]*>)/gi;\n return html.replace(blockTags, \" \").replace(/<[^>]+>/g, \"\");\n}\n\n/** Decode the `NAMED_ENTITIES` set plus numeric `&#NN;` / `&#xNN;`. Runs after tag\n * stripping so a decoded `<` can't masquerade as a tag. Unknown entities and\n * out-of-range code points are left verbatim. */\nfunction decodeEntities(s: string): string {\n return s.replace(/&(#x[0-9a-f]+|#[0-9]+|[a-z]+);/gi, (match, code: string) => {\n if (code[0] === \"#\") {\n const cp =\n code[1] === \"x\" || code[1] === \"X\"\n ? parseInt(code.slice(2), 16)\n : parseInt(code.slice(1), 10);\n return Number.isFinite(cp) && cp >= 1 && cp <= 0x10ffff ? String.fromCodePoint(cp) : match;\n }\n return NAMED_ENTITIES[code.toLowerCase()] ?? match;\n });\n}\n\nfunction collapseWhitespace(s: string): string {\n return s.replace(/\\s+/g, \" \").trim();\n}\n\nconst graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: \"grapheme\" });\n\n/** Truncate to at most `max` graphemes (never mid-grapheme), appending an\n * ellipsis only when the text was actually longer. */\nfunction truncateGraphemes(text: string, max: number): string {\n const graphemes: string[] = [];\n for (const { segment } of graphemeSegmenter.segment(text)) {\n graphemes.push(segment);\n if (graphemes.length > max) break; // one past the limit is enough to know it's truncated\n }\n if (graphemes.length <= max) return text;\n return `${graphemes.slice(0, max).join(\"\")}…`;\n}\n\n/** Derive a clean one-line preview from sanitized comment HTML: strip markup,\n * decode entities, collapse whitespace, grapheme-safe truncate. */\nexport function snippetFromHtml(html: string, max = SNIPPET_MAX): string {\n const text = collapseWhitespace(decodeEntities(stripTags(html)));\n return truncateGraphemes(text, max);\n}\n"],"names":[],"mappings":";;AAAA,MAAM,WAAA,GAAc,GAAA;AAIpB,MAAM,cAAA,GAAyC;AAAA,EAC7C,GAAA,EAAK,GAAA;AAAA,EACL,EAAA,EAAI,GAAA;AAAA,EACJ,EAAA,EAAI,GAAA;AAAA,EACJ,IAAA,EAAM,GAAA;AAAA,EACN,IAAA,EAAM,GAAA;AAAA,EACN,IAAA,EAAM;AACR,CAAA;AAOA,SAAS,UAAU,IAAA,EAAsB;AACvC,EAAA,MAAM,SAAA,GACJ,yFAAA;AACF,EAAA,OAAO,KAAK,OAAA,CAAQ,SAAA,EAAW,GAAG,CAAA,CAAE,OAAA,CAAQ,YAAY,EAAE,CAAA;AAC5D;AAKA,SAAS,eAAe,CAAA,EAAmB;AACzC,EAAA,OAAO,CAAA,CAAE,OAAA,CAAQ,kCAAA,EAAoC,CAAC,OAAO,IAAA,KAAiB;AAC5E,IAAA,IAAI,IAAA,CAAK,CAAC,CAAA,KAAM,GAAA,EAAK;AACnB,MAAA,MAAM,EAAA,GACJ,KAAK,CAAC,CAAA,KAAM,OAAO,IAAA,CAAK,CAAC,MAAM,GAAA,GAC3B,QAAA,CAAS,KAAK,KAAA,CAAM,CAAC,GAAG,EAAE,CAAA,GAC1B,SAAS,IAAA,CAAK,KAAA,CAAM,CAAC,CAAA,EAAG,EAAE,CAAA;AAChC,MAAA,OAAO,MAAA,CAAO,QAAA,CAAS,EAAE,CAAA,IAAK,EAAA,IAAM,CAAA,IAAK,EAAA,IAAM,OAAA,GAAW,MAAA,CAAO,aAAA,CAAc,EAAE,CAAA,GAAI,KAAA;AAAA,IACvF;AACA,IAAA,OAAO,cAAA,CAAe,IAAA,CAAK,WAAA,EAAa,CAAA,IAAK,KAAA;AAAA,EAC/C,CAAC,CAAA;AACH;AAEA,SAAS,mBAAmB,CAAA,EAAmB;AAC7C,EAAA,OAAO,CAAA,CAAE,OAAA,CAAQ,MAAA,EAAQ,GAAG,EAAE,IAAA,EAAK;AACrC;AAEA,MAAM,iBAAA,GAAoB,IAAI,IAAA,CAAK,SAAA,CAAU,QAAW,EAAE,WAAA,EAAa,YAAY,CAAA;AAInF,SAAS,iBAAA,CAAkB,MAAc,GAAA,EAAqB;AAC5D,EAAA,MAAM,YAAsB,EAAC;AAC7B,EAAA,KAAA,MAAW,EAAE,OAAA,EAAQ,IAAK,iBAAA,CAAkB,OAAA,CAAQ,IAAI,CAAA,EAAG;AACzD,IAAA,SAAA,CAAU,KAAK,OAAO,CAAA;AACtB,IAAA,IAAI,SAAA,CAAU,SAAS,GAAA,EAAK;AAAA,EAC9B;AACA,EAAA,IAAI,SAAA,CAAU,MAAA,IAAU,GAAA,EAAK,OAAO,IAAA;AACpC,EAAA,OAAO,CAAA,EAAG,UAAU,KAAA,CAAM,CAAA,EAAG,GAAG,CAAA,CAAE,IAAA,CAAK,EAAE,CAAC,CAAA,MAAA,CAAA;AAC5C;AAIO,SAAS,eAAA,CAAgB,IAAA,EAAc,GAAA,GAAM,WAAA,EAAqB;AACvE,EAAA,MAAM,OAAO,kBAAA,CAAmB,cAAA,CAAe,SAAA,CAAU,IAAI,CAAC,CAAC,CAAA;AAC/D,EAAA,OAAO,iBAAA,CAAkB,MAAM,GAAG,CAAA;AACpC;;;;"}
@@ -1,10 +1,27 @@
1
1
  'use strict';
2
2
 
3
3
  var backendPluginApi = require('@backstage/backend-plugin-api');
4
+ var SectionOwnershipStore = require('./siteIndex/SectionOwnershipStore.cjs.js');
5
+ var RegistryStore = require('./siteIndex/RegistryStore.cjs.js');
6
+ var SiteRefreshStore = require('./siteIndex/SiteRefreshStore.cjs.js');
7
+ var runScan = require('./siteIndex/runScan.cjs.js');
8
+ var runWorker = require('./siteIndex/runWorker.cjs.js');
9
+ var schedule = require('./siteIndex/schedule.cjs.js');
4
10
  var config = require('@backstage/config');
5
11
  var backstagePluginRwCommon = require('@rwdocs/backstage-plugin-rw-common');
12
+ var pluginCatalogNode = require('@backstage/plugin-catalog-node');
13
+ var pluginEventsNode = require('@backstage/plugin-events-node');
6
14
  var router = require('./router.cjs.js');
7
15
  var hub = require('./hub.cjs.js');
16
+ var CommentStore = require('./comments/CommentStore.cjs.js');
17
+ var router$1 = require('./comments/router.cjs.js');
18
+ var CommentEventPublisher = require('./comments/CommentEventPublisher.cjs.js');
19
+ var SectionsReader = require('./siteIndex/SectionsReader.cjs.js');
20
+ var PagesReader = require('./siteIndex/PagesReader.cjs.js');
21
+ var permissions = require('./comments/permissions.cjs.js');
22
+ var mapping = require('./comments/mapping.cjs.js');
23
+ var InboxStore = require('./inbox/InboxStore.cjs.js');
24
+ var inboxRouter = require('./inbox/inboxRouter.cjs.js');
8
25
 
9
26
  const rwPlugin = backendPluginApi.createBackendPlugin({
10
27
  pluginId: "rw",
@@ -15,9 +32,29 @@ const rwPlugin = backendPluginApi.createBackendPlugin({
15
32
  httpAuth: backendPluginApi.coreServices.httpAuth,
16
33
  logger: backendPluginApi.coreServices.logger,
17
34
  config: backendPluginApi.coreServices.rootConfig,
18
- scheduler: backendPluginApi.coreServices.scheduler
35
+ scheduler: backendPluginApi.coreServices.scheduler,
36
+ database: backendPluginApi.coreServices.database,
37
+ permissions: backendPluginApi.coreServices.permissions,
38
+ permissionsRegistry: backendPluginApi.coreServices.permissionsRegistry,
39
+ userInfo: backendPluginApi.coreServices.userInfo,
40
+ auth: backendPluginApi.coreServices.auth,
41
+ catalog: pluginCatalogNode.catalogServiceRef,
42
+ events: pluginEventsNode.eventsServiceRef
19
43
  },
20
- async init({ httpRouter, httpAuth, logger, config: config$1, scheduler }) {
44
+ async init({
45
+ httpRouter,
46
+ httpAuth,
47
+ logger,
48
+ config: config$1,
49
+ scheduler,
50
+ database,
51
+ permissions: permissions$1,
52
+ permissionsRegistry,
53
+ userInfo,
54
+ auth,
55
+ catalog,
56
+ events
57
+ }) {
21
58
  const siteConfig = backstagePluginRwCommon.readRwSiteConfig(config$1);
22
59
  const cacheSize = config$1.getOptionalNumber("rw.cacheSize");
23
60
  const hubOptions = {
@@ -45,8 +82,88 @@ const rwPlugin = backendPluginApi.createBackendPlugin({
45
82
  fn: async () => hub$1.reloadAll(logger)
46
83
  });
47
84
  }
48
- const router$1 = await router.createRouter({ hub: hub$1 });
49
- httpRouter.use(router$1);
85
+ const client = await database.getClient();
86
+ if (!database.migrations?.skip) {
87
+ await client.migrate.latest({
88
+ directory: backendPluginApi.resolvePackagePath("@rwdocs/backstage-plugin-rw-backend", "migrations")
89
+ });
90
+ }
91
+ const store = new CommentStore.CommentStore(client);
92
+ const inboxStore = new InboxStore.InboxStore(client);
93
+ const sectionOwnershipStore = new SectionOwnershipStore.SectionOwnershipStore(client);
94
+ const registryStore = new RegistryStore.RegistryStore(client);
95
+ const siteRefreshStore = new SiteRefreshStore.SiteRefreshStore(client);
96
+ const sectionsReader = new SectionsReader.SectionsReader(client);
97
+ const pagesReader = new PagesReader.PagesReader(client);
98
+ const publisher = new CommentEventPublisher.CommentEventPublisher({
99
+ events,
100
+ sections: sectionsReader,
101
+ comments: store,
102
+ logger,
103
+ pages: pagesReader
104
+ });
105
+ const makeSite = schedule.makeSiteFactory(siteConfig);
106
+ const scanSchedule = config$1.has("rw.siteIndex.schedule") ? backendPluginApi.readSchedulerServiceTaskScheduleDefinitionFromConfig(
107
+ config$1.getConfig("rw.siteIndex.schedule")
108
+ ) : { frequency: { minutes: 15 }, timeout: { minutes: 10 }, initialDelay: { seconds: 30 } };
109
+ const workerSchedule = config$1.has("rw.siteIndex.worker") ? backendPluginApi.readSchedulerServiceTaskScheduleDefinitionFromConfig(
110
+ config$1.getConfig("rw.siteIndex.worker")
111
+ ) : { frequency: { seconds: 30 }, timeout: { minutes: 5 }, initialDelay: { seconds: 10 } };
112
+ await scheduler.scheduleTask({
113
+ id: "rw-site-index-scan",
114
+ scope: "global",
115
+ ...scanSchedule,
116
+ fn: async () => runScan.runScan({ catalog, auth, logger, siteConfig, sectionOwnershipStore, siteRefreshStore })
117
+ });
118
+ await scheduler.scheduleTask({
119
+ id: "rw-site-index-worker",
120
+ scope: "local",
121
+ ...workerSchedule,
122
+ fn: async () => runWorker.runWorker({ logger, siteRefreshStore, registryStore, sectionOwnershipStore, makeSite })
123
+ });
124
+ logger.info(
125
+ `Scheduled site index rebuild: scan ${JSON.stringify(scanSchedule.frequency)} (global), worker ${JSON.stringify(workerSchedule.frequency)} (local)`
126
+ );
127
+ const commentsEnabled = config$1.getOptionalBoolean("rw.comments.enabled") ?? true;
128
+ permissionsRegistry.addResourceType({
129
+ resourceRef: permissions.commentResourceRef,
130
+ permissions: backstagePluginRwCommon.rwCommentResourcePermissions,
131
+ rules: [permissions.isCommentAuthor],
132
+ getResources: async (ids) => Promise.all(
133
+ ids.map(async (id) => {
134
+ const row = await store.get(id);
135
+ return row ? mapping.toCommentResponse(row, void 0) : void 0;
136
+ })
137
+ )
138
+ });
139
+ const router$2 = await router.createRouter({
140
+ hub: hub$1
141
+ });
142
+ httpRouter.use(router$2);
143
+ httpRouter.use(
144
+ inboxRouter.createInboxRouter({
145
+ httpAuth,
146
+ permissions: permissions$1,
147
+ userInfo,
148
+ store: inboxStore,
149
+ commentStore: store,
150
+ siteRefreshStore
151
+ })
152
+ );
153
+ httpRouter.use(
154
+ router$1.createCommentsRouter({
155
+ store,
156
+ logger,
157
+ httpAuth,
158
+ auth,
159
+ userInfo,
160
+ permissions: permissions$1,
161
+ permissionsRegistry,
162
+ catalog,
163
+ commentsEnabled,
164
+ publisher
165
+ })
166
+ );
50
167
  httpRouter.addAuthPolicy({
51
168
  path: "/health",
52
169
  allow: "unauthenticated"
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["import { coreServices, createBackendPlugin } from \"@backstage/backend-plugin-api\";\nimport { readDurationFromConfig } from \"@backstage/config\";\nimport { readRwSiteConfig, toEntityPath } from \"@rwdocs/backstage-plugin-rw-common\";\nimport { createRouter } from \"./router\";\nimport { Hub, type HubOptions } from \"./hub\";\n\nexport const rwPlugin = createBackendPlugin({\n pluginId: \"rw\",\n register(env) {\n env.registerInit({\n deps: {\n httpRouter: coreServices.httpRouter,\n httpAuth: coreServices.httpAuth,\n logger: coreServices.logger,\n config: coreServices.rootConfig,\n scheduler: coreServices.scheduler,\n },\n async init({ httpRouter, httpAuth, logger, config, scheduler }) {\n const siteConfig = readRwSiteConfig(config);\n const cacheSize = config.getOptionalNumber(\"rw.cacheSize\");\n\n const hubOptions: HubOptions = {\n ...siteConfig,\n cacheSize,\n };\n\n const hub = new Hub(hubOptions);\n\n if (siteConfig.s3) {\n logger.info(\n `Hub: S3 mode (bucket: ${siteConfig.s3.bucket}, cache size: ${cacheSize ?? 20})`,\n );\n } else {\n logger.info(\n `Hub: local mode (${siteConfig.projectDir}, entity: ${siteConfig.entity ? toEntityPath(siteConfig.entity) : siteConfig.entity})`,\n );\n }\n\n if (config.has(\"rw.reloadInterval\")) {\n const frequency = readDurationFromConfig(config, { key: \"rw.reloadInterval\" });\n logger.info(`Scheduling site reload with interval: ${JSON.stringify(frequency)}`);\n\n await scheduler.scheduleTask({\n id: \"rw-site-reload\",\n frequency,\n timeout: frequency,\n scope: \"local\",\n fn: async () => hub.reloadAll(logger),\n });\n }\n\n const router = await createRouter({ logger, httpAuth, hub });\n httpRouter.use(router);\n httpRouter.addAuthPolicy({\n path: \"/health\",\n allow: \"unauthenticated\",\n });\n },\n });\n },\n});\n"],"names":["createBackendPlugin","coreServices","config","readRwSiteConfig","hub","Hub","toEntityPath","readDurationFromConfig","router","createRouter"],"mappings":";;;;;;;;AAMO,MAAM,WAAWA,oCAAA,CAAoB;AAAA,EAC1C,QAAA,EAAU,IAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACZ,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACf,IAAA,EAAM;AAAA,QACJ,YAAYC,6BAAA,CAAa,UAAA;AAAA,QACzB,UAAUA,6BAAA,CAAa,QAAA;AAAA,QACvB,QAAQA,6BAAA,CAAa,MAAA;AAAA,QACrB,QAAQA,6BAAA,CAAa,UAAA;AAAA,QACrB,WAAWA,6BAAA,CAAa;AAAA,OAC1B;AAAA,MACA,MAAM,KAAK,EAAE,UAAA,EAAY,UAAU,MAAA,UAAQC,QAAA,EAAQ,WAAU,EAAG;AAC9D,QAAA,MAAM,UAAA,GAAaC,yCAAiBD,QAAM,CAAA;AAC1C,QAAA,MAAM,SAAA,GAAYA,QAAA,CAAO,iBAAA,CAAkB,cAAc,CAAA;AAEzD,QAAA,MAAM,UAAA,GAAyB;AAAA,UAC7B,GAAG,UAAA;AAAA,UACH;AAAA,SACF;AAEA,QAAA,MAAME,KAAA,GAAM,IAAIC,OAAA,CAAI,UAAU,CAAA;AAE9B,QAAA,IAAI,WAAW,EAAA,EAAI;AACjB,UAAA,MAAA,CAAO,IAAA;AAAA,YACL,yBAAyB,UAAA,CAAW,EAAA,CAAG,MAAM,CAAA,cAAA,EAAiB,aAAa,EAAE,CAAA,CAAA;AAAA,WAC/E;AAAA,QACF,CAAA,MAAO;AACL,UAAA,MAAA,CAAO,IAAA;AAAA,YACL,CAAA,iBAAA,EAAoB,UAAA,CAAW,UAAU,CAAA,UAAA,EAAa,UAAA,CAAW,MAAA,GAASC,oCAAA,CAAa,UAAA,CAAW,MAAM,CAAA,GAAI,UAAA,CAAW,MAAM,CAAA,CAAA;AAAA,WAC/H;AAAA,QACF;AAEA,QAAA,IAAIJ,QAAA,CAAO,GAAA,CAAI,mBAAmB,CAAA,EAAG;AACnC,UAAA,MAAM,YAAYK,6BAAA,CAAuBL,QAAA,EAAQ,EAAE,GAAA,EAAK,qBAAqB,CAAA;AAC7E,UAAA,MAAA,CAAO,KAAK,CAAA,sCAAA,EAAyC,IAAA,CAAK,SAAA,CAAU,SAAS,CAAC,CAAA,CAAE,CAAA;AAEhF,UAAA,MAAM,UAAU,YAAA,CAAa;AAAA,YAC3B,EAAA,EAAI,gBAAA;AAAA,YACJ,SAAA;AAAA,YACA,OAAA,EAAS,SAAA;AAAA,YACT,KAAA,EAAO,OAAA;AAAA,YACP,EAAA,EAAI,YAAYE,KAAA,CAAI,SAAA,CAAU,MAAM;AAAA,WACrC,CAAA;AAAA,QACH;AAEA,QAAA,MAAMI,WAAS,MAAMC,mBAAA,CAAa,OAAoBL,OAAK,CAAA;AAC3D,QAAA,UAAA,CAAW,IAAII,QAAM,CAAA;AACrB,QAAA,UAAA,CAAW,aAAA,CAAc;AAAA,UACvB,IAAA,EAAM,SAAA;AAAA,UACN,KAAA,EAAO;AAAA,SACR,CAAA;AAAA,MACH;AAAA,KACD,CAAA;AAAA,EACH;AACF,CAAC;;;;"}
1
+ {"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["import {\n coreServices,\n createBackendPlugin,\n resolvePackagePath,\n readSchedulerServiceTaskScheduleDefinitionFromConfig,\n} from \"@backstage/backend-plugin-api\";\nimport { SectionOwnershipStore } from \"./siteIndex/SectionOwnershipStore\";\nimport { RegistryStore } from \"./siteIndex/RegistryStore\";\nimport { SiteRefreshStore } from \"./siteIndex/SiteRefreshStore\";\nimport { runScan } from \"./siteIndex/runScan\";\nimport { runWorker } from \"./siteIndex/runWorker\";\nimport { makeSiteFactory } from \"./siteIndex/schedule\";\nimport { readDurationFromConfig } from \"@backstage/config\";\nimport {\n readRwSiteConfig,\n rwCommentResourcePermissions,\n toEntityPath,\n} from \"@rwdocs/backstage-plugin-rw-common\";\nimport { catalogServiceRef } from \"@backstage/plugin-catalog-node\";\nimport { eventsServiceRef } from \"@backstage/plugin-events-node\";\nimport { createRouter } from \"./router\";\nimport { Hub, type HubOptions } from \"./hub\";\nimport { CommentStore } from \"./comments/CommentStore\";\nimport { createCommentsRouter } from \"./comments/router\";\nimport { CommentEventPublisher } from \"./comments/CommentEventPublisher\";\nimport { SectionsReader } from \"./siteIndex/SectionsReader\";\nimport { PagesReader } from \"./siteIndex/PagesReader\";\nimport { commentResourceRef, isCommentAuthor } from \"./comments/permissions\";\nimport { toCommentResponse } from \"./comments/mapping\";\nimport { InboxStore } from \"./inbox/InboxStore\";\nimport { createInboxRouter } from \"./inbox/inboxRouter\";\n\nexport const rwPlugin = createBackendPlugin({\n pluginId: \"rw\",\n register(env) {\n env.registerInit({\n deps: {\n httpRouter: coreServices.httpRouter,\n httpAuth: coreServices.httpAuth,\n logger: coreServices.logger,\n config: coreServices.rootConfig,\n scheduler: coreServices.scheduler,\n database: coreServices.database,\n permissions: coreServices.permissions,\n permissionsRegistry: coreServices.permissionsRegistry,\n userInfo: coreServices.userInfo,\n auth: coreServices.auth,\n catalog: catalogServiceRef,\n events: eventsServiceRef,\n },\n async init({\n httpRouter,\n httpAuth,\n logger,\n config,\n scheduler,\n database,\n permissions,\n permissionsRegistry,\n userInfo,\n auth,\n catalog,\n events,\n }) {\n const siteConfig = readRwSiteConfig(config);\n const cacheSize = config.getOptionalNumber(\"rw.cacheSize\");\n\n const hubOptions: HubOptions = {\n ...siteConfig,\n cacheSize,\n };\n\n const hub = new Hub(hubOptions);\n\n if (siteConfig.s3) {\n logger.info(\n `Hub: S3 mode (bucket: ${siteConfig.s3.bucket}, cache size: ${cacheSize ?? 20})`,\n );\n } else {\n logger.info(\n `Hub: local mode (${siteConfig.projectDir}, entity: ${siteConfig.entity ? toEntityPath(siteConfig.entity) : siteConfig.entity})`,\n );\n }\n\n if (config.has(\"rw.reloadInterval\")) {\n const frequency = readDurationFromConfig(config, { key: \"rw.reloadInterval\" });\n logger.info(`Scheduling site reload with interval: ${JSON.stringify(frequency)}`);\n\n await scheduler.scheduleTask({\n id: \"rw-site-reload\",\n frequency,\n timeout: frequency,\n scope: \"local\",\n fn: async () => hub.reloadAll(logger),\n });\n }\n\n const client = await database.getClient();\n if (!database.migrations?.skip) {\n await client.migrate.latest({\n directory: resolvePackagePath(\"@rwdocs/backstage-plugin-rw-backend\", \"migrations\"),\n });\n }\n const store = new CommentStore(client);\n const inboxStore = new InboxStore(client);\n\n const sectionOwnershipStore = new SectionOwnershipStore(client);\n const registryStore = new RegistryStore(client);\n const siteRefreshStore = new SiteRefreshStore(client);\n const sectionsReader = new SectionsReader(client);\n const pagesReader = new PagesReader(client);\n const publisher = new CommentEventPublisher({\n events,\n sections: sectionsReader,\n comments: store,\n logger,\n pages: pagesReader,\n });\n const makeSite = makeSiteFactory(siteConfig);\n\n const scanSchedule = config.has(\"rw.siteIndex.schedule\")\n ? readSchedulerServiceTaskScheduleDefinitionFromConfig(\n config.getConfig(\"rw.siteIndex.schedule\"),\n )\n : { frequency: { minutes: 15 }, timeout: { minutes: 10 }, initialDelay: { seconds: 30 } };\n const workerSchedule = config.has(\"rw.siteIndex.worker\")\n ? readSchedulerServiceTaskScheduleDefinitionFromConfig(\n config.getConfig(\"rw.siteIndex.worker\"),\n )\n : { frequency: { seconds: 30 }, timeout: { minutes: 5 }, initialDelay: { seconds: 10 } };\n\n await scheduler.scheduleTask({\n id: \"rw-site-index-scan\",\n scope: \"global\",\n ...scanSchedule,\n fn: async () =>\n runScan({ catalog, auth, logger, siteConfig, sectionOwnershipStore, siteRefreshStore }),\n });\n await scheduler.scheduleTask({\n id: \"rw-site-index-worker\",\n scope: \"local\",\n ...workerSchedule,\n fn: async () =>\n runWorker({ logger, siteRefreshStore, registryStore, sectionOwnershipStore, makeSite }),\n });\n logger.info(\n `Scheduled site index rebuild: scan ${JSON.stringify(scanSchedule.frequency)} (global), ` +\n `worker ${JSON.stringify(workerSchedule.frequency)} (local)`,\n );\n\n const commentsEnabled = config.getOptionalBoolean(\"rw.comments.enabled\") ?? true;\n\n permissionsRegistry.addResourceType({\n resourceRef: commentResourceRef,\n permissions: rwCommentResourcePermissions,\n rules: [isCommentAuthor],\n getResources: async (ids: string[]) =>\n Promise.all(\n ids.map(async (id) => {\n const row = await store.get(id);\n return row ? toCommentResponse(row, undefined) : undefined;\n }),\n ),\n });\n\n const router = await createRouter({\n logger,\n httpAuth,\n hub,\n });\n httpRouter.use(router);\n // The inbox router MUST be mounted before the comments router: it owns the\n // exact path `/comments/inbox`, which would otherwise be shadowed by the\n // comments router's `/comments/:id` route (id=\"inbox\" → 404) when comments\n // are enabled, or its `/comments/*` catch-all (404) when disabled.\n httpRouter.use(\n createInboxRouter({\n httpAuth,\n permissions,\n userInfo,\n store: inboxStore,\n commentStore: store,\n siteRefreshStore,\n }),\n );\n httpRouter.use(\n createCommentsRouter({\n store,\n logger,\n httpAuth,\n auth,\n userInfo,\n permissions,\n permissionsRegistry,\n catalog,\n commentsEnabled,\n publisher,\n }),\n );\n httpRouter.addAuthPolicy({\n path: \"/health\",\n allow: \"unauthenticated\",\n });\n },\n });\n },\n});\n"],"names":["createBackendPlugin","coreServices","catalogServiceRef","eventsServiceRef","config","permissions","readRwSiteConfig","hub","Hub","toEntityPath","readDurationFromConfig","resolvePackagePath","CommentStore","InboxStore","SectionOwnershipStore","RegistryStore","SiteRefreshStore","SectionsReader","PagesReader","CommentEventPublisher","makeSiteFactory","readSchedulerServiceTaskScheduleDefinitionFromConfig","runScan","runWorker","commentResourceRef","rwCommentResourcePermissions","isCommentAuthor","toCommentResponse","router","createRouter","createInboxRouter","createCommentsRouter"],"mappings":"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"}
@@ -1,15 +1,15 @@
1
1
  'use strict';
2
2
 
3
- var Router = require('express-promise-router');
3
+ var PromiseRouter = require('express-promise-router');
4
4
  var errors = require('@backstage/errors');
5
5
 
6
6
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
7
7
 
8
- var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
8
+ var PromiseRouter__default = /*#__PURE__*/_interopDefaultCompat(PromiseRouter);
9
9
 
10
10
  async function createRouter(options) {
11
11
  const { hub } = options;
12
- const router = Router__default.default();
12
+ const router = PromiseRouter__default.default();
13
13
  router.get("/health", (_req, res) => {
14
14
  res.json({ status: "ok" });
15
15
  });
@@ -85,4 +85,5 @@ function toStorageError(err) {
85
85
  }
86
86
 
87
87
  exports.createRouter = createRouter;
88
+ exports.toStorageError = toStorageError;
88
89
  //# sourceMappingURL=router.cjs.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"router.cjs.js","sources":["../src/router.ts"],"sourcesContent":["import Router from \"express-promise-router\";\nimport type { HttpAuthService, LoggerService } from \"@backstage/backend-plugin-api\";\nimport { InputError, NotFoundError, ServiceUnavailableError } from \"@backstage/errors\";\nimport type { RwSite } from \"@rwdocs/core\";\nimport type { Hub } from \"./hub\";\n\nexport interface RouterOptions {\n logger: LoggerService;\n httpAuth: HttpAuthService;\n hub: Hub;\n}\n\nexport async function createRouter(options: RouterOptions) {\n const { hub } = options;\n const router = Router();\n\n router.get(\"/health\", (_req, res) => {\n res.json({ status: \"ok\" });\n });\n\n router.use(\"/site/:namespace/:kind/:name\", (req, res, next) => {\n const { namespace, kind, name } = req.params;\n const siteRef = `${namespace}/${kind}/${name}`.toLowerCase();\n\n const site = hub.getSite(siteRef);\n if (!site) {\n throw new NotFoundError(`No documentation site found for entity: ${siteRef}`);\n }\n\n res.locals.rwSite = site;\n next();\n });\n\n router.get(\"/site/:namespace/:kind/:name/config\", (_req, res) => {\n res.json({ liveReloadEnabled: false });\n });\n\n router.get(\"/site/:namespace/:kind/:name/navigation\", async (req, res) => {\n const site: RwSite = res.locals.rwSite;\n const sectionRefParam = req.query.sectionRef;\n const sectionRef = typeof sectionRefParam === \"string\" ? sectionRefParam : null;\n const nav = await getNavigationOrThrow(site, sectionRef);\n res.json(nav);\n });\n\n router.get(\"/site/:namespace/:kind/:name/pages/\", async (req, res) => {\n const site: RwSite = res.locals.rwSite;\n const sectionRefParam = req.query.sectionRef;\n const sectionRef = typeof sectionRefParam === \"string\" ? sectionRefParam : undefined;\n\n let pagePath = \"\";\n if (sectionRef) {\n const nav = await getNavigationOrThrow(site, sectionRef);\n if (nav.scope?.path) {\n pagePath = nav.scope.path.replace(/^\\//, \"\");\n }\n }\n\n const page = await renderPageOrThrow(site, pagePath);\n res.json(page);\n });\n\n router.get(\"/site/:namespace/:kind/:name/pages/:path(*)\", async (req, res) => {\n const site: RwSite = res.locals.rwSite;\n const pagePath = req.params.path || \"\";\n if (pagePath.split(\"/\").includes(\"..\")) {\n throw new InputError(\"Invalid path\");\n }\n const page = await renderPageOrThrow(site, pagePath);\n res.json(page);\n });\n\n return router;\n}\n\nasync function getNavigationOrThrow(site: RwSite, sectionRef: string | null) {\n try {\n return await site.getNavigation(sectionRef);\n } catch (err) {\n throw toStorageError(err);\n }\n}\n\nasync function renderPageOrThrow(site: RwSite, pagePath: string) {\n try {\n return await site.renderPage(pagePath);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n if (message.includes(\"Content not found\")) {\n throw new NotFoundError(`Page not found: /${pagePath}`);\n }\n // Message prefix comes from @rwdocs/core native addon (RenderError::Storage).\n // Must be updated if the upstream error format changes.\n if (message.includes(\"Storage error\")) {\n throw toStorageError(err);\n }\n throw err;\n }\n}\n\nfunction toStorageError(err: unknown): ServiceUnavailableError {\n const message = err instanceof Error ? err.message : String(err);\n return new ServiceUnavailableError(`Storage unavailable: ${message}`);\n}\n"],"names":["Router","NotFoundError","InputError","ServiceUnavailableError"],"mappings":"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"}
1
+ {"version":3,"file":"router.cjs.js","sources":["../src/router.ts"],"sourcesContent":["import Router from \"express-promise-router\";\nimport type { HttpAuthService, LoggerService } from \"@backstage/backend-plugin-api\";\nimport { InputError, NotFoundError, ServiceUnavailableError } from \"@backstage/errors\";\nimport type { RwSite } from \"@rwdocs/core\";\nimport type { Hub } from \"./hub\";\n\nexport interface RouterOptions {\n logger: LoggerService;\n httpAuth: HttpAuthService;\n hub: Hub;\n}\n\nexport async function createRouter(options: RouterOptions) {\n const { hub } = options;\n const router = Router();\n\n router.get(\"/health\", (_req, res) => {\n res.json({ status: \"ok\" });\n });\n\n router.use(\"/site/:namespace/:kind/:name\", (req, res, next) => {\n const { namespace, kind, name } = req.params;\n const siteRef = `${namespace}/${kind}/${name}`.toLowerCase();\n\n const site = hub.getSite(siteRef);\n if (!site) {\n throw new NotFoundError(`No documentation site found for entity: ${siteRef}`);\n }\n\n res.locals.rwSite = site;\n next();\n });\n\n router.get(\"/site/:namespace/:kind/:name/config\", (_req, res) => {\n res.json({ liveReloadEnabled: false });\n });\n\n router.get(\"/site/:namespace/:kind/:name/navigation\", async (req, res) => {\n const site: RwSite = res.locals.rwSite;\n const sectionRefParam = req.query.sectionRef;\n const sectionRef = typeof sectionRefParam === \"string\" ? sectionRefParam : null;\n const nav = await getNavigationOrThrow(site, sectionRef);\n res.json(nav);\n });\n\n router.get(\"/site/:namespace/:kind/:name/pages/\", async (req, res) => {\n const site: RwSite = res.locals.rwSite;\n const sectionRefParam = req.query.sectionRef;\n const sectionRef = typeof sectionRefParam === \"string\" ? sectionRefParam : undefined;\n\n let pagePath = \"\";\n if (sectionRef) {\n const nav = await getNavigationOrThrow(site, sectionRef);\n if (nav.scope?.path) {\n pagePath = nav.scope.path.replace(/^\\//, \"\");\n }\n }\n\n const page = await renderPageOrThrow(site, pagePath);\n res.json(page);\n });\n\n router.get(\"/site/:namespace/:kind/:name/pages/:path(*)\", async (req, res) => {\n const site: RwSite = res.locals.rwSite;\n const pagePath = req.params.path || \"\";\n if (pagePath.split(\"/\").includes(\"..\")) {\n throw new InputError(\"Invalid path\");\n }\n const page = await renderPageOrThrow(site, pagePath);\n res.json(page);\n });\n\n return router;\n}\n\nasync function getNavigationOrThrow(site: RwSite, sectionRef: string | null) {\n try {\n return await site.getNavigation(sectionRef);\n } catch (err) {\n throw toStorageError(err);\n }\n}\n\nasync function renderPageOrThrow(site: RwSite, pagePath: string) {\n try {\n return await site.renderPage(pagePath);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n if (message.includes(\"Content not found\")) {\n throw new NotFoundError(`Page not found: /${pagePath}`);\n }\n // Message prefix comes from @rwdocs/core native addon (RenderError::Storage).\n // Must be updated if the upstream error format changes.\n if (message.includes(\"Storage error\")) {\n throw toStorageError(err);\n }\n throw err;\n }\n}\n\nexport function toStorageError(err: unknown): ServiceUnavailableError {\n const message = err instanceof Error ? err.message : String(err);\n return new ServiceUnavailableError(`Storage unavailable: ${message}`);\n}\n"],"names":["Router","NotFoundError","InputError","ServiceUnavailableError"],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ 'use strict';
2
+
3
+ const TABLE = "pages";
4
+ class PagesReader {
5
+ constructor(knex) {
6
+ this.knex = knex;
7
+ }
8
+ async getTitle(siteRef, sectionRef, subpath) {
9
+ const row = await this.knex(TABLE).where({ site_ref: siteRef, section_ref: sectionRef, subpath }).select("title").first();
10
+ return row?.title ?? null;
11
+ }
12
+ }
13
+
14
+ exports.PagesReader = PagesReader;
15
+ //# sourceMappingURL=PagesReader.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PagesReader.cjs.js","sources":["../../src/siteIndex/PagesReader.ts"],"sourcesContent":["import type { Knex } from \"knex\";\n\nconst TABLE = \"pages\";\n\n/** By-key reader for the `pages` registry. Used by the comment-event publisher\n * to resolve the page title (pageTitle) for a given (siteRef, sectionRef, subpath). */\nexport class PagesReader {\n constructor(private readonly knex: Knex) {}\n\n async getTitle(siteRef: string, sectionRef: string, subpath: string): Promise<string | null> {\n const row = await this.knex(TABLE)\n .where({ site_ref: siteRef, section_ref: sectionRef, subpath })\n .select(\"title\")\n .first();\n return row?.title ?? null;\n }\n}\n"],"names":[],"mappings":";;AAEA,MAAM,KAAA,GAAQ,OAAA;AAIP,MAAM,WAAA,CAAY;AAAA,EACvB,YAA6B,IAAA,EAAY;AAAZ,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AAAA,EAAa;AAAA,EAE1C,MAAM,QAAA,CAAS,OAAA,EAAiB,UAAA,EAAoB,OAAA,EAAyC;AAC3F,IAAA,MAAM,MAAM,MAAM,IAAA,CAAK,KAAK,KAAK,CAAA,CAC9B,MAAM,EAAE,QAAA,EAAU,OAAA,EAAS,WAAA,EAAa,YAAY,OAAA,EAAS,EAC7D,MAAA,CAAO,OAAO,EACd,KAAA,EAAM;AACT,IAAA,OAAO,KAAK,KAAA,IAAS,IAAA;AAAA,EACvB;AACF;;;;"}
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ class RegistryStore {
4
+ constructor(knex) {
5
+ this.knex = knex;
6
+ }
7
+ /** Replace the site's `sections` and `pages` rows in one transaction. `sections` carries the
8
+ * effective-ownership rollup, so this swap is the single point where ownership is published. */
9
+ async swapSite(siteRef, sections, pages) {
10
+ await this.knex.transaction(async (tx) => {
11
+ await tx("sections").where({ site_ref: siteRef }).del();
12
+ await tx("pages").where({ site_ref: siteRef }).del();
13
+ if (sections.length) await tx.batchInsert("sections", sections, 500);
14
+ if (pages.length) await tx.batchInsert("pages", pages, 500);
15
+ });
16
+ }
17
+ }
18
+
19
+ exports.RegistryStore = RegistryStore;
20
+ //# sourceMappingURL=RegistryStore.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RegistryStore.cjs.js","sources":["../../src/siteIndex/RegistryStore.ts"],"sourcesContent":["import type { Knex } from \"knex\";\nimport type { SectionRow, PageRow } from \"./types\";\n\nexport class RegistryStore {\n constructor(private readonly knex: Knex) {}\n\n /** Replace the site's `sections` and `pages` rows in one transaction. `sections` carries the\n * effective-ownership rollup, so this swap is the single point where ownership is published. */\n async swapSite(siteRef: string, sections: SectionRow[], pages: PageRow[]): Promise<void> {\n await this.knex.transaction(async (tx) => {\n await tx(\"sections\").where({ site_ref: siteRef }).del();\n await tx(\"pages\").where({ site_ref: siteRef }).del();\n if (sections.length) await tx.batchInsert(\"sections\", sections, 500);\n if (pages.length) await tx.batchInsert(\"pages\", pages, 500);\n });\n }\n}\n"],"names":[],"mappings":";;AAGO,MAAM,aAAA,CAAc;AAAA,EACzB,YAA6B,IAAA,EAAY;AAAZ,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AAAA,EAAa;AAAA;AAAA;AAAA,EAI1C,MAAM,QAAA,CAAS,OAAA,EAAiB,QAAA,EAAwB,KAAA,EAAiC;AACvF,IAAA,MAAM,IAAA,CAAK,IAAA,CAAK,WAAA,CAAY,OAAO,EAAA,KAAO;AACxC,MAAA,MAAM,EAAA,CAAG,UAAU,CAAA,CAAE,KAAA,CAAM,EAAE,QAAA,EAAU,OAAA,EAAS,CAAA,CAAE,GAAA,EAAI;AACtD,MAAA,MAAM,EAAA,CAAG,OAAO,CAAA,CAAE,KAAA,CAAM,EAAE,QAAA,EAAU,OAAA,EAAS,CAAA,CAAE,GAAA,EAAI;AACnD,MAAA,IAAI,SAAS,MAAA,EAAQ,MAAM,GAAG,WAAA,CAAY,UAAA,EAAY,UAAU,GAAG,CAAA;AACnE,MAAA,IAAI,MAAM,MAAA,EAAQ,MAAM,GAAG,WAAA,CAAY,OAAA,EAAS,OAAO,GAAG,CAAA;AAAA,IAC5D,CAAC,CAAA;AAAA,EACH;AACF;;;;"}
@@ -0,0 +1,21 @@
1
+ 'use strict';
2
+
3
+ const TABLE = "section_ownership";
4
+ class SectionOwnershipStore {
5
+ constructor(knex) {
6
+ this.knex = knex;
7
+ }
8
+ /** Replace all links for `siteRef`. Pass `executor` to join a per-site transaction. */
9
+ async swapSite(siteRef, links, executor) {
10
+ const exec = executor ?? this.knex;
11
+ await exec(TABLE).where({ site_ref: siteRef }).del();
12
+ if (links.length) await exec.batchInsert(TABLE, links, 500);
13
+ }
14
+ /** Return all ownership links for `siteRef`. */
15
+ async listForSite(siteRef) {
16
+ return this.knex(TABLE).where({ site_ref: siteRef }).select("*");
17
+ }
18
+ }
19
+
20
+ exports.SectionOwnershipStore = SectionOwnershipStore;
21
+ //# sourceMappingURL=SectionOwnershipStore.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SectionOwnershipStore.cjs.js","sources":["../../src/siteIndex/SectionOwnershipStore.ts"],"sourcesContent":["import type { Knex } from \"knex\";\nimport type { SectionOwnershipRow } from \"./types\";\n\nconst TABLE = \"section_ownership\";\n\nexport class SectionOwnershipStore {\n constructor(private readonly knex: Knex) {}\n\n /** Replace all links for `siteRef`. Pass `executor` to join a per-site transaction. */\n async swapSite(\n siteRef: string,\n links: SectionOwnershipRow[],\n executor?: Knex | Knex.Transaction,\n ): Promise<void> {\n const exec = executor ?? this.knex;\n await exec(TABLE).where({ site_ref: siteRef }).del();\n if (links.length) await exec.batchInsert(TABLE, links, 500);\n }\n\n /** Return all ownership links for `siteRef`. */\n async listForSite(siteRef: string): Promise<SectionOwnershipRow[]> {\n return this.knex(TABLE).where({ site_ref: siteRef }).select(\"*\");\n }\n}\n"],"names":[],"mappings":";;AAGA,MAAM,KAAA,GAAQ,mBAAA;AAEP,MAAM,qBAAA,CAAsB;AAAA,EACjC,YAA6B,IAAA,EAAY;AAAZ,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AAAA,EAAa;AAAA;AAAA,EAG1C,MAAM,QAAA,CACJ,OAAA,EACA,KAAA,EACA,QAAA,EACe;AACf,IAAA,MAAM,IAAA,GAAO,YAAY,IAAA,CAAK,IAAA;AAC9B,IAAA,MAAM,IAAA,CAAK,KAAK,CAAA,CAAE,KAAA,CAAM,EAAE,QAAA,EAAU,OAAA,EAAS,CAAA,CAAE,GAAA,EAAI;AACnD,IAAA,IAAI,MAAM,MAAA,EAAQ,MAAM,KAAK,WAAA,CAAY,KAAA,EAAO,OAAO,GAAG,CAAA;AAAA,EAC5D;AAAA;AAAA,EAGA,MAAM,YAAY,OAAA,EAAiD;AACjE,IAAA,OAAO,IAAA,CAAK,IAAA,CAAK,KAAK,CAAA,CAAE,KAAA,CAAM,EAAE,QAAA,EAAU,OAAA,EAAS,CAAA,CAAE,MAAA,CAAO,GAAG,CAAA;AAAA,EACjE;AACF;;;;"}
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+
3
+ const TABLE = "sections";
4
+ class SectionsReader {
5
+ constructor(knex) {
6
+ this.knex = knex;
7
+ }
8
+ async getSection(siteRef, sectionRef) {
9
+ return this.knex(TABLE).where({ site_ref: siteRef, section_ref: sectionRef }).first();
10
+ }
11
+ }
12
+
13
+ exports.SectionsReader = SectionsReader;
14
+ //# sourceMappingURL=SectionsReader.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SectionsReader.cjs.js","sources":["../../src/siteIndex/SectionsReader.ts"],"sourcesContent":["import type { Knex } from \"knex\";\nimport { SectionRow } from \"./types\";\n\nconst TABLE = \"sections\";\n\n/** By-key reader for the dense `sections` registry. Used by the comment-event publisher\n * to resolve a section's effective owner (`entity_owner_ref`), its owning entity\n * (`entity_ref`), and its owner-relative `section_path` for the deep link. One indexed\n * point-read on PK (site_ref, section_ref); no RwSite load. Kept separate from the\n * swap-only RegistryStore and the by-owner InboxStore reads. */\nexport class SectionsReader {\n constructor(private readonly knex: Knex) {}\n\n async getSection(siteRef: string, sectionRef: string): Promise<SectionRow | undefined> {\n return this.knex<SectionRow>(TABLE)\n .where({ site_ref: siteRef, section_ref: sectionRef })\n .first();\n }\n}\n"],"names":[],"mappings":";;AAGA,MAAM,KAAA,GAAQ,UAAA;AAOP,MAAM,cAAA,CAAe;AAAA,EAC1B,YAA6B,IAAA,EAAY;AAAZ,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AAAA,EAAa;AAAA,EAE1C,MAAM,UAAA,CAAW,OAAA,EAAiB,UAAA,EAAqD;AACrF,IAAA,OAAO,IAAA,CAAK,IAAA,CAAiB,KAAK,CAAA,CAC/B,KAAA,CAAM,EAAE,QAAA,EAAU,OAAA,EAAS,WAAA,EAAa,UAAA,EAAY,CAAA,CACpD,KAAA,EAAM;AAAA,EACX;AACF;;;;"}
@@ -0,0 +1,73 @@
1
+ 'use strict';
2
+
3
+ const TABLE = "site_refresh";
4
+ const LOCKING_CLIENTS = ["pg", "mysql", "mysql2"];
5
+ class SiteRefreshStore {
6
+ constructor(knex) {
7
+ this.knex = knex;
8
+ this.useLocking = LOCKING_CLIENTS.includes(String(knex.client.config.client));
9
+ }
10
+ useLocking;
11
+ /** Insert a new queue row (due now) or, on conflict, only advance last_discovery_at. */
12
+ async upsertSite(siteRef, scanStart, executor) {
13
+ const exec = executor ?? this.knex;
14
+ await exec(TABLE).insert({
15
+ site_ref: siteRef,
16
+ next_update_at: scanStart,
17
+ last_built_at: null,
18
+ result_hash: null,
19
+ errors: null,
20
+ last_discovery_at: scanStart
21
+ }).onConflict("site_ref").merge(["last_discovery_at"]);
22
+ }
23
+ /** Delete queue rows not seen in the current (completed) scan. Returns count. */
24
+ async pruneMissing(scanStart) {
25
+ return this.knex(TABLE).where("last_discovery_at", "<", scanStart).del();
26
+ }
27
+ /** Claim up to `batch` due rows, bumping their lease, in one transaction. */
28
+ async claimDue(now, batch, leaseUntil) {
29
+ return this.knex.transaction(async (tx) => {
30
+ const q = tx(TABLE).where("next_update_at", "<=", now).orderBy("next_update_at", "asc").limit(batch).select("site_ref", "result_hash");
31
+ if (this.useLocking) q.forUpdate().skipLocked();
32
+ const rows = await q;
33
+ if (rows.length) {
34
+ await tx(TABLE).whereIn(
35
+ "site_ref",
36
+ rows.map((r) => r.site_ref)
37
+ ).update({ next_update_at: leaseUntil });
38
+ }
39
+ return rows.map((r) => ({ siteRef: r.site_ref, resultHash: r.result_hash ?? null }));
40
+ });
41
+ }
42
+ async completeSuccess(siteRef, resultHash, nextUpdateAt, now) {
43
+ await this.knex(TABLE).where({ site_ref: siteRef }).update({
44
+ last_built_at: now,
45
+ result_hash: resultHash,
46
+ errors: null,
47
+ next_update_at: nextUpdateAt
48
+ });
49
+ }
50
+ async recordError(siteRef, message) {
51
+ await this.knex(TABLE).where({ site_ref: siteRef }).update({ errors: message });
52
+ }
53
+ /** True iff every queue row has been built at least once. Returns true for an empty table
54
+ * (vacuous truth) — prefer anyBuilt() to test readiness. */
55
+ async allBuilt() {
56
+ const [{ count }] = await this.knex(TABLE).whereNull("last_built_at").count({ count: "*" });
57
+ return Number(count) === 0;
58
+ }
59
+ /** True iff at least one queue row has been built. Preferred over allBuilt() for the inbox
60
+ * readiness check: avoids (a) empty-table false-positive and (b) one permanently-failing site
61
+ * blocking the inbox for all users forever. */
62
+ async anyBuilt() {
63
+ const [{ count }] = await this.knex(TABLE).whereNotNull("last_built_at").count({ count: "*" });
64
+ return Number(count) > 0;
65
+ }
66
+ /** Run fn in a single DB transaction (lets a caller atomically span multiple stores). */
67
+ async transaction(fn) {
68
+ return this.knex.transaction(fn);
69
+ }
70
+ }
71
+
72
+ exports.SiteRefreshStore = SiteRefreshStore;
73
+ //# sourceMappingURL=SiteRefreshStore.cjs.js.map