From 21cca8479e76afb0d202cd62dc0e77acc4f1a349 Mon Sep 17 00:00:00 2001 From: Patrick Jungermann Date: Thu, 18 Jan 2024 22:22:44 +0100 Subject: [PATCH] feat(events): add event service Signed-off-by: Patrick Jungermann --- .../events-backend-module-azure/api-report.md | 3 +- .../src/router/AzureDevOpsEventRouter.test.ts | 33 ++-- .../src/router/AzureDevOpsEventRouter.ts | 8 +- ...eventsModuleAzureDevOpsEventRouter.test.ts | 30 ++-- .../eventsModuleAzureDevOpsEventRouter.ts | 12 +- .../api-report.md | 3 +- .../router/BitbucketCloudEventRouter.test.ts | 33 ++-- .../src/router/BitbucketCloudEventRouter.ts | 8 +- ...ntsModuleBitbucketCloudEventRouter.test.ts | 33 ++-- .../eventsModuleBitbucketCloudEventRouter.ts | 12 +- .../api-report.md | 3 +- .../src/router/GerritEventRouter.test.ts | 33 ++-- .../src/router/GerritEventRouter.ts | 8 +- .../eventsModuleGerritEventRouter.test.ts | 30 ++-- .../service/eventsModuleGerritEventRouter.ts | 12 +- .../api-report.md | 3 +- .../src/router/GithubEventRouter.test.ts | 33 ++-- .../src/router/GithubEventRouter.ts | 8 +- .../eventsModuleGithubEventRouter.test.ts | 30 ++-- .../service/eventsModuleGithubEventRouter.ts | 12 +- .../api-report.md | 3 +- .../src/router/GitlabEventRouter.test.ts | 33 ++-- .../src/router/GitlabEventRouter.ts | 8 +- .../eventsModuleGitlabEventRouter.test.ts | 30 ++-- .../service/eventsModuleGitlabEventRouter.ts | 12 +- .../events-backend-test-utils/api-report.md | 20 ++- .../src/testUtils/TestEventBroker.ts | 5 +- .../src/testUtils/TestEventPublisher.ts | 5 +- .../src/testUtils/TestEventService.ts | 48 ++++++ .../src/testUtils/TestEventSubscriber.ts | 5 +- .../src/testUtils/index.ts | 1 + plugins/events-backend/api-report.md | 13 +- .../src/service/DefaultEventBroker.ts | 1 + .../src/service/EventsBackend.ts | 1 + .../src/service/EventsPlugin.test.ts | 49 +++--- .../src/service/EventsPlugin.ts | 53 +------ .../HttpPostIngressEventPublisher.test.ts | 40 ++--- .../http/HttpPostIngressEventPublisher.ts | 27 ++-- plugins/events-node/api-report-alpha.md | 6 +- plugins/events-node/api-report.md | 57 +++++-- plugins/events-node/package.json | 1 + .../src/api/DefaultEventService.test.ts | 148 ++++++++++++++++++ .../src/api/DefaultEventService.ts | 77 +++++++++ plugins/events-node/src/api/EventBroker.ts | 1 + plugins/events-node/src/api/EventPublisher.ts | 1 + .../events-node/src/api/EventRouter.test.ts | 35 ++--- plugins/events-node/src/api/EventRouter.ts | 44 ++++-- plugins/events-node/src/api/EventService.ts | 54 +++++++ .../events-node/src/api/EventSubscriber.ts | 1 + .../src/api/SubTopicEventRouter.test.ts | 31 ++-- .../src/api/SubTopicEventRouter.ts | 15 +- plugins/events-node/src/api/index.ts | 6 + plugins/events-node/src/extensions.ts | 9 ++ plugins/events-node/src/index.ts | 5 + plugins/events-node/src/service.ts | 48 ++++++ yarn.lock | 1 + 56 files changed, 859 insertions(+), 382 deletions(-) create mode 100644 plugins/events-backend-test-utils/src/testUtils/TestEventService.ts create mode 100644 plugins/events-node/src/api/DefaultEventService.test.ts create mode 100644 plugins/events-node/src/api/DefaultEventService.ts create mode 100644 plugins/events-node/src/api/EventService.ts create mode 100644 plugins/events-node/src/service.ts diff --git a/plugins/events-backend-module-azure/api-report.md b/plugins/events-backend-module-azure/api-report.md index 4460aeb510a6f..f9852bbe8d8de 100644 --- a/plugins/events-backend-module-azure/api-report.md +++ b/plugins/events-backend-module-azure/api-report.md @@ -4,11 +4,12 @@ ```ts import { EventParams } from '@backstage/plugin-events-node'; +import { EventService } from '@backstage/plugin-events-node'; import { SubTopicEventRouter } from '@backstage/plugin-events-node'; // @public export class AzureDevOpsEventRouter extends SubTopicEventRouter { - constructor(); + constructor(options: { eventService: EventService }); // (undocumented) protected determineSubTopic(params: EventParams): string | undefined; } diff --git a/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.test.ts b/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.test.ts index 56e761a8fcfc7..701f602119774 100644 --- a/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.test.ts +++ b/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.test.ts @@ -14,37 +14,44 @@ * limitations under the License. */ -import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { TestEventService } from '@backstage/plugin-events-backend-test-utils'; import { AzureDevOpsEventRouter } from './AzureDevOpsEventRouter'; describe('AzureDevOpsEventRouter', () => { - const eventRouter = new AzureDevOpsEventRouter(); + const eventService = new TestEventService(); + const eventRouter = new AzureDevOpsEventRouter({ eventService }); const topic = 'azureDevOps'; const eventPayload = { eventType: 'test.type', test: 'payload' }; const metadata = {}; - it('no $.eventType', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); + beforeEach(() => { + eventService.reset(); + }); + it('subscribed to topic', () => { + eventRouter.subscribe(); + + expect(eventService.subscribed).toHaveLength(1); + expect(eventService.subscribed[0].id).toEqual('AzureDevOpsEventRouter'); + expect(eventService.subscribed[0].topics).toEqual([topic]); + }); + + it('no $.eventType', () => { eventRouter.onEvent({ topic, eventPayload: { invalid: 'payload' }, metadata, }); - expect(eventBroker.published).toEqual([]); + expect(eventService.published).toEqual([]); }); it('with $.eventType', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); - eventRouter.onEvent({ topic, eventPayload, metadata }); - expect(eventBroker.published.length).toBe(1); - expect(eventBroker.published[0].topic).toEqual('azureDevOps.test.type'); - expect(eventBroker.published[0].eventPayload).toEqual(eventPayload); - expect(eventBroker.published[0].metadata).toEqual(metadata); + expect(eventService.published).toHaveLength(1); + expect(eventService.published[0].topic).toEqual('azureDevOps.test.type'); + expect(eventService.published[0].eventPayload).toEqual(eventPayload); + expect(eventService.published[0].metadata).toEqual(metadata); }); }); diff --git a/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.ts b/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.ts index 11dd7546ddbdc..c138ce5eeef46 100644 --- a/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.ts +++ b/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.ts @@ -16,6 +16,7 @@ import { EventParams, + EventService, SubTopicEventRouter, } from '@backstage/plugin-events-node'; @@ -27,8 +28,11 @@ import { * @public */ export class AzureDevOpsEventRouter extends SubTopicEventRouter { - constructor() { - super('azureDevOps'); + constructor(options: { eventService: EventService }) { + super({ + eventService: options.eventService, + topic: 'azureDevOps', + }); } protected determineSubTopic(params: EventParams): string | undefined { diff --git a/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.test.ts b/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.test.ts index d67b5eb071a1f..f01b7581e2498 100644 --- a/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.test.ts +++ b/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.test.ts @@ -14,32 +14,28 @@ * limitations under the License. */ +import { createServiceFactory } from '@backstage/backend-plugin-api'; import { startTestBackend } from '@backstage/backend-test-utils'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { TestEventService } from '@backstage/plugin-events-backend-test-utils'; +import { eventServiceRef } from '@backstage/plugin-events-node'; import { eventsModuleAzureDevOpsEventRouter } from './eventsModuleAzureDevOpsEventRouter'; -import { AzureDevOpsEventRouter } from '../router/AzureDevOpsEventRouter'; describe('eventsModuleAzureDevOpsEventRouter', () => { it('should be correctly wired and set up', async () => { - let addedPublisher: AzureDevOpsEventRouter | undefined; - let addedSubscriber: AzureDevOpsEventRouter | undefined; - const extensionPoint = { - addPublishers: (publisher: any) => { - addedPublisher = publisher; + const eventService = new TestEventService(); + const eventServiceFactory = createServiceFactory({ + service: eventServiceRef, + deps: {}, + async factory({}) { + return eventService; }, - addSubscribers: (subscriber: any) => { - addedSubscriber = subscriber; - }, - }; + }); await startTestBackend({ - extensionPoints: [[eventsExtensionPoint, extensionPoint]], - features: [eventsModuleAzureDevOpsEventRouter()], + features: [eventServiceFactory(), eventsModuleAzureDevOpsEventRouter()], }); - expect(addedPublisher).not.toBeUndefined(); - expect(addedPublisher).toBeInstanceOf(AzureDevOpsEventRouter); - expect(addedSubscriber).not.toBeUndefined(); - expect(addedSubscriber).toBeInstanceOf(AzureDevOpsEventRouter); + expect(eventService.subscribed).toHaveLength(1); + expect(eventService.subscribed[0].id).toEqual('AzureDevOpsEventRouter'); }); }); diff --git a/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.ts b/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.ts index 50bc384502330..06b5e2b1cc594 100644 --- a/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.ts +++ b/plugins/events-backend-module-azure/src/service/eventsModuleAzureDevOpsEventRouter.ts @@ -15,7 +15,7 @@ */ import { createBackendModule } from '@backstage/backend-plugin-api'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { eventServiceRef } from '@backstage/plugin-events-node'; import { AzureDevOpsEventRouter } from '../router/AzureDevOpsEventRouter'; /** @@ -31,13 +31,11 @@ export const eventsModuleAzureDevOpsEventRouter = createBackendModule({ register(env) { env.registerInit({ deps: { - events: eventsExtensionPoint, + eventService: eventServiceRef, }, - async init({ events }) { - const eventRouter = new AzureDevOpsEventRouter(); - - events.addPublishers(eventRouter); - events.addSubscribers(eventRouter); + async init({ eventService }) { + const eventRouter = new AzureDevOpsEventRouter({ eventService }); + await eventRouter.subscribe(); }, }); }, diff --git a/plugins/events-backend-module-bitbucket-cloud/api-report.md b/plugins/events-backend-module-bitbucket-cloud/api-report.md index 4795edd89a843..33dd14f3e3745 100644 --- a/plugins/events-backend-module-bitbucket-cloud/api-report.md +++ b/plugins/events-backend-module-bitbucket-cloud/api-report.md @@ -4,11 +4,12 @@ ```ts import { EventParams } from '@backstage/plugin-events-node'; +import { EventService } from '@backstage/plugin-events-node'; import { SubTopicEventRouter } from '@backstage/plugin-events-node'; // @public export class BitbucketCloudEventRouter extends SubTopicEventRouter { - constructor(); + constructor(options: { eventService: EventService }); // (undocumented) protected determineSubTopic(params: EventParams): string | undefined; } diff --git a/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.test.ts b/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.test.ts index b7a47984e4a69..f90b0fdc2bcaf 100644 --- a/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.test.ts +++ b/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.test.ts @@ -14,33 +14,40 @@ * limitations under the License. */ -import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { TestEventService } from '@backstage/plugin-events-backend-test-utils'; import { BitbucketCloudEventRouter } from './BitbucketCloudEventRouter'; describe('BitbucketCloudEventRouter', () => { - const eventRouter = new BitbucketCloudEventRouter(); + const eventService = new TestEventService(); + const eventRouter = new BitbucketCloudEventRouter({ eventService }); const topic = 'bitbucketCloud'; const eventPayload = { test: 'payload' }; const metadata = { 'x-event-key': 'test:type' }; - it('no x-event-key', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); + beforeEach(() => { + eventService.reset(); + }); + it('subscribed to topic', () => { + eventRouter.subscribe(); + + expect(eventService.subscribed).toHaveLength(1); + expect(eventService.subscribed[0].id).toEqual('BitbucketCloudEventRouter'); + expect(eventService.subscribed[0].topics).toEqual([topic]); + }); + + it('no x-event-key', () => { eventRouter.onEvent({ topic, eventPayload }); - expect(eventBroker.published).toEqual([]); + expect(eventService.published).toEqual([]); }); it('with x-event-key', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); - eventRouter.onEvent({ topic, eventPayload, metadata }); - expect(eventBroker.published.length).toBe(1); - expect(eventBroker.published[0].topic).toEqual('bitbucketCloud.test:type'); - expect(eventBroker.published[0].eventPayload).toEqual(eventPayload); - expect(eventBroker.published[0].metadata).toEqual(metadata); + expect(eventService.published.length).toBe(1); + expect(eventService.published[0].topic).toEqual('bitbucketCloud.test:type'); + expect(eventService.published[0].eventPayload).toEqual(eventPayload); + expect(eventService.published[0].metadata).toEqual(metadata); }); }); diff --git a/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.ts b/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.ts index 8350511d656e1..3309a0d8fdc6d 100644 --- a/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.ts +++ b/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.ts @@ -16,6 +16,7 @@ import { EventParams, + EventService, SubTopicEventRouter, } from '@backstage/plugin-events-node'; @@ -27,8 +28,11 @@ import { * @public */ export class BitbucketCloudEventRouter extends SubTopicEventRouter { - constructor() { - super('bitbucketCloud'); + constructor(options: { eventService: EventService }) { + super({ + eventService: options.eventService, + topic: 'bitbucketCloud', + }); } protected determineSubTopic(params: EventParams): string | undefined { diff --git a/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.test.ts b/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.test.ts index 025d994b4bb82..f5ded9bcccf15 100644 --- a/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.test.ts +++ b/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.test.ts @@ -14,32 +14,31 @@ * limitations under the License. */ +import { createServiceFactory } from '@backstage/backend-plugin-api'; import { startTestBackend } from '@backstage/backend-test-utils'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { TestEventService } from '@backstage/plugin-events-backend-test-utils'; +import { eventServiceRef } from '@backstage/plugin-events-node'; import { eventsModuleBitbucketCloudEventRouter } from './eventsModuleBitbucketCloudEventRouter'; -import { BitbucketCloudEventRouter } from '../router/BitbucketCloudEventRouter'; describe('eventsModuleBitbucketCloudEventRouter', () => { it('should be correctly wired and set up', async () => { - let addedPublisher: BitbucketCloudEventRouter | undefined; - let addedSubscriber: BitbucketCloudEventRouter | undefined; - const extensionPoint = { - addPublishers: (publisher: any) => { - addedPublisher = publisher; + const eventService = new TestEventService(); + const eventServiceFactory = createServiceFactory({ + service: eventServiceRef, + deps: {}, + async factory({}) { + return eventService; }, - addSubscribers: (subscriber: any) => { - addedSubscriber = subscriber; - }, - }; + }); await startTestBackend({ - extensionPoints: [[eventsExtensionPoint, extensionPoint]], - features: [eventsModuleBitbucketCloudEventRouter()], + features: [ + eventServiceFactory(), + eventsModuleBitbucketCloudEventRouter(), + ], }); - expect(addedPublisher).not.toBeUndefined(); - expect(addedPublisher).toBeInstanceOf(BitbucketCloudEventRouter); - expect(addedSubscriber).not.toBeUndefined(); - expect(addedSubscriber).toBeInstanceOf(BitbucketCloudEventRouter); + expect(eventService.subscribed).toHaveLength(1); + expect(eventService.subscribed[0].id).toEqual('BitbucketCloudEventRouter'); }); }); diff --git a/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.ts b/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.ts index 841a463001f4a..b2877bd20703a 100644 --- a/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.ts +++ b/plugins/events-backend-module-bitbucket-cloud/src/service/eventsModuleBitbucketCloudEventRouter.ts @@ -15,7 +15,7 @@ */ import { createBackendModule } from '@backstage/backend-plugin-api'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { eventServiceRef } from '@backstage/plugin-events-node'; import { BitbucketCloudEventRouter } from '../router/BitbucketCloudEventRouter'; /** @@ -31,13 +31,11 @@ export const eventsModuleBitbucketCloudEventRouter = createBackendModule({ register(env) { env.registerInit({ deps: { - events: eventsExtensionPoint, + eventService: eventServiceRef, }, - async init({ events }) { - const eventRouter = new BitbucketCloudEventRouter(); - - events.addPublishers(eventRouter); - events.addSubscribers(eventRouter); + async init({ eventService }) { + const eventRouter = new BitbucketCloudEventRouter({ eventService }); + await eventRouter.subscribe(); }, }); }, diff --git a/plugins/events-backend-module-gerrit/api-report.md b/plugins/events-backend-module-gerrit/api-report.md index ba3c4dd29fdf1..e1c37b5ff3572 100644 --- a/plugins/events-backend-module-gerrit/api-report.md +++ b/plugins/events-backend-module-gerrit/api-report.md @@ -4,11 +4,12 @@ ```ts import { EventParams } from '@backstage/plugin-events-node'; +import { EventService } from '@backstage/plugin-events-node'; import { SubTopicEventRouter } from '@backstage/plugin-events-node'; // @public export class GerritEventRouter extends SubTopicEventRouter { - constructor(); + constructor(options: { eventService: EventService }); // (undocumented) protected determineSubTopic(params: EventParams): string | undefined; } diff --git a/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.test.ts b/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.test.ts index 7302a26012a92..dead0c2c23c12 100644 --- a/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.test.ts +++ b/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.test.ts @@ -14,37 +14,44 @@ * limitations under the License. */ -import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { TestEventService } from '@backstage/plugin-events-backend-test-utils'; import { GerritEventRouter } from './GerritEventRouter'; describe('GerritEventRouter', () => { - const eventRouter = new GerritEventRouter(); + const eventService = new TestEventService(); + const eventRouter = new GerritEventRouter({ eventService }); const topic = 'gerrit'; const eventPayload = { type: 'test-type', test: 'payload' }; const metadata = {}; - it('no $.type', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); + beforeEach(() => { + eventService.reset(); + }); + it('subscribed to topic', () => { + eventRouter.subscribe(); + + expect(eventService.subscribed).toHaveLength(1); + expect(eventService.subscribed[0].id).toEqual('GerritEventRouter'); + expect(eventService.subscribed[0].topics).toEqual([topic]); + }); + + it('no $.type', () => { eventRouter.onEvent({ topic, eventPayload: { invalid: 'payload' }, metadata, }); - expect(eventBroker.published).toEqual([]); + expect(eventService.published).toEqual([]); }); it('with $.type', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); - eventRouter.onEvent({ topic, eventPayload, metadata }); - expect(eventBroker.published.length).toBe(1); - expect(eventBroker.published[0].topic).toEqual('gerrit.test-type'); - expect(eventBroker.published[0].eventPayload).toEqual(eventPayload); - expect(eventBroker.published[0].metadata).toEqual(metadata); + expect(eventService.published.length).toBe(1); + expect(eventService.published[0].topic).toEqual('gerrit.test-type'); + expect(eventService.published[0].eventPayload).toEqual(eventPayload); + expect(eventService.published[0].metadata).toEqual(metadata); }); }); diff --git a/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.ts b/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.ts index 3d97508b62fd5..7016ebc380b19 100644 --- a/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.ts +++ b/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.ts @@ -16,6 +16,7 @@ import { EventParams, + EventService, SubTopicEventRouter, } from '@backstage/plugin-events-node'; @@ -27,8 +28,11 @@ import { * @public */ export class GerritEventRouter extends SubTopicEventRouter { - constructor() { - super('gerrit'); + constructor(options: { eventService: EventService }) { + super({ + eventService: options.eventService, + topic: 'gerrit', + }); } protected determineSubTopic(params: EventParams): string | undefined { diff --git a/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.test.ts b/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.test.ts index c11f4c42db145..3c3545c7334a2 100644 --- a/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.test.ts +++ b/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.test.ts @@ -14,32 +14,28 @@ * limitations under the License. */ +import { createServiceFactory } from '@backstage/backend-plugin-api'; import { startTestBackend } from '@backstage/backend-test-utils'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { eventServiceRef } from '@backstage/plugin-events-node'; +import { TestEventService } from '@backstage/plugin-events-backend-test-utils'; import { eventsModuleGerritEventRouter } from './eventsModuleGerritEventRouter'; -import { GerritEventRouter } from '../router/GerritEventRouter'; describe('eventsModuleGerritEventRouter', () => { it('should be correctly wired and set up', async () => { - let addedPublisher: GerritEventRouter | undefined; - let addedSubscriber: GerritEventRouter | undefined; - const extensionPoint = { - addPublishers: (publisher: any) => { - addedPublisher = publisher; + const eventService = new TestEventService(); + const eventServiceFactory = createServiceFactory({ + service: eventServiceRef, + deps: {}, + async factory({}) { + return eventService; }, - addSubscribers: (subscriber: any) => { - addedSubscriber = subscriber; - }, - }; + }); await startTestBackend({ - extensionPoints: [[eventsExtensionPoint, extensionPoint]], - features: [eventsModuleGerritEventRouter()], + features: [eventServiceFactory(), eventsModuleGerritEventRouter()], }); - expect(addedPublisher).not.toBeUndefined(); - expect(addedPublisher).toBeInstanceOf(GerritEventRouter); - expect(addedSubscriber).not.toBeUndefined(); - expect(addedSubscriber).toBeInstanceOf(GerritEventRouter); + expect(eventService.subscribed).toHaveLength(1); + expect(eventService.subscribed[0].id).toEqual('GerritEventRouter'); }); }); diff --git a/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.ts b/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.ts index 780ff7f8787fd..494fcd15e7448 100644 --- a/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.ts +++ b/plugins/events-backend-module-gerrit/src/service/eventsModuleGerritEventRouter.ts @@ -15,7 +15,7 @@ */ import { createBackendModule } from '@backstage/backend-plugin-api'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { eventServiceRef } from '@backstage/plugin-events-node'; import { GerritEventRouter } from '../router/GerritEventRouter'; /** @@ -31,13 +31,11 @@ export const eventsModuleGerritEventRouter = createBackendModule({ register(env) { env.registerInit({ deps: { - events: eventsExtensionPoint, + eventService: eventServiceRef, }, - async init({ events }) { - const eventRouter = new GerritEventRouter(); - - events.addPublishers(eventRouter); - events.addSubscribers(eventRouter); + async init({ eventService }) { + const eventRouter = new GerritEventRouter({ eventService }); + await eventRouter.subscribe(); }, }); }, diff --git a/plugins/events-backend-module-github/api-report.md b/plugins/events-backend-module-github/api-report.md index bcf9b8ed7439f..517f15c6d8950 100644 --- a/plugins/events-backend-module-github/api-report.md +++ b/plugins/events-backend-module-github/api-report.md @@ -5,6 +5,7 @@ ```ts import { Config } from '@backstage/config'; import { EventParams } from '@backstage/plugin-events-node'; +import { EventService } from '@backstage/plugin-events-node'; import { RequestValidator } from '@backstage/plugin-events-node'; import { SubTopicEventRouter } from '@backstage/plugin-events-node'; @@ -15,7 +16,7 @@ export function createGithubSignatureValidator( // @public export class GithubEventRouter extends SubTopicEventRouter { - constructor(); + constructor(options: { eventService: EventService }); // (undocumented) protected determineSubTopic(params: EventParams): string | undefined; } diff --git a/plugins/events-backend-module-github/src/router/GithubEventRouter.test.ts b/plugins/events-backend-module-github/src/router/GithubEventRouter.test.ts index 14cf6b993322f..839358a76a802 100644 --- a/plugins/events-backend-module-github/src/router/GithubEventRouter.test.ts +++ b/plugins/events-backend-module-github/src/router/GithubEventRouter.test.ts @@ -14,33 +14,40 @@ * limitations under the License. */ -import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { TestEventService } from '@backstage/plugin-events-backend-test-utils'; import { GithubEventRouter } from './GithubEventRouter'; describe('GithubEventRouter', () => { - const eventRouter = new GithubEventRouter(); + const eventService = new TestEventService(); + const eventRouter = new GithubEventRouter({ eventService }); const topic = 'github'; const eventPayload = { test: 'payload' }; const metadata = { 'x-github-event': 'test_type' }; - it('no x-github-event', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); + beforeEach(() => { + eventService.reset(); + }); + it('subscribed to topic', () => { + eventRouter.subscribe(); + + expect(eventService.subscribed).toHaveLength(1); + expect(eventService.subscribed[0].id).toEqual('GithubEventRouter'); + expect(eventService.subscribed[0].topics).toEqual([topic]); + }); + + it('no x-github-event', () => { eventRouter.onEvent({ topic, eventPayload }); - expect(eventBroker.published).toEqual([]); + expect(eventService.published).toEqual([]); }); it('with x-github-event', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); - eventRouter.onEvent({ topic, eventPayload, metadata }); - expect(eventBroker.published.length).toBe(1); - expect(eventBroker.published[0].topic).toEqual('github.test_type'); - expect(eventBroker.published[0].eventPayload).toEqual(eventPayload); - expect(eventBroker.published[0].metadata).toEqual(metadata); + expect(eventService.published.length).toBe(1); + expect(eventService.published[0].topic).toEqual('github.test_type'); + expect(eventService.published[0].eventPayload).toEqual(eventPayload); + expect(eventService.published[0].metadata).toEqual(metadata); }); }); diff --git a/plugins/events-backend-module-github/src/router/GithubEventRouter.ts b/plugins/events-backend-module-github/src/router/GithubEventRouter.ts index 10dd1c55c6e7d..ba4da5c77fa6a 100644 --- a/plugins/events-backend-module-github/src/router/GithubEventRouter.ts +++ b/plugins/events-backend-module-github/src/router/GithubEventRouter.ts @@ -16,6 +16,7 @@ import { EventParams, + EventService, SubTopicEventRouter, } from '@backstage/plugin-events-node'; @@ -27,8 +28,11 @@ import { * @public */ export class GithubEventRouter extends SubTopicEventRouter { - constructor() { - super('github'); + constructor(options: { eventService: EventService }) { + super({ + eventService: options.eventService, + topic: 'github', + }); } protected determineSubTopic(params: EventParams): string | undefined { diff --git a/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.test.ts b/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.test.ts index 02151d0dcf23e..682b254beda9e 100644 --- a/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.test.ts +++ b/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.test.ts @@ -14,32 +14,28 @@ * limitations under the License. */ +import { createServiceFactory } from '@backstage/backend-plugin-api'; import { startTestBackend } from '@backstage/backend-test-utils'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { TestEventService } from '@backstage/plugin-events-backend-test-utils'; +import { eventServiceRef } from '@backstage/plugin-events-node'; import { eventsModuleGithubEventRouter } from './eventsModuleGithubEventRouter'; -import { GithubEventRouter } from '../router/GithubEventRouter'; describe('eventsModuleGithubEventRouter', () => { it('should be correctly wired and set up', async () => { - let addedPublisher: GithubEventRouter | undefined; - let addedSubscriber: GithubEventRouter | undefined; - const extensionPoint = { - addPublishers: (publisher: any) => { - addedPublisher = publisher; + const eventService = new TestEventService(); + const eventServiceFactory = createServiceFactory({ + service: eventServiceRef, + deps: {}, + async factory({}) { + return eventService; }, - addSubscribers: (subscriber: any) => { - addedSubscriber = subscriber; - }, - }; + }); await startTestBackend({ - extensionPoints: [[eventsExtensionPoint, extensionPoint]], - features: [eventsModuleGithubEventRouter()], + features: [eventServiceFactory(), eventsModuleGithubEventRouter()], }); - expect(addedPublisher).not.toBeUndefined(); - expect(addedPublisher).toBeInstanceOf(GithubEventRouter); - expect(addedSubscriber).not.toBeUndefined(); - expect(addedSubscriber).toBeInstanceOf(GithubEventRouter); + expect(eventService.subscribed).toHaveLength(1); + expect(eventService.subscribed[0].id).toEqual('GithubEventRouter'); }); }); diff --git a/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.ts b/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.ts index 093307dfaf2dd..02f31bc172c13 100644 --- a/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.ts +++ b/plugins/events-backend-module-github/src/service/eventsModuleGithubEventRouter.ts @@ -15,7 +15,7 @@ */ import { createBackendModule } from '@backstage/backend-plugin-api'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { eventServiceRef } from '@backstage/plugin-events-node'; import { GithubEventRouter } from '../router/GithubEventRouter'; /** @@ -31,13 +31,11 @@ export const eventsModuleGithubEventRouter = createBackendModule({ register(env) { env.registerInit({ deps: { - events: eventsExtensionPoint, + eventService: eventServiceRef, }, - async init({ events }) { - const eventRouter = new GithubEventRouter(); - - events.addPublishers(eventRouter); - events.addSubscribers(eventRouter); + async init({ eventService }) { + const eventRouter = new GithubEventRouter({ eventService }); + await eventRouter.subscribe(); }, }); }, diff --git a/plugins/events-backend-module-gitlab/api-report.md b/plugins/events-backend-module-gitlab/api-report.md index 8a0c513857f38..8c35ea46c2420 100644 --- a/plugins/events-backend-module-gitlab/api-report.md +++ b/plugins/events-backend-module-gitlab/api-report.md @@ -5,6 +5,7 @@ ```ts import { Config } from '@backstage/config'; import { EventParams } from '@backstage/plugin-events-node'; +import { EventService } from '@backstage/plugin-events-node'; import { RequestValidator } from '@backstage/plugin-events-node'; import { SubTopicEventRouter } from '@backstage/plugin-events-node'; @@ -13,7 +14,7 @@ export function createGitlabTokenValidator(config: Config): RequestValidator; // @public export class GitlabEventRouter extends SubTopicEventRouter { - constructor(); + constructor(options: { eventService: EventService }); // (undocumented) protected determineSubTopic(params: EventParams): string | undefined; } diff --git a/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.test.ts b/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.test.ts index 6ced12d3cb84c..233acbe5bbf51 100644 --- a/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.test.ts +++ b/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.test.ts @@ -14,37 +14,44 @@ * limitations under the License. */ -import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { TestEventService } from '@backstage/plugin-events-backend-test-utils'; import { GitlabEventRouter } from './GitlabEventRouter'; describe('GitlabEventRouter', () => { - const eventRouter = new GitlabEventRouter(); + const eventService = new TestEventService(); + const eventRouter = new GitlabEventRouter({ eventService }); const topic = 'gitlab'; const eventPayload = { event_name: 'test_type', test: 'payload' }; const metadata = {}; - it('no $.event_name', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); + beforeEach(() => { + eventService.reset(); + }); + it('subscribed to topic', () => { + eventRouter.subscribe(); + + expect(eventService.subscribed).toHaveLength(1); + expect(eventService.subscribed[0].id).toEqual('GitlabEventRouter'); + expect(eventService.subscribed[0].topics).toEqual([topic]); + }); + + it('no $.event_name', () => { eventRouter.onEvent({ topic, eventPayload: { invalid: 'payload' }, metadata, }); - expect(eventBroker.published).toEqual([]); + expect(eventService.published).toEqual([]); }); it('with $.event_name', () => { - const eventBroker = new TestEventBroker(); - eventRouter.setEventBroker(eventBroker); - eventRouter.onEvent({ topic, eventPayload, metadata }); - expect(eventBroker.published.length).toBe(1); - expect(eventBroker.published[0].topic).toEqual('gitlab.test_type'); - expect(eventBroker.published[0].eventPayload).toEqual(eventPayload); - expect(eventBroker.published[0].metadata).toEqual(metadata); + expect(eventService.published.length).toBe(1); + expect(eventService.published[0].topic).toEqual('gitlab.test_type'); + expect(eventService.published[0].eventPayload).toEqual(eventPayload); + expect(eventService.published[0].metadata).toEqual(metadata); }); }); diff --git a/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.ts b/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.ts index 16324340eeaac..74e95256a2636 100644 --- a/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.ts +++ b/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.ts @@ -16,6 +16,7 @@ import { EventParams, + EventService, SubTopicEventRouter, } from '@backstage/plugin-events-node'; @@ -27,8 +28,11 @@ import { * @public */ export class GitlabEventRouter extends SubTopicEventRouter { - constructor() { - super('gitlab'); + constructor(options: { eventService: EventService }) { + super({ + eventService: options.eventService, + topic: 'gitlab', + }); } protected determineSubTopic(params: EventParams): string | undefined { diff --git a/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.test.ts b/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.test.ts index 34a68ccbe444d..66a9e8f89bec1 100644 --- a/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.test.ts +++ b/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.test.ts @@ -14,32 +14,28 @@ * limitations under the License. */ +import { createServiceFactory } from '@backstage/backend-plugin-api'; import { startTestBackend } from '@backstage/backend-test-utils'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { TestEventService } from '@backstage/plugin-events-backend-test-utils'; +import { eventServiceRef } from '@backstage/plugin-events-node'; import { eventsModuleGitlabEventRouter } from './eventsModuleGitlabEventRouter'; -import { GitlabEventRouter } from '../router/GitlabEventRouter'; describe('eventsModuleGitlabEventRouter', () => { it('should be correctly wired and set up', async () => { - let addedPublisher: GitlabEventRouter | undefined; - let addedSubscriber: GitlabEventRouter | undefined; - const extensionPoint = { - addPublishers: (publisher: any) => { - addedPublisher = publisher; + const eventService = new TestEventService(); + const eventServiceFactory = createServiceFactory({ + service: eventServiceRef, + deps: {}, + async factory({}) { + return eventService; }, - addSubscribers: (subscriber: any) => { - addedSubscriber = subscriber; - }, - }; + }); await startTestBackend({ - extensionPoints: [[eventsExtensionPoint, extensionPoint]], - features: [eventsModuleGitlabEventRouter()], + features: [eventServiceFactory(), eventsModuleGitlabEventRouter()], }); - expect(addedPublisher).not.toBeUndefined(); - expect(addedPublisher).toBeInstanceOf(GitlabEventRouter); - expect(addedSubscriber).not.toBeUndefined(); - expect(addedSubscriber).toBeInstanceOf(GitlabEventRouter); + expect(eventService.subscribed).toHaveLength(1); + expect(eventService.subscribed[0].id).toEqual('GitlabEventRouter'); }); }); diff --git a/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.ts b/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.ts index fc44e95057fb2..d4a0be779ddb6 100644 --- a/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.ts +++ b/plugins/events-backend-module-gitlab/src/service/eventsModuleGitlabEventRouter.ts @@ -15,7 +15,7 @@ */ import { createBackendModule } from '@backstage/backend-plugin-api'; -import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; +import { eventServiceRef } from '@backstage/plugin-events-node'; import { GitlabEventRouter } from '../router/GitlabEventRouter'; /** @@ -31,13 +31,11 @@ export const eventsModuleGitlabEventRouter = createBackendModule({ register(env) { env.registerInit({ deps: { - events: eventsExtensionPoint, + eventService: eventServiceRef, }, - async init({ events }) { - const eventRouter = new GitlabEventRouter(); - - events.addPublishers(eventRouter); - events.addSubscribers(eventRouter); + async init({ eventService }) { + const eventRouter = new GitlabEventRouter({ eventService }); + await eventRouter.subscribe(); }, }); }, diff --git a/plugins/events-backend-test-utils/api-report.md b/plugins/events-backend-test-utils/api-report.md index 46131c4244703..bc3627dc532d3 100644 --- a/plugins/events-backend-test-utils/api-report.md +++ b/plugins/events-backend-test-utils/api-report.md @@ -6,9 +6,11 @@ import { EventBroker } from '@backstage/plugin-events-node'; import { EventParams } from '@backstage/plugin-events-node'; import { EventPublisher } from '@backstage/plugin-events-node'; +import { EventService } from '@backstage/plugin-events-node'; import { EventSubscriber } from '@backstage/plugin-events-node'; +import { EventSubscriptionOptions } from '@backstage/plugin-events-node'; -// @public (undocumented) +// @public @deprecated (undocumented) export class TestEventBroker implements EventBroker { // (undocumented) publish(params: EventParams): Promise; @@ -22,7 +24,7 @@ export class TestEventBroker implements EventBroker { readonly subscribed: EventSubscriber[]; } -// @public (undocumented) +// @public @deprecated (undocumented) export class TestEventPublisher implements EventPublisher { // (undocumented) get eventBroker(): EventBroker | undefined; @@ -31,6 +33,20 @@ export class TestEventPublisher implements EventPublisher { } // @public (undocumented) +export class TestEventService implements EventService { + // (undocumented) + publish(params: EventParams): Promise; + // (undocumented) + get published(): EventParams[]; + // (undocumented) + reset(): void; + // (undocumented) + subscribe(options: EventSubscriptionOptions): Promise; + // (undocumented) + get subscribed(): EventSubscriptionOptions[]; +} + +// @public @deprecated (undocumented) export class TestEventSubscriber implements EventSubscriber { constructor(name: string, topics: string[]); // (undocumented) diff --git a/plugins/events-backend-test-utils/src/testUtils/TestEventBroker.ts b/plugins/events-backend-test-utils/src/testUtils/TestEventBroker.ts index c697a6506fbf7..db55629d566c6 100644 --- a/plugins/events-backend-test-utils/src/testUtils/TestEventBroker.ts +++ b/plugins/events-backend-test-utils/src/testUtils/TestEventBroker.ts @@ -20,7 +20,10 @@ import { EventSubscriber, } from '@backstage/plugin-events-node'; -/** @public */ +/** + * @public + * @deprecated use `TestEventService` instead + */ export class TestEventBroker implements EventBroker { readonly published: EventParams[] = []; readonly subscribed: EventSubscriber[] = []; diff --git a/plugins/events-backend-test-utils/src/testUtils/TestEventPublisher.ts b/plugins/events-backend-test-utils/src/testUtils/TestEventPublisher.ts index c1b2038afb1da..c1afbbeaadb62 100644 --- a/plugins/events-backend-test-utils/src/testUtils/TestEventPublisher.ts +++ b/plugins/events-backend-test-utils/src/testUtils/TestEventPublisher.ts @@ -16,7 +16,10 @@ import { EventBroker, EventPublisher } from '@backstage/plugin-events-node'; -/** @public */ +/** + * @public + * @deprecated `EventPublisher` was replaced by `EventService.publish` + */ export class TestEventPublisher implements EventPublisher { #eventBroker?: EventBroker; diff --git a/plugins/events-backend-test-utils/src/testUtils/TestEventService.ts b/plugins/events-backend-test-utils/src/testUtils/TestEventService.ts new file mode 100644 index 0000000000000..5e9e6a8f84e55 --- /dev/null +++ b/plugins/events-backend-test-utils/src/testUtils/TestEventService.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + EventParams, + EventService, + EventSubscriptionOptions, +} from '@backstage/plugin-events-node'; + +/** @public */ +export class TestEventService implements EventService { + #published: EventParams[] = []; + #subscribed: EventSubscriptionOptions[] = []; + + async publish(params: EventParams): Promise { + this.#published.push(params); + } + + async subscribe(options: EventSubscriptionOptions): Promise { + this.#subscribed.push(options); + } + + get published(): EventParams[] { + return this.#published; + } + + get subscribed(): EventSubscriptionOptions[] { + return this.#subscribed; + } + + reset(): void { + this.#published = []; + this.#subscribed = []; + } +} diff --git a/plugins/events-backend-test-utils/src/testUtils/TestEventSubscriber.ts b/plugins/events-backend-test-utils/src/testUtils/TestEventSubscriber.ts index ef5758b804df6..758dc78f035a0 100644 --- a/plugins/events-backend-test-utils/src/testUtils/TestEventSubscriber.ts +++ b/plugins/events-backend-test-utils/src/testUtils/TestEventSubscriber.ts @@ -16,7 +16,10 @@ import { EventParams, EventSubscriber } from '@backstage/plugin-events-node'; -/** @public */ +/** + * @public + * @deprecated `EventSubscriber` was replaced by `EventService.subscribe`. + */ export class TestEventSubscriber implements EventSubscriber { readonly name: string; readonly topics: string[]; diff --git a/plugins/events-backend-test-utils/src/testUtils/index.ts b/plugins/events-backend-test-utils/src/testUtils/index.ts index a571ba30754f1..26ce8d0f23ae4 100644 --- a/plugins/events-backend-test-utils/src/testUtils/index.ts +++ b/plugins/events-backend-test-utils/src/testUtils/index.ts @@ -16,4 +16,5 @@ export { TestEventBroker } from './TestEventBroker'; export { TestEventPublisher } from './TestEventPublisher'; +export { TestEventService } from './TestEventService'; export { TestEventSubscriber } from './TestEventSubscriber'; diff --git a/plugins/events-backend/api-report.md b/plugins/events-backend/api-report.md index aeb6f9363d058..e1a4ddc601621 100644 --- a/plugins/events-backend/api-report.md +++ b/plugins/events-backend/api-report.md @@ -7,12 +7,14 @@ import { Config } from '@backstage/config'; import { EventBroker } from '@backstage/plugin-events-node'; import { EventParams } from '@backstage/plugin-events-node'; import { EventPublisher } from '@backstage/plugin-events-node'; +import { EventService } from '@backstage/plugin-events-node'; import { EventSubscriber } from '@backstage/plugin-events-node'; import express from 'express'; import { HttpPostIngressOptions } from '@backstage/plugin-events-node'; import { Logger } from 'winston'; +import { LoggerService } from '@backstage/backend-plugin-api'; -// @public +// @public @deprecated export class DefaultEventBroker implements EventBroker { constructor(logger: Logger); // (undocumented) @@ -23,7 +25,7 @@ export class DefaultEventBroker implements EventBroker { ): void; } -// @public +// @public @deprecated export class EventsBackend { constructor(logger: Logger); // (undocumented) @@ -40,18 +42,17 @@ export class EventsBackend { } // @public -export class HttpPostIngressEventPublisher implements EventPublisher { +export class HttpPostIngressEventPublisher { // (undocumented) bind(router: express.Router): void; // (undocumented) static fromConfig(env: { config: Config; + eventService: EventService; ingresses?: { [topic: string]: Omit; }; - logger: Logger; + logger: LoggerService; }): HttpPostIngressEventPublisher; - // (undocumented) - setEventBroker(eventBroker: EventBroker): Promise; } ``` diff --git a/plugins/events-backend/src/service/DefaultEventBroker.ts b/plugins/events-backend/src/service/DefaultEventBroker.ts index c3824b3e7d3f8..4aeaeeac623a9 100644 --- a/plugins/events-backend/src/service/DefaultEventBroker.ts +++ b/plugins/events-backend/src/service/DefaultEventBroker.ts @@ -27,6 +27,7 @@ import { Logger } from 'winston'; * Events will not be persisted in any form. * * @public + * @deprecated use `DefaultEventService` from `@backstage/plugin-events-node` instead */ // TODO(pjungermann): add prom metrics? (see plugins/catalog-backend/src/util/metrics.ts, etc.) export class DefaultEventBroker implements EventBroker { diff --git a/plugins/events-backend/src/service/EventsBackend.ts b/plugins/events-backend/src/service/EventsBackend.ts index 4415b8703a9b6..215773649cd23 100644 --- a/plugins/events-backend/src/service/EventsBackend.ts +++ b/plugins/events-backend/src/service/EventsBackend.ts @@ -26,6 +26,7 @@ import { DefaultEventBroker } from './DefaultEventBroker'; * A builder that helps wire up all component parts of the event management. * * @public + * @deprecated `EventBroker`, `EventPublisher`, and `EventSubscriber` got replaced by `EventService` and its methods. */ export class EventsBackend { private eventBroker: EventBroker; diff --git a/plugins/events-backend/src/service/EventsPlugin.test.ts b/plugins/events-backend/src/service/EventsPlugin.test.ts index e65147ebbb8ba..61ce155ca314a 100644 --- a/plugins/events-backend/src/service/EventsPlugin.test.ts +++ b/plugins/events-backend/src/service/EventsPlugin.test.ts @@ -21,12 +21,9 @@ import { createServiceFactory, } from '@backstage/backend-plugin-api'; import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { eventServiceRef } from '@backstage/plugin-events-node'; import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; -import { - TestEventBroker, - TestEventPublisher, - TestEventSubscriber, -} from '@backstage/plugin-events-backend-test-utils'; +import { TestEventService } from '@backstage/plugin-events-backend-test-utils'; import express from 'express'; import Router from 'express-promise-router'; import request from 'supertest'; @@ -34,9 +31,14 @@ import { eventsPlugin } from './EventsPlugin'; describe('eventPlugin', () => { it('should be initialized properly', async () => { - const eventBroker = new TestEventBroker(); - const publisher = new TestEventPublisher(); - const subscriber = new TestEventSubscriber('sub', ['fake']); + const eventService = new TestEventService(); + const eventServiceFactory = createServiceFactory({ + service: eventServiceRef, + deps: {}, + async factory({}) { + return eventService; + }, + }); const httpRouter = Router(); httpRouter.use(express.json()); @@ -52,9 +54,9 @@ describe('eventPlugin', () => { events: eventsExtensionPoint, }, async init({ events }) { - events.setEventBroker(eventBroker); - events.addPublishers(publisher); - events.addSubscribers(subscriber); + events.addHttpPostIngress({ + topic: 'fake-ext', + }); }, }); }, @@ -63,6 +65,7 @@ describe('eventPlugin', () => { await startTestBackend({ extensionPoints: [], features: [ + eventServiceFactory(), eventsPlugin(), testModule(), mockServices.logger.factory(), @@ -83,18 +86,24 @@ describe('eventPlugin', () => { ], }); - expect(publisher.eventBroker).toBe(eventBroker); - expect(eventBroker.subscribed.length).toEqual(1); - expect(eventBroker.subscribed[0]).toBe(subscriber); - - const response = await request(app) + const response1 = await request(app) .post('/http/fake') .timeout(1000) .send({ test: 'fake' }); - expect(response.status).toBe(202); + expect(response1.status).toBe(202); - expect(eventBroker.published.length).toEqual(1); - expect(eventBroker.published[0].topic).toEqual('fake'); - expect(eventBroker.published[0].eventPayload).toEqual({ test: 'fake' }); + const response2 = await request(app) + .post('/http/fake-ext') + .timeout(1000) + .send({ test: 'fake-ext' }); + expect(response2.status).toBe(202); + + expect(eventService.published).toHaveLength(2); + expect(eventService.published[0].topic).toEqual('fake'); + expect(eventService.published[0].eventPayload).toEqual({ test: 'fake' }); + expect(eventService.published[1].topic).toEqual('fake-ext'); + expect(eventService.published[1].eventPayload).toEqual({ + test: 'fake-ext', + }); }); }); diff --git a/plugins/events-backend/src/service/EventsPlugin.ts b/plugins/events-backend/src/service/EventsPlugin.ts index 27456b5b2aa34..34dab67742338 100644 --- a/plugins/events-backend/src/service/EventsPlugin.ts +++ b/plugins/events-backend/src/service/EventsPlugin.ts @@ -18,59 +18,30 @@ import { createBackendPlugin, coreServices, } from '@backstage/backend-plugin-api'; -import { loggerToWinstonLogger } from '@backstage/backend-common'; import { eventsExtensionPoint, EventsExtensionPoint, } from '@backstage/plugin-events-node/alpha'; import { - EventBroker, - EventPublisher, - EventSubscriber, + eventServiceRef, HttpPostIngressOptions, } from '@backstage/plugin-events-node'; -import { DefaultEventBroker } from './DefaultEventBroker'; import Router from 'express-promise-router'; import { HttpPostIngressEventPublisher } from './http'; class EventsExtensionPointImpl implements EventsExtensionPoint { - #eventBroker: EventBroker | undefined; #httpPostIngresses: HttpPostIngressOptions[] = []; - #publishers: EventPublisher[] = []; - #subscribers: EventSubscriber[] = []; - setEventBroker(eventBroker: EventBroker): void { - this.#eventBroker = eventBroker; - } + setEventBroker(_: any): void {} - addPublishers( - ...publishers: Array> - ): void { - this.#publishers.push(...publishers.flat()); - } + addPublishers(_: any): void {} - addSubscribers( - ...subscribers: Array> - ): void { - this.#subscribers.push(...subscribers.flat()); - } + addSubscribers(_: any): void {} addHttpPostIngress(options: HttpPostIngressOptions) { this.#httpPostIngresses.push(options); } - get eventBroker() { - return this.#eventBroker; - } - - get publishers() { - return this.#publishers; - } - - get subscribers() { - return this.#subscribers; - } - get httpPostIngresses() { return this.#httpPostIngresses; } @@ -90,12 +61,11 @@ export const eventsPlugin = createBackendPlugin({ env.registerInit({ deps: { config: coreServices.rootConfig, + eventService: eventServiceRef, logger: coreServices.logger, router: coreServices.httpRouter, }, - async init({ config, logger, router }) { - const winstonLogger = loggerToWinstonLogger(logger); - + async init({ config, eventService, logger, router }) { const ingresses = Object.fromEntries( extensionPoint.httpPostIngresses.map(ingress => [ ingress.topic, @@ -105,20 +75,13 @@ export const eventsPlugin = createBackendPlugin({ const http = HttpPostIngressEventPublisher.fromConfig({ config, + eventService, ingresses, - logger: winstonLogger, + logger, }); const eventsRouter = Router(); http.bind(eventsRouter); router.use(eventsRouter); - - const eventBroker = - extensionPoint.eventBroker ?? new DefaultEventBroker(winstonLogger); - - eventBroker.subscribe(extensionPoint.subscribers); - [extensionPoint.publishers, http] - .flat() - .forEach(publisher => publisher.setEventBroker(eventBroker)); }, }); }, diff --git a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts index 72e0b94c57a2a..f4a9007b3ecb0 100644 --- a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts +++ b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts @@ -16,7 +16,7 @@ import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; -import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { TestEventService } from '@backstage/plugin-events-backend-test-utils'; import express from 'express'; import Router from 'express-promise-router'; import request from 'supertest'; @@ -36,9 +36,11 @@ describe('HttpPostIngressEventPublisher', () => { const router = Router(); const app = express().use(router); + const eventService = new TestEventService(); const publisher = HttpPostIngressEventPublisher.fromConfig({ config, + eventService, ingresses: { testB: {}, }, @@ -46,9 +48,6 @@ describe('HttpPostIngressEventPublisher', () => { }); publisher.bind(router); - const eventBroker = new TestEventBroker(); - await publisher.setEventBroker(eventBroker); - const notFoundResponse = await request(app) .post('/http/unknown') .timeout(1000) @@ -69,18 +68,18 @@ describe('HttpPostIngressEventPublisher', () => { .send({ testB: 'data' }); expect(response2.status).toBe(202); - expect(eventBroker.published.length).toEqual(2); - expect(eventBroker.published[0].topic).toEqual('testA'); - expect(eventBroker.published[0].eventPayload).toEqual({ testA: 'data' }); - expect(eventBroker.published[0].metadata).toEqual( + expect(eventService.published).toHaveLength(2); + expect(eventService.published[0].topic).toEqual('testA'); + expect(eventService.published[0].eventPayload).toEqual({ testA: 'data' }); + expect(eventService.published[0].metadata).toEqual( expect.objectContaining({ 'content-type': 'application/json', 'x-custom-header': 'test-value', }), ); - expect(eventBroker.published[1].topic).toEqual('testB'); - expect(eventBroker.published[1].eventPayload).toEqual({ testB: 'data' }); - expect(eventBroker.published[1].metadata).toEqual( + expect(eventService.published[1].topic).toEqual('testB'); + expect(eventService.published[1].eventPayload).toEqual({ testB: 'data' }); + expect(eventService.published[1].metadata).toEqual( expect.objectContaining({ 'content-type': 'application/json', 'x-custom-header': 'test-value', @@ -99,9 +98,11 @@ describe('HttpPostIngressEventPublisher', () => { const router = Router(); const app = express().use(router); + const eventService = new TestEventService(); const publisher = HttpPostIngressEventPublisher.fromConfig({ config, + eventService, ingresses: { testB: { validator: async (req, context) => { @@ -146,9 +147,6 @@ describe('HttpPostIngressEventPublisher', () => { }); publisher.bind(router); - const eventBroker = new TestEventBroker(); - await publisher.setEventBroker(eventBroker); - const response1 = await request(app) .post('/http/testA') .timeout(1000) @@ -191,12 +189,12 @@ describe('HttpPostIngressEventPublisher', () => { expect(response6.status).toBe(403); expect(response6.body).toEqual({}); - expect(eventBroker.published.length).toEqual(2); - expect(eventBroker.published[0].topic).toEqual('testA'); - expect(eventBroker.published[0].eventPayload).toEqual({ test: 'data' }); - expect(eventBroker.published[1].topic).toEqual('testB'); - expect(eventBroker.published[1].eventPayload).toEqual({ test: 'data' }); - expect(eventBroker.published[1].metadata).toEqual( + expect(eventService.published).toHaveLength(2); + expect(eventService.published[0].topic).toEqual('testA'); + expect(eventService.published[0].eventPayload).toEqual({ test: 'data' }); + expect(eventService.published[1].topic).toEqual('testB'); + expect(eventService.published[1].eventPayload).toEqual({ test: 'data' }); + expect(eventService.published[1].metadata).toEqual( expect.objectContaining({ 'x-test-signature': 'testB-signature', }), @@ -205,10 +203,12 @@ describe('HttpPostIngressEventPublisher', () => { it('without configuration', async () => { const config = new ConfigReader({}); + const eventService = new TestEventService(); expect(() => HttpPostIngressEventPublisher.fromConfig({ config, + eventService, logger, }), ).not.toThrow(); diff --git a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts index b5a6ccbca1ff7..6496bd838b050 100644 --- a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts +++ b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts @@ -15,16 +15,15 @@ */ import { errorHandler } from '@backstage/backend-common'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { - EventBroker, - EventPublisher, + EventService, HttpPostIngressOptions, RequestValidator, } from '@backstage/plugin-events-node'; import express from 'express'; import Router from 'express-promise-router'; -import { Logger } from 'winston'; import { RequestValidationContextImpl } from './validation'; /** @@ -34,13 +33,12 @@ import { RequestValidationContextImpl } from './validation'; * @public */ // TODO(pjungermann): add prom metrics? (see plugins/catalog-backend/src/util/metrics.ts, etc.) -export class HttpPostIngressEventPublisher implements EventPublisher { - private eventBroker?: EventBroker; - +export class HttpPostIngressEventPublisher { static fromConfig(env: { config: Config; + eventService: EventService; ingresses?: { [topic: string]: Omit }; - logger: Logger; + logger: LoggerService; }): HttpPostIngressEventPublisher { const topics = env.config.getOptionalStringArray('events.http.topics') ?? []; @@ -54,11 +52,16 @@ export class HttpPostIngressEventPublisher implements EventPublisher { } }); - return new HttpPostIngressEventPublisher(env.logger, ingresses); + return new HttpPostIngressEventPublisher( + env.eventService, + env.logger, + ingresses, + ); } private constructor( - private readonly logger: Logger, + private readonly eventService: EventService, + private readonly logger: LoggerService, private readonly ingresses: { [topic: string]: Omit; }, @@ -68,10 +71,6 @@ export class HttpPostIngressEventPublisher implements EventPublisher { router.use('/http', this.createRouter(this.ingresses)); } - async setEventBroker(eventBroker: EventBroker): Promise { - this.eventBroker = eventBroker; - } - private createRouter(ingresses: { [topic: string]: Omit; }): express.Router { @@ -108,7 +107,7 @@ export class HttpPostIngressEventPublisher implements EventPublisher { } const eventPayload = request.body; - await this.eventBroker!.publish({ + await this.eventService.publish({ topic, eventPayload, metadata: request.headers, diff --git a/plugins/events-node/api-report-alpha.md b/plugins/events-node/api-report-alpha.md index fd30f54d4522e..f61048ac948c8 100644 --- a/plugins/events-node/api-report-alpha.md +++ b/plugins/events-node/api-report-alpha.md @@ -13,15 +13,15 @@ import { HttpPostIngressOptions } from '@backstage/plugin-events-node'; export interface EventsExtensionPoint { // (undocumented) addHttpPostIngress(options: HttpPostIngressOptions): void; - // (undocumented) + // @deprecated (undocumented) addPublishers( ...publishers: Array> ): void; - // (undocumented) + // @deprecated (undocumented) addSubscribers( ...subscribers: Array> ): void; - // (undocumented) + // @deprecated (undocumented) setEventBroker(eventBroker: EventBroker): void; } diff --git a/plugins/events-node/api-report.md b/plugins/events-node/api-report.md index dfda48d97e59e..c2e5333f3b261 100644 --- a/plugins/events-node/api-report.md +++ b/plugins/events-node/api-report.md @@ -3,7 +3,23 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { LoggerService } from '@backstage/backend-plugin-api'; +import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceRef } from '@backstage/backend-plugin-api'; + // @public +export class DefaultEventService implements EventService { + // (undocumented) + static create(options: { + logger: LoggerService; + }): Promise; + // (undocumented) + publish(params: EventParams): Promise; + // (undocumented) + subscribe(options: EventSubscriptionOptions): Promise; +} + +// @public @deprecated export interface EventBroker { publish(params: EventParams): Promise; subscribe( @@ -18,32 +34,54 @@ export interface EventParams { topic: string; } -// @public +// @public @deprecated export interface EventPublisher { // (undocumented) setEventBroker(eventBroker: EventBroker): Promise; } // @public -export abstract class EventRouter implements EventPublisher, EventSubscriber { +export abstract class EventRouter { + protected constructor(options: { + eventService: EventService; + topics: string[]; + }); // (undocumented) protected abstract determineDestinationTopic( params: EventParams, ): string | undefined; // (undocumented) onEvent(params: EventParams): Promise; - // (undocumented) - setEventBroker(eventBroker: EventBroker): Promise; - // (undocumented) - abstract supportsEventTopics(): string[]; + subscribe(): Promise; +} + +// @public +export interface EventService { + publish(params: EventParams): Promise; + subscribe(options: EventSubscriptionOptions): Promise; } // @public +const eventServiceFactory: () => ServiceFactory; +export default eventServiceFactory; +export { eventServiceFactory }; + +// @public +export const eventServiceRef: ServiceRef; + +// @public @deprecated export interface EventSubscriber { onEvent(params: EventParams): Promise; supportsEventTopics(): string[]; } +// @public (undocumented) +export type EventSubscriptionOptions = { + id: string; + topics: string[]; + onEvent: OnEventHandler; +}; + // @public (undocumented) export interface HttpPostIngressOptions { // (undocumented) @@ -52,6 +90,9 @@ export interface HttpPostIngressOptions { validator?: RequestValidator; } +// @public (undocumented) +export type OnEventHandler = (params: EventParams) => Promise; + // @public (undocumented) export interface RequestDetails { body: unknown; @@ -79,12 +120,10 @@ export type RequestValidator = ( // @public export abstract class SubTopicEventRouter extends EventRouter { - protected constructor(topic: string); + protected constructor(options: { eventService: EventService; topic: string }); // (undocumented) protected determineDestinationTopic(params: EventParams): string | undefined; // (undocumented) protected abstract determineSubTopic(params: EventParams): string | undefined; - // (undocumented) - supportsEventTopics(): string[]; } ``` diff --git a/plugins/events-node/package.json b/plugins/events-node/package.json index d2a9e5424d403..77c3296e0c8a3 100644 --- a/plugins/events-node/package.json +++ b/plugins/events-node/package.json @@ -42,6 +42,7 @@ "postpack": "backstage-cli package postpack" }, "dependencies": { + "@backstage/backend-common": "workspace:^", "@backstage/backend-plugin-api": "workspace:^" }, "devDependencies": { diff --git a/plugins/events-node/src/api/DefaultEventService.test.ts b/plugins/events-node/src/api/DefaultEventService.test.ts new file mode 100644 index 0000000000000..d7c6c8b972c1c --- /dev/null +++ b/plugins/events-node/src/api/DefaultEventService.test.ts @@ -0,0 +1,148 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getVoidLogger } from '@backstage/backend-common'; +import { DefaultEventService } from './DefaultEventService'; +import { EventParams } from './EventParams'; + +const logger = getVoidLogger(); + +describe('DefaultEventService', () => { + it('passes events to interested subscribers', async () => { + const eventService = await DefaultEventService.create({ logger }); + const eventsSubscriber1: EventParams[] = []; + const eventsSubscriber2: EventParams[] = []; + + await eventService.subscribe({ + id: 'subscriber1', + topics: ['topicA', 'topicB'], + onEvent: async event => { + eventsSubscriber1.push(event); + }, + }); + await eventService.subscribe({ + id: 'subscriber2', + topics: ['topicB', 'topicC'], + onEvent: async event => { + eventsSubscriber2.push(event); + }, + }); + await eventService.publish({ + topic: 'topicA', + eventPayload: { test: 'topicA' }, + }); + await eventService.publish({ + topic: 'topicB', + eventPayload: { test: 'topicB' }, + }); + await eventService.publish({ + topic: 'topicC', + eventPayload: { test: 'topicC' }, + }); + await eventService.publish({ + topic: 'topicD', + eventPayload: { test: 'topicD' }, + }); + + expect(eventsSubscriber1.map(event => event.topic)).toEqual([ + 'topicA', + 'topicB', + ]); + expect( + eventsSubscriber1.filter(event => event.topic === 'topicA'), + ).toHaveLength(1); + expect( + eventsSubscriber1.filter(event => event.topic === 'topicA')[0], + ).toEqual({ + topic: 'topicA', + eventPayload: { test: 'topicA' }, + }); + expect( + eventsSubscriber1.filter(event => event.topic === 'topicB'), + ).toHaveLength(1); + expect( + eventsSubscriber1.filter(event => event.topic === 'topicB')[0], + ).toEqual({ + topic: 'topicB', + eventPayload: { test: 'topicB' }, + }); + + expect(eventsSubscriber2.map(event => event.topic)).toEqual([ + 'topicB', + 'topicC', + ]); + expect( + eventsSubscriber2.filter(event => event.topic === 'topicB'), + ).toHaveLength(1); + expect( + eventsSubscriber2.filter(event => event.topic === 'topicB')[0], + ).toEqual({ + topic: 'topicB', + eventPayload: { test: 'topicB' }, + }); + expect( + eventsSubscriber2.filter(event => event.topic === 'topicC'), + ).toHaveLength(1); + expect( + eventsSubscriber2.filter(event => event.topic === 'topicC')[0], + ).toEqual({ + topic: 'topicC', + eventPayload: { test: 'topicC' }, + }); + }); + + it('logs errors from subscribers', async () => { + const topic = 'testTopic'; + + const errorSpy = jest.spyOn(logger, 'error'); + const eventService = await DefaultEventService.create({ logger }); + + await eventService.subscribe({ + id: 'subscriber1', + topics: [topic], + onEvent: event => { + throw new Error(`NOPE ${event.eventPayload}`); + }, + }); + await eventService.publish({ topic, eventPayload: '1' }); + + expect(errorSpy).toHaveBeenCalledTimes(1); + expect(errorSpy).toHaveBeenCalledWith( + 'Subscriber "subscriber1" failed to process event for topic "testTopic"', + new Error('NOPE 1'), + ); + + await eventService.subscribe({ + id: 'subscriber2', + topics: [topic], + onEvent: event => { + throw new Error(`NOPE ${event.eventPayload}`); + }, + }); + await eventService.publish({ topic, eventPayload: '2' }); + + // With two subscribers we should not halt on the first error but call all subscribers + expect(errorSpy).toHaveBeenCalledTimes(3); + expect(errorSpy).toHaveBeenCalledWith( + 'Subscriber "subscriber1" failed to process event for topic "testTopic"', + new Error('NOPE 2'), + ); + expect(errorSpy).toHaveBeenCalledWith( + 'Subscriber "subscriber2" failed to process event for topic "testTopic"', + new Error('NOPE 2'), + ); + }); +}); diff --git a/plugins/events-node/src/api/DefaultEventService.ts b/plugins/events-node/src/api/DefaultEventService.ts new file mode 100644 index 0000000000000..11d93bf6a2e47 --- /dev/null +++ b/plugins/events-node/src/api/DefaultEventService.ts @@ -0,0 +1,77 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LoggerService } from '@backstage/backend-plugin-api'; +import { EventParams } from './EventParams'; +import { + EventService, + EventSubscriptionOptions, + OnEventHandler, +} from './EventService'; + +/** + * In-process event broker which will pass the event to all registered subscribers + * interested in it. + * Events will not be persisted in any form. + * Events will not be passed to subscribers at other instances of the same cluster. + * + * @public + */ +// TODO(pjungermann): add prom metrics? (see plugins/catalog-backend/src/util/metrics.ts, etc.) +export class DefaultEventService implements EventService { + private readonly subscribers: { + [topic: string]: { + [id: string]: OnEventHandler; + }; + } = {}; + + private constructor(private readonly logger: LoggerService) {} + + static async create(options: { + logger: LoggerService; + }): Promise { + return new DefaultEventService(options.logger); + } + + async publish(params: EventParams): Promise { + this.logger.debug( + `Event received: topic=${params.topic}, metadata=${JSON.stringify( + params.metadata, + )}, payload=${JSON.stringify(params.eventPayload)}`, + ); + + const subscribed = this.subscribers[params.topic] ?? {}; + await Promise.all( + Object.entries(subscribed).map(async ([id, onEvent]) => { + try { + await onEvent(params); + } catch (error) { + this.logger.error( + `Subscriber "${id}" failed to process event for topic "${params.topic}"`, + error, + ); + } + }), + ); + } + + async subscribe(options: EventSubscriptionOptions): Promise { + options.topics.forEach(topic => { + this.subscribers[topic] = this.subscribers[topic] ?? {}; + this.subscribers[topic][options.id] = options.onEvent; + }); + } +} diff --git a/plugins/events-node/src/api/EventBroker.ts b/plugins/events-node/src/api/EventBroker.ts index 736c2a2bf0a7b..ad9dbdd3f6710 100644 --- a/plugins/events-node/src/api/EventBroker.ts +++ b/plugins/events-node/src/api/EventBroker.ts @@ -23,6 +23,7 @@ import { EventSubscriber } from './EventSubscriber'; * others can subscribe for future events for topics they are interested in. * * @public + * @deprecated use `EventService` instead */ export interface EventBroker { /** diff --git a/plugins/events-node/src/api/EventPublisher.ts b/plugins/events-node/src/api/EventPublisher.ts index 285f427804af1..9c5cea00356de 100644 --- a/plugins/events-node/src/api/EventPublisher.ts +++ b/plugins/events-node/src/api/EventPublisher.ts @@ -23,6 +23,7 @@ import { EventBroker } from './EventBroker'; * or from event brokers, queues, etc. * * @public + * @deprecated not needed anymore when using `EventService` */ export interface EventPublisher { setEventBroker(eventBroker: EventBroker): Promise; diff --git a/plugins/events-node/src/api/EventRouter.test.ts b/plugins/events-node/src/api/EventRouter.test.ts index 551c5ea67dc1f..43a22f97df864 100644 --- a/plugins/events-node/src/api/EventRouter.test.ts +++ b/plugins/events-node/src/api/EventRouter.test.ts @@ -14,11 +14,15 @@ * limitations under the License. */ -import { EventBroker } from './EventBroker'; import { EventParams } from './EventParams'; import { EventRouter } from './EventRouter'; +import { EventService } from './EventService'; class TestEventRouter extends EventRouter { + constructor(eventService: EventService) { + super({ eventService, topics: ['my-topic'] }); + } + protected determineDestinationTopic(params: EventParams): string | undefined { const payload = params.eventPayload as { value?: number }; if (payload.value === undefined) { @@ -27,26 +31,21 @@ class TestEventRouter extends EventRouter { return payload.value % 2 === 0 ? 'even' : 'odd'; } - - supportsEventTopics(): string[] { - return ['my-topic']; - } } describe('EventRouter', () => { - const eventRouter = new TestEventRouter(); + const published: EventParams[] = []; + const eventService: EventService = { + publish: async event => { + published.push(event); + }, + subscribe: async _subscription => {}, + }; + const eventRouter = new TestEventRouter(eventService); const topic = 'my-topic'; const metadata = { random: 'metadata' }; it('no destination topic', async () => { - const published: EventParams[] = []; - const eventBroker = { - publish: (params: EventParams) => { - published.push(params); - }, - } as EventBroker; - await eventRouter.setEventBroker(eventBroker); - await eventRouter.onEvent({ topic, eventPayload: { discarded: 'event' }, @@ -57,14 +56,6 @@ describe('EventRouter', () => { }); it('with destination topic', async () => { - const published: EventParams[] = []; - const eventBroker = { - publish: (params: EventParams) => { - published.push(params); - }, - } as EventBroker; - await eventRouter.setEventBroker(eventBroker); - const payloadEven = { value: 2 }; const payloadOdd = { value: 3 }; await eventRouter.onEvent({ topic, eventPayload: payloadEven, metadata }); diff --git a/plugins/events-node/src/api/EventRouter.ts b/plugins/events-node/src/api/EventRouter.ts index b435ef15f4e5e..c758623d538ae 100644 --- a/plugins/events-node/src/api/EventRouter.ts +++ b/plugins/events-node/src/api/EventRouter.ts @@ -14,10 +14,8 @@ * limitations under the License. */ -import { EventBroker } from './EventBroker'; import { EventParams } from './EventParams'; -import { EventPublisher } from './EventPublisher'; -import { EventSubscriber } from './EventSubscriber'; +import { EventService } from './EventService'; /** * Subscribes to a topic and - depending on a set of conditions - @@ -26,13 +24,41 @@ import { EventSubscriber } from './EventSubscriber'; * @see {@link https://www.enterpriseintegrationpatterns.com/MessageRouter.html | Message Router pattern}. * @public */ -export abstract class EventRouter implements EventPublisher, EventSubscriber { - private eventBroker?: EventBroker; +export abstract class EventRouter { + private readonly eventService: EventService; + private readonly topics: string[]; + private subscribed: boolean = false; + + protected constructor(options: { + eventService: EventService; + topics: string[]; + }) { + this.eventService = options.eventService; + this.topics = options.topics; + } protected abstract determineDestinationTopic( params: EventParams, ): string | undefined; + /** + * Subscribes itself to the topic(s), + * after which events potentially can be received + * and processed by {@link EventRouter.onEvent}. + */ + async subscribe(): Promise { + if (this.subscribed) { + return; + } + + await this.eventService.subscribe({ + id: this.constructor.name, + topics: this.topics, + onEvent: this.onEvent, + }); + this.subscribed = true; + } + async onEvent(params: EventParams): Promise { const topic = this.determineDestinationTopic(params); @@ -41,15 +67,9 @@ export abstract class EventRouter implements EventPublisher, EventSubscriber { } // republish to different topic - this.eventBroker?.publish({ + await this.eventService.publish({ ...params, topic, }); } - - async setEventBroker(eventBroker: EventBroker): Promise { - this.eventBroker = eventBroker; - } - - abstract supportsEventTopics(): string[]; } diff --git a/plugins/events-node/src/api/EventService.ts b/plugins/events-node/src/api/EventService.ts new file mode 100644 index 0000000000000..a303b3a1534ce --- /dev/null +++ b/plugins/events-node/src/api/EventService.ts @@ -0,0 +1,54 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EventParams } from './EventParams'; + +/** + * Allows a decoupled and asynchronous communication between components. + * Components can publish events for a given topic and + * others can subscribe for future events for topics they are interested in. + * + * @public + */ +export interface EventService { + /** + * Publishes an event for the topic. + * + * @param params - parameters for the to be published event. + */ + publish(params: EventParams): Promise; + + /** + * Subscribes to one or more topics, registering an event handler for them. + * + * @param options - event subscription options. + */ + subscribe(options: EventSubscriptionOptions): Promise; +} + +/** + * @public + */ +export type EventSubscriptionOptions = { + id: string; + topics: string[]; + onEvent: OnEventHandler; +}; + +/** + * @public + */ +export type OnEventHandler = (params: EventParams) => Promise; diff --git a/plugins/events-node/src/api/EventSubscriber.ts b/plugins/events-node/src/api/EventSubscriber.ts index 439f49b8901b6..d1cb7e29b8f14 100644 --- a/plugins/events-node/src/api/EventSubscriber.ts +++ b/plugins/events-node/src/api/EventSubscriber.ts @@ -22,6 +22,7 @@ import { EventParams } from './EventParams'; * or other actions to react on events. * * @public + * @deprecated not needed anymore when using `EventService` */ export interface EventSubscriber { /** diff --git a/plugins/events-node/src/api/SubTopicEventRouter.test.ts b/plugins/events-node/src/api/SubTopicEventRouter.test.ts index d5c79895a5df5..a7fb95dc568d1 100644 --- a/plugins/events-node/src/api/SubTopicEventRouter.test.ts +++ b/plugins/events-node/src/api/SubTopicEventRouter.test.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { EventBroker } from './EventBroker'; import { EventParams } from './EventParams'; +import { EventService } from './EventService'; import { SubTopicEventRouter } from './SubTopicEventRouter'; class TestSubTopicEventRouter extends SubTopicEventRouter { - constructor() { - super('my-topic'); + constructor(eventService: EventService) { + super({ eventService, topic: 'my-topic' }); } protected determineSubTopic(params: EventParams): string | undefined { @@ -29,34 +29,25 @@ class TestSubTopicEventRouter extends SubTopicEventRouter { } describe('SubTopicEventRouter', () => { - const eventRouter = new TestSubTopicEventRouter(); + const published: EventParams[] = []; + const eventService: EventService = { + publish: async event => { + published.push(event); + }, + subscribe: async _subscription => {}, + }; + const eventRouter = new TestSubTopicEventRouter(eventService); const topic = 'my-topic'; const eventPayload = { test: 'payload' }; const metadata = { 'x-my-event': 'test.type' }; it('no x-my-event', async () => { - const published: EventParams[] = []; - const eventBroker = { - publish: (params: EventParams) => { - published.push(params); - }, - } as EventBroker; - await eventRouter.setEventBroker(eventBroker); - await eventRouter.onEvent({ topic, eventPayload }); expect(published).toEqual([]); }); it('with x-my-event', async () => { - const published: EventParams[] = []; - const eventBroker = { - publish: (params: EventParams) => { - published.push(params); - }, - } as EventBroker; - await eventRouter.setEventBroker(eventBroker); - await eventRouter.onEvent({ topic, eventPayload, metadata }); expect(published.length).toBe(1); diff --git a/plugins/events-node/src/api/SubTopicEventRouter.ts b/plugins/events-node/src/api/SubTopicEventRouter.ts index 04abe1400907e..d9a1e37ac316f 100644 --- a/plugins/events-node/src/api/SubTopicEventRouter.ts +++ b/plugins/events-node/src/api/SubTopicEventRouter.ts @@ -16,6 +16,7 @@ import { EventParams } from './EventParams'; import { EventRouter } from './EventRouter'; +import { EventService } from './EventService'; /** * Subscribes to the provided (generic) topic @@ -27,8 +28,14 @@ import { EventRouter } from './EventRouter'; * @public */ export abstract class SubTopicEventRouter extends EventRouter { - protected constructor(private readonly topic: string) { - super(); + protected constructor(options: { + eventService: EventService; + topic: string; + }) { + super({ + eventService: options.eventService, + topics: [options.topic], + }); } protected abstract determineSubTopic(params: EventParams): string | undefined; @@ -37,8 +44,4 @@ export abstract class SubTopicEventRouter extends EventRouter { const subTopic = this.determineSubTopic(params); return subTopic ? `${params.topic}.${subTopic}` : undefined; } - - supportsEventTopics(): string[] { - return [this.topic]; - } } diff --git a/plugins/events-node/src/api/index.ts b/plugins/events-node/src/api/index.ts index 91711c0e38507..85c9fd425cf42 100644 --- a/plugins/events-node/src/api/index.ts +++ b/plugins/events-node/src/api/index.ts @@ -18,6 +18,12 @@ export type { EventBroker } from './EventBroker'; export type { EventParams } from './EventParams'; export type { EventPublisher } from './EventPublisher'; export { EventRouter } from './EventRouter'; +export type { + EventService, + EventSubscriptionOptions, + OnEventHandler, +} from './EventService'; export type { EventSubscriber } from './EventSubscriber'; +export { DefaultEventService } from './DefaultEventService'; export * from './http'; export { SubTopicEventRouter } from './SubTopicEventRouter'; diff --git a/plugins/events-node/src/extensions.ts b/plugins/events-node/src/extensions.ts index 2e44b381af411..fcb65ef307f40 100644 --- a/plugins/events-node/src/extensions.ts +++ b/plugins/events-node/src/extensions.ts @@ -26,12 +26,21 @@ import { * @alpha */ export interface EventsExtensionPoint { + /** + * @deprecated use `eventServiceRef` and `eventServiceFactory` instead + */ setEventBroker(eventBroker: EventBroker): void; + /** + * @deprecated use `EventService.publish` instead + */ addPublishers( ...publishers: Array> ): void; + /** + * @deprecated use `EventService.subscribe` instead + */ addSubscribers( ...subscribers: Array> ): void; diff --git a/plugins/events-node/src/index.ts b/plugins/events-node/src/index.ts index 2bdf456f13e78..79872def8a0c0 100644 --- a/plugins/events-node/src/index.ts +++ b/plugins/events-node/src/index.ts @@ -21,3 +21,8 @@ */ export * from './api'; +export { + eventServiceFactory, + eventServiceFactory as default, + eventServiceRef, +} from './service'; diff --git a/plugins/events-node/src/service.ts b/plugins/events-node/src/service.ts new file mode 100644 index 0000000000000..626d036bcef1b --- /dev/null +++ b/plugins/events-node/src/service.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + coreServices, + createServiceFactory, + createServiceRef, +} from '@backstage/backend-plugin-api'; +import { EventService, DefaultEventService } from './api'; + +/** + * The EventService that allows to publish events, and subscribe to topics. + * Uses the `root` scope so that events can be shared across all plugins, modules, and more. + * + * @public + */ +export const eventServiceRef = createServiceRef({ + id: 'events.service', + scope: 'root', +}); + +/** + * Factory for the {@link eventServiceRef}. + * + * @public + */ +export const eventServiceFactory = createServiceFactory({ + service: eventServiceRef, + deps: { + logger: coreServices.rootLogger, + }, + async factory({ logger }) { + return DefaultEventService.create({ logger }); + }, +}); diff --git a/yarn.lock b/yarn.lock index 1d180ec720929..a6fc317a9f1a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6405,6 +6405,7 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-events-node@workspace:plugins/events-node" dependencies: + "@backstage/backend-common": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/cli": "workspace:^" languageName: unknown