@backstage/plugin-events-backend 0.3.13-next.0 → 0.3.13-next.1

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.
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ var DefaultEventBroker = require('./DefaultEventBroker.cjs.js');
4
+
5
+ class EventsBackend {
6
+ eventBroker;
7
+ publishers = [];
8
+ subscribers = [];
9
+ constructor(logger) {
10
+ this.eventBroker = new DefaultEventBroker.DefaultEventBroker(logger);
11
+ }
12
+ setEventBroker(eventBroker) {
13
+ this.eventBroker = eventBroker;
14
+ return this;
15
+ }
16
+ addPublishers(...publishers) {
17
+ this.publishers.push(...publishers.flat());
18
+ return this;
19
+ }
20
+ addSubscribers(...subscribers) {
21
+ this.subscribers.push(...subscribers.flat());
22
+ return this;
23
+ }
24
+ /**
25
+ * Wires up and returns all component parts of the event management.
26
+ */
27
+ async start() {
28
+ this.eventBroker.subscribe(this.subscribers);
29
+ this.publishers.forEach(
30
+ (publisher) => publisher.setEventBroker(this.eventBroker)
31
+ );
32
+ }
33
+ }
34
+
35
+ exports.EventsBackend = EventsBackend;
36
+ //# sourceMappingURL=EventsBackend.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EventsBackend.cjs.js","sources":["../../src/service/EventsBackend.ts"],"sourcesContent":["/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n EventBroker,\n EventPublisher,\n EventSubscriber,\n} from '@backstage/plugin-events-node';\nimport { Logger } from 'winston';\nimport { DefaultEventBroker } from './DefaultEventBroker';\n\n/**\n * A builder that helps wire up all component parts of the event management.\n *\n * @public\n * @deprecated `EventBroker`, `EventPublisher`, and `EventSubscriber` got replaced by `EventsService` and its methods.\n */\nexport class EventsBackend {\n private eventBroker: EventBroker;\n private publishers: EventPublisher[] = [];\n private subscribers: EventSubscriber[] = [];\n\n constructor(logger: Logger) {\n this.eventBroker = new DefaultEventBroker(logger);\n }\n\n setEventBroker(eventBroker: EventBroker): EventsBackend {\n this.eventBroker = eventBroker;\n return this;\n }\n\n addPublishers(\n ...publishers: Array<EventPublisher | Array<EventPublisher>>\n ): EventsBackend {\n this.publishers.push(...publishers.flat());\n return this;\n }\n\n addSubscribers(\n ...subscribers: Array<EventSubscriber | Array<EventSubscriber>>\n ): EventsBackend {\n this.subscribers.push(...subscribers.flat());\n return this;\n }\n\n /**\n * Wires up and returns all component parts of the event management.\n */\n async start(): Promise<void> {\n this.eventBroker.subscribe(this.subscribers);\n this.publishers.forEach(publisher =>\n publisher.setEventBroker(this.eventBroker),\n );\n }\n}\n"],"names":["DefaultEventBroker"],"mappings":";;;;AA8BO,MAAM,aAAc,CAAA;AAAA,EACjB,WAAA,CAAA;AAAA,EACA,aAA+B,EAAC,CAAA;AAAA,EAChC,cAAiC,EAAC,CAAA;AAAA,EAE1C,YAAY,MAAgB,EAAA;AAC1B,IAAK,IAAA,CAAA,WAAA,GAAc,IAAIA,qCAAA,CAAmB,MAAM,CAAA,CAAA;AAAA,GAClD;AAAA,EAEA,eAAe,WAAyC,EAAA;AACtD,IAAA,IAAA,CAAK,WAAc,GAAA,WAAA,CAAA;AACnB,IAAO,OAAA,IAAA,CAAA;AAAA,GACT;AAAA,EAEA,iBACK,UACY,EAAA;AACf,IAAA,IAAA,CAAK,UAAW,CAAA,IAAA,CAAK,GAAG,UAAA,CAAW,MAAM,CAAA,CAAA;AACzC,IAAO,OAAA,IAAA,CAAA;AAAA,GACT;AAAA,EAEA,kBACK,WACY,EAAA;AACf,IAAA,IAAA,CAAK,WAAY,CAAA,IAAA,CAAK,GAAG,WAAA,CAAY,MAAM,CAAA,CAAA;AAC3C,IAAO,OAAA,IAAA,CAAA;AAAA,GACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,KAAuB,GAAA;AAC3B,IAAK,IAAA,CAAA,WAAA,CAAY,SAAU,CAAA,IAAA,CAAK,WAAW,CAAA,CAAA;AAC3C,IAAA,IAAA,CAAK,UAAW,CAAA,OAAA;AAAA,MAAQ,CACtB,SAAA,KAAA,SAAA,CAAU,cAAe,CAAA,IAAA,CAAK,WAAW,CAAA;AAAA,KAC3C,CAAA;AAAA,GACF;AACF;;;;"}
@@ -0,0 +1,98 @@
1
+ 'use strict';
2
+
3
+ var backendPluginApi = require('@backstage/backend-plugin-api');
4
+ var alpha = require('@backstage/plugin-events-node/alpha');
5
+ var pluginEventsNode = require('@backstage/plugin-events-node');
6
+ var Router = require('express-promise-router');
7
+ var HttpPostIngressEventPublisher = require('./http/HttpPostIngressEventPublisher.cjs.js');
8
+ var createEventBusRouter = require('./hub/createEventBusRouter.cjs.js');
9
+
10
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
11
+
12
+ var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
13
+
14
+ class EventsExtensionPointImpl {
15
+ #httpPostIngresses = [];
16
+ setEventBroker(_) {
17
+ throw new Error(
18
+ "setEventBroker is not supported anymore; use eventsServiceRef instead"
19
+ );
20
+ }
21
+ addPublishers(_) {
22
+ throw new Error(
23
+ "addPublishers is not supported anymore; use EventsService instead"
24
+ );
25
+ }
26
+ addSubscribers(_) {
27
+ throw new Error(
28
+ "addSubscribers is not supported anymore; use EventsService instead"
29
+ );
30
+ }
31
+ addHttpPostIngress(options) {
32
+ this.#httpPostIngresses.push(options);
33
+ }
34
+ get httpPostIngresses() {
35
+ return this.#httpPostIngresses;
36
+ }
37
+ }
38
+ const eventsPlugin = backendPluginApi.createBackendPlugin({
39
+ pluginId: "events",
40
+ register(env) {
41
+ const extensionPoint = new EventsExtensionPointImpl();
42
+ env.registerExtensionPoint(alpha.eventsExtensionPoint, extensionPoint);
43
+ env.registerInit({
44
+ deps: {
45
+ config: backendPluginApi.coreServices.rootConfig,
46
+ events: pluginEventsNode.eventsServiceRef,
47
+ database: backendPluginApi.coreServices.database,
48
+ logger: backendPluginApi.coreServices.logger,
49
+ scheduler: backendPluginApi.coreServices.scheduler,
50
+ lifecycle: backendPluginApi.coreServices.lifecycle,
51
+ httpAuth: backendPluginApi.coreServices.httpAuth,
52
+ router: backendPluginApi.coreServices.httpRouter
53
+ },
54
+ async init({
55
+ config,
56
+ events,
57
+ database,
58
+ logger,
59
+ scheduler,
60
+ lifecycle,
61
+ httpAuth,
62
+ router
63
+ }) {
64
+ const ingresses = Object.fromEntries(
65
+ extensionPoint.httpPostIngresses.map((ingress) => [
66
+ ingress.topic,
67
+ ingress
68
+ ])
69
+ );
70
+ const http = HttpPostIngressEventPublisher.HttpPostIngressEventPublisher.fromConfig({
71
+ config,
72
+ events,
73
+ ingresses,
74
+ logger
75
+ });
76
+ const eventsRouter = Router__default.default();
77
+ http.bind(eventsRouter);
78
+ router.use(
79
+ await createEventBusRouter.createEventBusRouter({
80
+ database,
81
+ logger,
82
+ httpAuth,
83
+ scheduler,
84
+ lifecycle
85
+ })
86
+ );
87
+ router.use(eventsRouter);
88
+ router.addAuthPolicy({
89
+ allow: "unauthenticated",
90
+ path: "/http"
91
+ });
92
+ }
93
+ });
94
+ }
95
+ });
96
+
97
+ exports.eventsPlugin = eventsPlugin;
98
+ //# sourceMappingURL=EventsPlugin.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EventsPlugin.cjs.js","sources":["../../src/service/EventsPlugin.ts"],"sourcesContent":["/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n createBackendPlugin,\n coreServices,\n} from '@backstage/backend-plugin-api';\nimport {\n eventsExtensionPoint,\n EventsExtensionPoint,\n} from '@backstage/plugin-events-node/alpha';\nimport {\n eventsServiceRef,\n HttpPostIngressOptions,\n} from '@backstage/plugin-events-node';\nimport Router from 'express-promise-router';\nimport { HttpPostIngressEventPublisher } from './http';\nimport { createEventBusRouter } from './hub';\n\nclass EventsExtensionPointImpl implements EventsExtensionPoint {\n #httpPostIngresses: HttpPostIngressOptions[] = [];\n\n setEventBroker(_: any): void {\n throw new Error(\n 'setEventBroker is not supported anymore; use eventsServiceRef instead',\n );\n }\n\n addPublishers(_: any): void {\n throw new Error(\n 'addPublishers is not supported anymore; use EventsService instead',\n );\n }\n\n addSubscribers(_: any): void {\n throw new Error(\n 'addSubscribers is not supported anymore; use EventsService instead',\n );\n }\n\n addHttpPostIngress(options: HttpPostIngressOptions) {\n this.#httpPostIngresses.push(options);\n }\n\n get httpPostIngresses() {\n return this.#httpPostIngresses;\n }\n}\n\n/**\n * Events plugin\n *\n * @alpha\n */\nexport const eventsPlugin = createBackendPlugin({\n pluginId: 'events',\n register(env) {\n const extensionPoint = new EventsExtensionPointImpl();\n env.registerExtensionPoint(eventsExtensionPoint, extensionPoint);\n\n env.registerInit({\n deps: {\n config: coreServices.rootConfig,\n events: eventsServiceRef,\n database: coreServices.database,\n logger: coreServices.logger,\n scheduler: coreServices.scheduler,\n lifecycle: coreServices.lifecycle,\n httpAuth: coreServices.httpAuth,\n router: coreServices.httpRouter,\n },\n async init({\n config,\n events,\n database,\n logger,\n scheduler,\n lifecycle,\n httpAuth,\n router,\n }) {\n const ingresses = Object.fromEntries(\n extensionPoint.httpPostIngresses.map(ingress => [\n ingress.topic,\n ingress as Omit<HttpPostIngressOptions, 'topic'>,\n ]),\n );\n\n const http = HttpPostIngressEventPublisher.fromConfig({\n config,\n events,\n ingresses,\n logger,\n });\n const eventsRouter = Router();\n http.bind(eventsRouter);\n\n router.use(\n await createEventBusRouter({\n database,\n logger,\n httpAuth,\n scheduler,\n lifecycle,\n }),\n );\n\n router.use(eventsRouter);\n router.addAuthPolicy({\n allow: 'unauthenticated',\n path: '/http',\n });\n },\n });\n },\n});\n"],"names":["createBackendPlugin","eventsExtensionPoint","coreServices","eventsServiceRef","HttpPostIngressEventPublisher","Router","createEventBusRouter"],"mappings":";;;;;;;;;;;;;AAgCA,MAAM,wBAAyD,CAAA;AAAA,EAC7D,qBAA+C,EAAC,CAAA;AAAA,EAEhD,eAAe,CAAc,EAAA;AAC3B,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,uEAAA;AAAA,KACF,CAAA;AAAA,GACF;AAAA,EAEA,cAAc,CAAc,EAAA;AAC1B,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,mEAAA;AAAA,KACF,CAAA;AAAA,GACF;AAAA,EAEA,eAAe,CAAc,EAAA;AAC3B,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,oEAAA;AAAA,KACF,CAAA;AAAA,GACF;AAAA,EAEA,mBAAmB,OAAiC,EAAA;AAClD,IAAK,IAAA,CAAA,kBAAA,CAAmB,KAAK,OAAO,CAAA,CAAA;AAAA,GACtC;AAAA,EAEA,IAAI,iBAAoB,GAAA;AACtB,IAAA,OAAO,IAAK,CAAA,kBAAA,CAAA;AAAA,GACd;AACF,CAAA;AAOO,MAAM,eAAeA,oCAAoB,CAAA;AAAA,EAC9C,QAAU,EAAA,QAAA;AAAA,EACV,SAAS,GAAK,EAAA;AACZ,IAAM,MAAA,cAAA,GAAiB,IAAI,wBAAyB,EAAA,CAAA;AACpD,IAAI,GAAA,CAAA,sBAAA,CAAuBC,4BAAsB,cAAc,CAAA,CAAA;AAE/D,IAAA,GAAA,CAAI,YAAa,CAAA;AAAA,MACf,IAAM,EAAA;AAAA,QACJ,QAAQC,6BAAa,CAAA,UAAA;AAAA,QACrB,MAAQ,EAAAC,iCAAA;AAAA,QACR,UAAUD,6BAAa,CAAA,QAAA;AAAA,QACvB,QAAQA,6BAAa,CAAA,MAAA;AAAA,QACrB,WAAWA,6BAAa,CAAA,SAAA;AAAA,QACxB,WAAWA,6BAAa,CAAA,SAAA;AAAA,QACxB,UAAUA,6BAAa,CAAA,QAAA;AAAA,QACvB,QAAQA,6BAAa,CAAA,UAAA;AAAA,OACvB;AAAA,MACA,MAAM,IAAK,CAAA;AAAA,QACT,MAAA;AAAA,QACA,MAAA;AAAA,QACA,QAAA;AAAA,QACA,MAAA;AAAA,QACA,SAAA;AAAA,QACA,SAAA;AAAA,QACA,QAAA;AAAA,QACA,MAAA;AAAA,OACC,EAAA;AACD,QAAA,MAAM,YAAY,MAAO,CAAA,WAAA;AAAA,UACvB,cAAA,CAAe,iBAAkB,CAAA,GAAA,CAAI,CAAW,OAAA,KAAA;AAAA,YAC9C,OAAQ,CAAA,KAAA;AAAA,YACR,OAAA;AAAA,WACD,CAAA;AAAA,SACH,CAAA;AAEA,QAAM,MAAA,IAAA,GAAOE,4DAA8B,UAAW,CAAA;AAAA,UACpD,MAAA;AAAA,UACA,MAAA;AAAA,UACA,SAAA;AAAA,UACA,MAAA;AAAA,SACD,CAAA,CAAA;AACD,QAAA,MAAM,eAAeC,uBAAO,EAAA,CAAA;AAC5B,QAAA,IAAA,CAAK,KAAK,YAAY,CAAA,CAAA;AAEtB,QAAO,MAAA,CAAA,GAAA;AAAA,UACL,MAAMC,yCAAqB,CAAA;AAAA,YACzB,QAAA;AAAA,YACA,MAAA;AAAA,YACA,QAAA;AAAA,YACA,SAAA;AAAA,YACA,SAAA;AAAA,WACD,CAAA;AAAA,SACH,CAAA;AAEA,QAAA,MAAA,CAAO,IAAI,YAAY,CAAA,CAAA;AACvB,QAAA,MAAA,CAAO,aAAc,CAAA;AAAA,UACnB,KAAO,EAAA,iBAAA;AAAA,UACP,IAAM,EAAA,OAAA;AAAA,SACP,CAAA,CAAA;AAAA,OACH;AAAA,KACD,CAAA,CAAA;AAAA,GACH;AACF,CAAC;;;;"}
@@ -3,28 +3,13 @@
3
3
  var backendCommon = require('@backstage/backend-common');
4
4
  var express = require('express');
5
5
  var Router = require('express-promise-router');
6
+ var RequestValidationContextImpl = require('./validation/RequestValidationContextImpl.cjs.js');
6
7
 
7
8
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
8
9
 
9
10
  var express__default = /*#__PURE__*/_interopDefaultCompat(express);
10
11
  var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
11
12
 
12
- class RequestValidationContextImpl {
13
- #rejectionDetails;
14
- reject(details) {
15
- this.#rejectionDetails = {
16
- status: details?.status ?? 403,
17
- payload: details?.payload ?? {}
18
- };
19
- }
20
- wasRejected() {
21
- return this.#rejectionDetails !== void 0;
22
- }
23
- get rejectionDetails() {
24
- return this.#rejectionDetails;
25
- }
26
- }
27
-
28
13
  class HttpPostIngressEventPublisher {
29
14
  constructor(events, logger, ingresses) {
30
15
  this.events = events;
@@ -60,7 +45,7 @@ class HttpPostIngressEventPublisher {
60
45
  body: request.body,
61
46
  headers: request.headers
62
47
  };
63
- const context = new RequestValidationContextImpl();
48
+ const context = new RequestValidationContextImpl.RequestValidationContextImpl();
64
49
  await validator?.(requestDetails, context);
65
50
  if (context.wasRejected()) {
66
51
  response.status(context.rejectionDetails.status).json(context.rejectionDetails.payload);
@@ -79,4 +64,4 @@ class HttpPostIngressEventPublisher {
79
64
  }
80
65
 
81
66
  exports.HttpPostIngressEventPublisher = HttpPostIngressEventPublisher;
82
- //# sourceMappingURL=HttpPostIngressEventPublisher-D6pQ6awS.cjs.js.map
67
+ //# sourceMappingURL=HttpPostIngressEventPublisher.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HttpPostIngressEventPublisher.cjs.js","sources":["../../../src/service/http/HttpPostIngressEventPublisher.ts"],"sourcesContent":["/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { errorHandler } from '@backstage/backend-common';\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { Config } from '@backstage/config';\nimport {\n EventsService,\n HttpPostIngressOptions,\n RequestValidator,\n} from '@backstage/plugin-events-node';\nimport express from 'express';\nimport Router from 'express-promise-router';\nimport { RequestValidationContextImpl } from './validation';\n\n/**\n * Publishes events received from their origin (e.g., webhook events from an SCM system)\n * via HTTP POST endpoint and passes the request body as event payload to the registered subscribers.\n *\n * @public\n */\n// TODO(pjungermann): add prom metrics? (see plugins/catalog-backend/src/util/metrics.ts, etc.)\nexport class HttpPostIngressEventPublisher {\n static fromConfig(env: {\n config: Config;\n events: EventsService;\n ingresses?: { [topic: string]: Omit<HttpPostIngressOptions, 'topic'> };\n logger: LoggerService;\n }): HttpPostIngressEventPublisher {\n const topics =\n env.config.getOptionalStringArray('events.http.topics') ?? [];\n\n const ingresses = env.ingresses ?? {};\n topics.forEach(topic => {\n // don't overwrite topic settings\n // (e.g., added at the config as well as argument)\n if (!ingresses[topic]) {\n ingresses[topic] = {};\n }\n });\n\n return new HttpPostIngressEventPublisher(env.events, env.logger, ingresses);\n }\n\n private constructor(\n private readonly events: EventsService,\n private readonly logger: LoggerService,\n private readonly ingresses: {\n [topic: string]: Omit<HttpPostIngressOptions, 'topic'>;\n },\n ) {}\n\n bind(router: express.Router): void {\n router.use('/http', this.createRouter(this.ingresses));\n }\n\n private createRouter(ingresses: {\n [topic: string]: Omit<HttpPostIngressOptions, 'topic'>;\n }): express.Router {\n const router = Router();\n router.use(express.json());\n\n Object.keys(ingresses).forEach(topic =>\n this.addRouteForTopic(router, topic, ingresses[topic].validator),\n );\n\n router.use(errorHandler());\n return router;\n }\n\n private addRouteForTopic(\n router: express.Router,\n topic: string,\n validator?: RequestValidator,\n ): void {\n const path = `/${topic}`;\n\n router.post(path, async (request, response) => {\n const requestDetails = {\n body: request.body,\n headers: request.headers,\n };\n const context = new RequestValidationContextImpl();\n await validator?.(requestDetails, context);\n if (context.wasRejected()) {\n response\n .status(context.rejectionDetails!.status)\n .json(context.rejectionDetails!.payload);\n return;\n }\n\n const eventPayload = request.body;\n await this.events.publish({\n topic,\n eventPayload,\n metadata: request.headers,\n });\n\n response.status(202).json({ status: 'accepted' });\n });\n\n // TODO(pjungermann): We don't really know the externally defined path prefix here,\n // however it is more useful for users to have it. Is there a better way?\n this.logger.info(`Registered /api/events/http${path} to receive events`);\n }\n}\n"],"names":["Router","express","errorHandler","RequestValidationContextImpl"],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ class RequestValidationContextImpl {
4
+ #rejectionDetails;
5
+ reject(details) {
6
+ this.#rejectionDetails = {
7
+ status: details?.status ?? 403,
8
+ payload: details?.payload ?? {}
9
+ };
10
+ }
11
+ wasRejected() {
12
+ return this.#rejectionDetails !== void 0;
13
+ }
14
+ get rejectionDetails() {
15
+ return this.#rejectionDetails;
16
+ }
17
+ }
18
+
19
+ exports.RequestValidationContextImpl = RequestValidationContextImpl;
20
+ //# sourceMappingURL=RequestValidationContextImpl.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RequestValidationContextImpl.cjs.js","sources":["../../../../src/service/http/validation/RequestValidationContextImpl.ts"],"sourcesContent":["/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n RequestRejectionDetails,\n RequestValidationContext,\n} from '@backstage/plugin-events-node';\n\nexport class RequestValidationContextImpl implements RequestValidationContext {\n #rejectionDetails: RequestRejectionDetails | undefined;\n\n reject(details?: Partial<RequestRejectionDetails>): void {\n this.#rejectionDetails = {\n status: details?.status ?? 403,\n payload: details?.payload ?? {},\n };\n }\n\n wasRejected(): boolean {\n return this.#rejectionDetails !== undefined;\n }\n\n get rejectionDetails() {\n return this.#rejectionDetails;\n }\n}\n"],"names":[],"mappings":";;AAqBO,MAAM,4BAAiE,CAAA;AAAA,EAC5E,iBAAA,CAAA;AAAA,EAEA,OAAO,OAAkD,EAAA;AACvD,IAAA,IAAA,CAAK,iBAAoB,GAAA;AAAA,MACvB,MAAA,EAAQ,SAAS,MAAU,IAAA,GAAA;AAAA,MAC3B,OAAA,EAAS,OAAS,EAAA,OAAA,IAAW,EAAC;AAAA,KAChC,CAAA;AAAA,GACF;AAAA,EAEA,WAAuB,GAAA;AACrB,IAAA,OAAO,KAAK,iBAAsB,KAAA,KAAA,CAAA,CAAA;AAAA,GACpC;AAAA,EAEA,IAAI,gBAAmB,GAAA;AACrB,IAAA,OAAO,IAAK,CAAA,iBAAA,CAAA;AAAA,GACd;AACF;;;;"}
@@ -0,0 +1,410 @@
1
+ 'use strict';
2
+
3
+ var backendPluginApi = require('@backstage/backend-plugin-api');
4
+ var errors = require('@backstage/errors');
5
+ var types = require('@backstage/types');
6
+
7
+ const WINDOW_MAX_COUNT_DEFAULT = 1e4;
8
+ const WINDOW_MIN_AGE_DEFAULT = { minutes: 10 };
9
+ const WINDOW_MAX_AGE_DEFAULT = { days: 1 };
10
+ const MAX_BATCH_SIZE = 10;
11
+ const LISTENER_CONNECTION_TIMEOUT_MS = 6e4;
12
+ const KEEPALIVE_INTERVAL_MS = 6e4;
13
+ const TABLE_EVENTS = "event_bus_events";
14
+ const TABLE_SUBSCRIPTIONS = "event_bus_subscriptions";
15
+ const TOPIC_PUBLISH = "event_bus_publish";
16
+ function creatorId(credentials) {
17
+ return `service=${credentials.principal.subject}`;
18
+ }
19
+ const migrationsDir = backendPluginApi.resolvePackagePath(
20
+ "@backstage/plugin-events-backend",
21
+ "migrations"
22
+ );
23
+ class DatabaseEventBusListener {
24
+ #client;
25
+ #logger;
26
+ #listeners = /* @__PURE__ */ new Set();
27
+ #isShuttingDown = false;
28
+ #connPromise;
29
+ #connTimeout;
30
+ #keepaliveInterval;
31
+ constructor(client, logger) {
32
+ this.#client = client;
33
+ this.#logger = logger.child({ type: "DatabaseEventBusListener" });
34
+ }
35
+ async setupListener(topics, signal) {
36
+ if (this.#connTimeout) {
37
+ clearTimeout(this.#connTimeout);
38
+ this.#connTimeout = void 0;
39
+ }
40
+ await this.#ensureConnection();
41
+ const updatePromise = new Promise((resolve, reject) => {
42
+ const listener = {
43
+ topics,
44
+ resolve(result) {
45
+ resolve(result);
46
+ cleanup();
47
+ },
48
+ reject(err) {
49
+ reject(err);
50
+ cleanup();
51
+ }
52
+ };
53
+ this.#listeners.add(listener);
54
+ const onAbort = () => {
55
+ this.#listeners.delete(listener);
56
+ this.#maybeTimeoutConnection();
57
+ reject(signal.reason);
58
+ cleanup();
59
+ };
60
+ function cleanup() {
61
+ signal.removeEventListener("abort", onAbort);
62
+ }
63
+ signal.addEventListener("abort", onAbort);
64
+ });
65
+ updatePromise.catch(() => {
66
+ });
67
+ return { waitForUpdate: () => updatePromise };
68
+ }
69
+ async shutdown() {
70
+ if (this.#isShuttingDown) {
71
+ return;
72
+ }
73
+ this.#isShuttingDown = true;
74
+ const conn = await this.#connPromise?.catch(() => void 0);
75
+ if (conn) {
76
+ this.#destroyConnection(conn);
77
+ }
78
+ }
79
+ #handleNotify(topic) {
80
+ this.#logger.debug(`Listener received notification for topic '${topic}'`);
81
+ for (const l of this.#listeners) {
82
+ if (l.topics.has(topic)) {
83
+ l.resolve({ topic });
84
+ this.#listeners.delete(l);
85
+ }
86
+ }
87
+ this.#maybeTimeoutConnection();
88
+ }
89
+ // We don't try to reconnect on error, instead we notify all listeners and let
90
+ // them try to establish a new connection
91
+ #handleError(error) {
92
+ this.#logger.error(
93
+ `Listener connection failed, notifying all listeners`,
94
+ error
95
+ );
96
+ for (const l of this.#listeners) {
97
+ l.reject(new Error("Listener connection failed"));
98
+ }
99
+ this.#listeners.clear();
100
+ this.#maybeTimeoutConnection();
101
+ }
102
+ #maybeTimeoutConnection() {
103
+ if (this.#listeners.size === 0 && !this.#connTimeout) {
104
+ this.#connTimeout = setTimeout(() => {
105
+ this.#connTimeout = void 0;
106
+ this.#connPromise?.then((conn) => {
107
+ this.#logger.info("Listener connection timed out, destroying");
108
+ this.#connPromise = void 0;
109
+ this.#destroyConnection(conn);
110
+ });
111
+ }, LISTENER_CONNECTION_TIMEOUT_MS);
112
+ }
113
+ }
114
+ #destroyConnection(conn) {
115
+ if (this.#keepaliveInterval) {
116
+ clearInterval(this.#keepaliveInterval);
117
+ this.#keepaliveInterval = void 0;
118
+ }
119
+ this.#client.destroyRawConnection(conn).catch((error) => {
120
+ this.#logger.error(`Listener failed to destroy connection`, error);
121
+ });
122
+ conn.removeAllListeners();
123
+ }
124
+ async #ensureConnection() {
125
+ if (this.#isShuttingDown) {
126
+ throw new Error("Listener is shutting down");
127
+ }
128
+ if (this.#connPromise) {
129
+ await this.#connPromise;
130
+ return;
131
+ }
132
+ this.#connPromise = Promise.resolve().then(async () => {
133
+ const conn = await this.#client.acquireRawConnection();
134
+ try {
135
+ await conn.query(`LISTEN ${TOPIC_PUBLISH}`);
136
+ if (this.#keepaliveInterval) {
137
+ clearInterval(this.#keepaliveInterval);
138
+ }
139
+ this.#keepaliveInterval = setInterval(() => {
140
+ conn.query("select 1").catch((error) => {
141
+ this.#connPromise = void 0;
142
+ this.#destroyConnection(conn);
143
+ this.#handleError(new errors.ForwardedError("Keepalive failed", error));
144
+ });
145
+ }, KEEPALIVE_INTERVAL_MS);
146
+ conn.on("notification", (event) => {
147
+ this.#handleNotify(event.payload);
148
+ });
149
+ conn.on("error", (error) => {
150
+ this.#connPromise = void 0;
151
+ this.#destroyConnection(conn);
152
+ this.#handleError(error);
153
+ });
154
+ conn.on("end", (error) => {
155
+ this.#connPromise = void 0;
156
+ this.#destroyConnection(conn);
157
+ this.#handleError(
158
+ error ?? new Error("Connection ended unexpectedly")
159
+ );
160
+ });
161
+ return conn;
162
+ } catch (error) {
163
+ this.#destroyConnection(conn);
164
+ throw error;
165
+ }
166
+ });
167
+ try {
168
+ await this.#connPromise;
169
+ } catch (error) {
170
+ this.#connPromise = void 0;
171
+ throw error;
172
+ }
173
+ }
174
+ }
175
+ class DatabaseEventBusStore {
176
+ static async create(options) {
177
+ const db = await options.database.getClient();
178
+ if (db.client.config.client !== "pg") {
179
+ throw new Error(
180
+ `DatabaseEventBusStore only supports PostgreSQL, got '${db.client.config.client}'`
181
+ );
182
+ }
183
+ if (!options.database.migrations?.skip) {
184
+ await db.migrate.latest({
185
+ directory: migrationsDir
186
+ });
187
+ }
188
+ const listener = new DatabaseEventBusListener(db.client, options.logger);
189
+ const store = new DatabaseEventBusStore(
190
+ db,
191
+ options.logger,
192
+ listener,
193
+ options.window?.maxCount ?? WINDOW_MAX_COUNT_DEFAULT,
194
+ types.durationToMilliseconds(options.window?.minAge ?? WINDOW_MIN_AGE_DEFAULT),
195
+ types.durationToMilliseconds(options.window?.maxAge ?? WINDOW_MAX_AGE_DEFAULT)
196
+ );
197
+ await options.scheduler.scheduleTask({
198
+ id: "event-bus-cleanup",
199
+ frequency: { seconds: 10 },
200
+ timeout: { minutes: 1 },
201
+ initialDelay: { seconds: 10 },
202
+ fn: () => store.#cleanup()
203
+ });
204
+ options.lifecycle.addShutdownHook(async () => {
205
+ await listener.shutdown();
206
+ });
207
+ return store;
208
+ }
209
+ /** @internal */
210
+ static async forTest({
211
+ db,
212
+ logger,
213
+ minAge = 0,
214
+ maxAge = 1e4
215
+ }) {
216
+ await db.migrate.latest({ directory: migrationsDir });
217
+ const store = new DatabaseEventBusStore(
218
+ db,
219
+ logger,
220
+ new DatabaseEventBusListener(db.client, logger),
221
+ 5,
222
+ minAge,
223
+ maxAge
224
+ );
225
+ return Object.assign(store, { clean: () => store.#cleanup() });
226
+ }
227
+ #db;
228
+ #logger;
229
+ #listener;
230
+ #windowMaxCount;
231
+ #windowMinAge;
232
+ #windowMaxAge;
233
+ constructor(db, logger, listener, windowMaxCount, windowMinAge, windowMaxAge) {
234
+ this.#db = db;
235
+ this.#logger = logger;
236
+ this.#listener = listener;
237
+ this.#windowMaxCount = windowMaxCount;
238
+ this.#windowMinAge = windowMinAge;
239
+ this.#windowMaxAge = windowMaxAge;
240
+ }
241
+ async publish(options) {
242
+ const topic = options.event.topic;
243
+ const notifiedSubscribers = options.notifiedSubscribers ?? [];
244
+ const result = await this.#db.into(
245
+ this.#db.raw("?? (??, ??, ??, ??)", [
246
+ TABLE_EVENTS,
247
+ // These are the rows that we insert, and should match the SELECT below
248
+ "created_by",
249
+ "topic",
250
+ "data_json",
251
+ "notified_subscribers"
252
+ ])
253
+ ).insert(
254
+ (q) => q.select(
255
+ this.#db.raw("?", [creatorId(options.credentials)]),
256
+ this.#db.raw("?", [topic]),
257
+ this.#db.raw("?", [
258
+ JSON.stringify({
259
+ payload: options.event.eventPayload,
260
+ metadata: options.event.metadata
261
+ })
262
+ ]),
263
+ this.#db.raw("?", [notifiedSubscribers])
264
+ ).from(TABLE_SUBSCRIPTIONS).whereNotIn("id", notifiedSubscribers).andWhere(this.#db.raw("? = ANY(topics)", [topic])).having(this.#db.raw("count(*)"), ">", 0)
265
+ // Check if there are any results
266
+ ).returning("id");
267
+ if (result.length === 0) {
268
+ return void 0;
269
+ }
270
+ if (result.length > 1) {
271
+ throw new Error(
272
+ `Failed to insert event, unexpectedly updated ${result.length} rows`
273
+ );
274
+ }
275
+ const [{ id }] = result;
276
+ const notifyResult = await this.#db.select(
277
+ this.#db.raw(`pg_notify(?, ?)`, [TOPIC_PUBLISH, topic])
278
+ );
279
+ if (notifyResult?.length !== 1) {
280
+ this.#logger.warn(
281
+ `Failed to notify subscribers of event with ID '${id}' on topic '${topic}'`
282
+ );
283
+ }
284
+ return { eventId: id };
285
+ }
286
+ async upsertSubscription(id, topics, credentials) {
287
+ const [{ max: maxId }] = await this.#db(TABLE_EVENTS).max("id");
288
+ const result = await this.#db(TABLE_SUBSCRIPTIONS).insert({
289
+ id,
290
+ created_by: creatorId(credentials),
291
+ updated_at: this.#db.fn.now(),
292
+ topics,
293
+ read_until: maxId || 0
294
+ }).onConflict("id").merge(["created_by", "topics", "updated_at"]).returning("*");
295
+ if (result.length !== 1) {
296
+ throw new Error(
297
+ `Failed to upsert subscription, updated ${result.length} rows`
298
+ );
299
+ }
300
+ }
301
+ async readSubscription(id) {
302
+ const { rows: result } = await this.#db.raw(
303
+ `
304
+ WITH subscription AS (
305
+ SELECT topics, read_until
306
+ FROM event_bus_subscriptions
307
+ WHERE id = :id
308
+ FOR UPDATE
309
+ ),
310
+ selected_events AS (
311
+ SELECT event_bus_events.*
312
+ FROM event_bus_events
313
+ INNER JOIN subscription
314
+ ON event_bus_events.topic = ANY(subscription.topics)
315
+ WHERE event_bus_events.id > subscription.read_until
316
+ AND NOT :id = ANY(event_bus_events.notified_subscribers)
317
+ ORDER BY event_bus_events.id ASC LIMIT :limit
318
+ ),
319
+ last_event_id AS (
320
+ SELECT max(id) AS last_event_id
321
+ FROM selected_events
322
+ ),
323
+ events_array AS (
324
+ SELECT json_agg(row_to_json(selected_events)) AS events
325
+ FROM selected_events
326
+ )
327
+ UPDATE event_bus_subscriptions
328
+ SET read_until = COALESCE(last_event_id, (SELECT MAX(id) FROM event_bus_events), 0)
329
+ FROM events_array, last_event_id
330
+ WHERE event_bus_subscriptions.id = :id
331
+ RETURNING events_array.events
332
+ `,
333
+ { id, limit: MAX_BATCH_SIZE }
334
+ );
335
+ if (result.length === 0) {
336
+ throw new errors.NotFoundError(`Subscription with ID '${id}' not found`);
337
+ } else if (result.length > 1) {
338
+ throw new Error(
339
+ `Failed to read subscription, unexpectedly updated ${result.length} rows`
340
+ );
341
+ }
342
+ const rows = result[0].events;
343
+ if (!rows || rows.length === 0) {
344
+ return { events: [] };
345
+ }
346
+ return {
347
+ events: rows.map((row) => {
348
+ const { payload, metadata } = JSON.parse(row.data_json);
349
+ return {
350
+ topic: row.topic,
351
+ eventPayload: payload,
352
+ metadata
353
+ };
354
+ })
355
+ };
356
+ }
357
+ async setupListener(subscriptionId, options) {
358
+ const result = await this.#db(TABLE_SUBSCRIPTIONS).select("topics").where({ id: subscriptionId }).first();
359
+ if (!result) {
360
+ throw new errors.NotFoundError(
361
+ `Subscription with ID '${subscriptionId}' not found`
362
+ );
363
+ }
364
+ options.signal.throwIfAborted();
365
+ return this.#listener.setupListener(
366
+ new Set(result.topics ?? []),
367
+ options.signal
368
+ );
369
+ }
370
+ async #cleanup() {
371
+ try {
372
+ const eventCount = await this.#db(TABLE_EVENTS).delete().orWhere(
373
+ (inner) => inner.whereIn(
374
+ "id",
375
+ this.#db.select("id").from(TABLE_EVENTS).orderBy("id", "desc").offset(this.#windowMaxCount)
376
+ ).andWhere(
377
+ "created_at",
378
+ "<",
379
+ new Date(Date.now() - this.#windowMinAge)
380
+ )
381
+ ).orWhere("created_at", "<", new Date(Date.now() - this.#windowMaxAge));
382
+ if (eventCount > 0) {
383
+ this.#logger.info(
384
+ `Event cleanup resulted in ${eventCount} old events being deleted`
385
+ );
386
+ }
387
+ } catch (error) {
388
+ this.#logger.error("Event cleanup failed", error);
389
+ }
390
+ try {
391
+ const [{ min: minId }] = await this.#db(TABLE_EVENTS).min("id");
392
+ let subscriberCount;
393
+ if (minId === null) {
394
+ subscriberCount = await this.#db(TABLE_SUBSCRIPTIONS).delete();
395
+ } else {
396
+ subscriberCount = await this.#db(TABLE_SUBSCRIPTIONS).delete().where("read_until", "<", minId - 1);
397
+ }
398
+ if (subscriberCount > 0) {
399
+ this.#logger.info(
400
+ `Subscription cleanup resulted in ${subscriberCount} stale subscribers being deleted`
401
+ );
402
+ }
403
+ } catch (error) {
404
+ this.#logger.error("Subscription cleanup failed", error);
405
+ }
406
+ }
407
+ }
408
+
409
+ exports.DatabaseEventBusStore = DatabaseEventBusStore;
410
+ //# sourceMappingURL=DatabaseEventBusStore.cjs.js.map