feat(events): add events management capabilities

This change introduces some new plugins which provide the basics
for managing events inside of backstage.
Hereby, it offers extension points to add event publishers and subscribers
as well as to exchange the event broker implementation.

- `@backstage/plugin-events-backend`:
  backend for the events management which connects all parts
  and provides a simple in-memory event broker
- `@backstage/plugin-events-node`:
  interfaces and API for `@backstage/plugin-events-backend`
- `@backstage/plugin-events-test-utils`:
  test utilities like implementations useful for writing tests at modules

All plugins support the new backend-plugin-api.

Relates-to: #11082
Signed-off-by: Patrick Jungermann <Patrick.Jungermann@gmail.com>
This commit is contained in:
Patrick Jungermann
2022-10-04 16:35:26 +02:00
parent dfbe08a24b
commit 7bbd2403a1
40 changed files with 1541 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
---
'@backstage/plugin-events-backend': minor
'@backstage/plugin-events-node': minor
'@backstage/plugin-events-backend-test-utils': minor
---
Adds a new backend plugin plugin-events-backend for managing events.
plugin-events-node exposes interfaces which can be used by modules.
plugin-events-backend-test-utils provides utilities which can be used while writing tests e.g. for modules.
Please find more information at
https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md.
+3
View File
@@ -36,6 +36,9 @@ yarn.lock @backstage/reviewers @backst
/plugins/code-coverage-backend @backstage/reviewers @alde @nissayeva
/plugins/cost-insights @backstage/reviewers @backstage/silver-lining
/plugins/cost-insights-* @backstage/reviewers @backstage/silver-lining
/plugins/events-backend @backstage/reviewers @pjungermann
/plugins/events-backend-test-utils @backstage/reviewers @pjungermann
/plugins/events-node @backstage/reviewers @pjungermann
/plugins/explore @backstage/reviewers @backstage/sda-se-reviewers
/plugins/explore-react @backstage/reviewers @backstage/sda-se-reviewers
/plugins/fossa @backstage/reviewers @backstage/sda-se-reviewers
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
@@ -0,0 +1,4 @@
# plugin-events-backend-test-utils
Houses implementations of plugin-events-node interfaces
which can be useful for test for events-backend and its modules.
@@ -0,0 +1,47 @@
## API Report File for "@backstage/plugin-events-backend-test-utils"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { EventBroker } from '@backstage/plugin-events-node';
import { EventParams } from '@backstage/plugin-events-node';
import { EventPublisher } from '@backstage/plugin-events-node';
import { EventSubscriber } from '@backstage/plugin-events-node';
// @public (undocumented)
export class TestEventBroker implements EventBroker {
// (undocumented)
publish(params: EventParams): Promise<void>;
// (undocumented)
readonly published: EventParams[];
// (undocumented)
subscribe(
...subscribers: Array<EventSubscriber | Array<EventSubscriber>>
): void;
// (undocumented)
readonly subscribed: EventSubscriber[];
}
// @public (undocumented)
export class TestEventPublisher implements EventPublisher {
// (undocumented)
get eventBroker(): EventBroker | undefined;
// (undocumented)
setEventBroker(eventBroker: EventBroker): Promise<void>;
}
// @public (undocumented)
export class TestEventSubscriber implements EventSubscriber {
constructor(name: string, topics: string[]);
// (undocumented)
readonly name: string;
// (undocumented)
onEvent(params: EventParams): Promise<void>;
// (undocumented)
readonly receivedEvents: Record<string, EventParams[]>;
// (undocumented)
supportsEventTopics(): string[];
// (undocumented)
readonly topics: string[];
}
```
@@ -0,0 +1,34 @@
{
"name": "@backstage/plugin-events-backend-test-utils",
"description": "The plugin-events-backend-test-utils for @backstage/plugin-events-node",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "node-library"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/plugin-events-node": "workspace:^"
},
"devDependencies": {
"@backstage/cli": "workspace:^"
},
"files": [
"dist"
]
}
@@ -0,0 +1,23 @@
/*
* 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.
*/
/**
* The events-test-utils module for `@backstage/plugin-events-node`.
*
* @packageDocumentation
*/
export * from './testUtils';
@@ -0,0 +1,37 @@
/*
* 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 {
EventBroker,
EventParams,
EventSubscriber,
} from '@backstage/plugin-events-node';
/** @public */
export class TestEventBroker implements EventBroker {
readonly published: EventParams[] = [];
readonly subscribed: EventSubscriber[] = [];
async publish(params: EventParams): Promise<void> {
this.published.push(params);
}
subscribe(
...subscribers: Array<EventSubscriber | Array<EventSubscriber>>
): void {
this.subscribed.push(...subscribers.flat());
}
}
@@ -0,0 +1,30 @@
/*
* 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 { EventBroker, EventPublisher } from '@backstage/plugin-events-node';
/** @public */
export class TestEventPublisher implements EventPublisher {
#eventBroker?: EventBroker;
async setEventBroker(eventBroker: EventBroker): Promise<void> {
this.#eventBroker = eventBroker;
}
get eventBroker() {
return this.#eventBroker;
}
}
@@ -0,0 +1,39 @@
/*
* 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 { EventParams, EventSubscriber } from '@backstage/plugin-events-node';
/** @public */
export class TestEventSubscriber implements EventSubscriber {
readonly name: string;
readonly topics: string[];
readonly receivedEvents: Record<string, EventParams[]> = {};
constructor(name: string, topics: string[]) {
this.name = name;
this.topics = topics;
}
supportsEventTopics(): string[] {
return this.topics;
}
async onEvent(params: EventParams): Promise<void> {
this.receivedEvents[params.topic] = this.receivedEvents[params.topic] ?? [];
this.receivedEvents[params.topic].push(params);
}
}
@@ -0,0 +1,19 @@
/*
* 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.
*/
export { TestEventBroker } from './TestEventBroker';
export { TestEventPublisher } from './TestEventPublisher';
export { TestEventSubscriber } from './TestEventSubscriber';
+1
View File
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+124
View File
@@ -0,0 +1,124 @@
# events-backend
Welcome to the events-backend backend plugin!
This plugin provides the wiring of all extension points
for managing events as defined by [plugin-events-node](../events-node)
including backend plugin `EventsPlugin` and `EventsBackend`.
Additionally, it uses a simple in-memory implementation for
the `EventBroker` by default which you can replace with a more sophisticated
implementation of your choice as you need (e.g., via module).
Some of these (non-exhaustive) may provide added persistence,
or use external systems like AWS EventBridge, AWS SNS, Kafka, etc.
## Installation
```bash
# From your Backstage root directory
yarn add --cwd packages/backend @backstage/plugin-events-backend
```
Add a file [`packages/backend/src/plugins/events.ts`](../../packages/backend/src/plugins/events.ts)
to your Backstage project.
There, you can add all publishers, subscribers, etc. you want.
Additionally, add the events plugin to your backend.
```diff
// packages/backend/src/index.ts
// [...]
+import events from './plugins/events';
// [...]
+ const eventsEnv = useHotMemoize(module, () => createEnv('events'));
// [...]
+ apiRouter.use('/events', await events(eventsEnv, []));
// [...]
```
### With Event-based Entity Providers
In case you use event-based `EntityProviders`,
you may need something like the following:
```diff
// packages/backend/src/index.ts
- apiRouter.use('/events', await events(eventsEnv, []));
+ apiRouter.use('/events', await events(eventsEnv, eventBasedEntityProviders));
```
as well as a file
[`packages/backend/src/plugins/catalogEventBasedProviders.ts`](../../packages/backend/src/plugins/catalogEventBasedProviders.ts)
which contains event-based entity providers.
In case you don't have this dependency added yet:
```bash
# From your Backstage root directory
yarn add --cwd packages/backend @backstage/plugin-events-backend
```
```diff
// packages/backend/src/plugins/catalog.ts
import { CatalogBuilder } from '@backstage/plugin-catalog-backend';
+import { EntityProvider } from '@backstage/plugin-catalog-node';
import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
export default async function createPlugin(
env: PluginEnvironment,
+ providers?: Array<EntityProvider>,
): Promise<Router> {
const builder = await CatalogBuilder.create(env);
builder.addProcessor(new ScaffolderEntitiesProcessor());
+ builder.addEntityProvider(providers ?? []);
const { processingEngine, router } = await builder.build();
await processingEngine.start();
return router;
}
```
## Use Cases
### Custom Event Broker
Example using the `EventsBackend`:
```ts
new EventsBackend(env.logger)
.setEventBroker(yourEventBroker)
// [...]
.start();
```
Example using a module:
```ts
import { eventsExtensionPoint } from '@backstage/plugin-events-node';
// [...]
export const yourModuleEventsModule = createBackendModule({
pluginId: 'events',
moduleId: 'yourModule',
register(env) {
// [...]
env.registerInit({
deps: {
// [...]
events: eventsExtensionPoint,
// [...]
},
async init({ /* ... */ events /*, ... */ }) {
// [...]
const yourEventBroker = new YourEventBroker();
// [...]
events.setEventBroker(yourEventBroker);
},
});
},
});
```
+30
View File
@@ -0,0 +1,30 @@
## API Report File for "@backstage/plugin-events-backend"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { BackendFeature } from '@backstage/backend-plugin-api';
import { EventBroker } from '@backstage/plugin-events-node';
import { EventPublisher } from '@backstage/plugin-events-node';
import { EventSubscriber } from '@backstage/plugin-events-node';
import { Logger } from 'winston';
// @public
export class EventsBackend {
constructor(logger: Logger);
// (undocumented)
addPublishers(
...publishers: Array<EventPublisher | Array<EventPublisher>>
): EventsBackend;
// (undocumented)
addSubscribers(
...subscribers: Array<EventSubscriber | Array<EventSubscriber>>
): EventsBackend;
// (undocumented)
setEventBroker(eventBroker: EventBroker): EventsBackend;
start(): Promise<void>;
}
// @alpha
export const eventsPlugin: (options?: undefined) => BackendFeature;
```
+40
View File
@@ -0,0 +1,40 @@
{
"name": "@backstage/plugin-events-backend",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"alphaTypes": "dist/index.alpha.d.ts",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "backend-plugin"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build --experimental-type-build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/plugin-events-node": "workspace:^",
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/backend-common": "workspace:^",
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/plugin-events-backend-test-utils": "workspace:^"
},
"files": [
"alpha",
"dist"
]
}
+24
View File
@@ -0,0 +1,24 @@
/*
* 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.
*/
/**
* The Backstage backend plugin "events" that provides the event management.
*
* @packageDocumentation
*/
export { EventsBackend } from './service/EventsBackend';
export { eventsPlugin } from './service/EventsPlugin';
@@ -0,0 +1,60 @@
/*
* 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 {
TestEventBroker,
TestEventPublisher,
TestEventSubscriber,
} from '@backstage/plugin-events-backend-test-utils';
import { EventsBackend } from './EventsBackend';
const logger = getVoidLogger();
describe('EventsBackend', () => {
it('wires up all components', async () => {
const eventBroker = new TestEventBroker();
const publisher1 = new TestEventPublisher();
const publisher2 = new TestEventPublisher();
await new EventsBackend(logger)
.setEventBroker(eventBroker)
.addPublishers(publisher1, [publisher2])
.addSubscribers(new TestEventSubscriber('one', ['topicA']), [
new TestEventSubscriber('two', ['topicA', 'topicB']),
])
.start();
await eventBroker.publish({
topic: 'topicA',
eventPayload: { test: 'payload' },
});
expect(eventBroker.published.length).toEqual(1);
expect(eventBroker.published[0].topic).toEqual('topicA');
expect(eventBroker.published[0].eventPayload).toEqual({ test: 'payload' });
expect(eventBroker.subscribed.length).toEqual(2);
expect(
eventBroker.subscribed.map(
sub => (sub as unknown as TestEventSubscriber).name,
),
).toEqual(['one', 'two']);
expect(publisher1.eventBroker).toBe(eventBroker);
expect(publisher2.eventBroker).toBe(eventBroker);
});
});
@@ -0,0 +1,67 @@
/*
* 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 {
EventBroker,
EventPublisher,
EventSubscriber,
} from '@backstage/plugin-events-node';
import { Logger } from 'winston';
import { InMemoryEventBroker } from './InMemoryEventBroker';
/**
* A builder that helps wire up all component parts of the event management.
*
* @public
*/
export class EventsBackend {
private eventBroker: EventBroker;
private publishers: EventPublisher[] = [];
private subscribers: EventSubscriber[] = [];
constructor(logger: Logger) {
this.eventBroker = new InMemoryEventBroker(logger);
}
setEventBroker(eventBroker: EventBroker): EventsBackend {
this.eventBroker = eventBroker;
return this;
}
addPublishers(
...publishers: Array<EventPublisher | Array<EventPublisher>>
): EventsBackend {
this.publishers.push(...publishers.flat());
return this;
}
addSubscribers(
...subscribers: Array<EventSubscriber | Array<EventSubscriber>>
): EventsBackend {
this.subscribers.push(...subscribers.flat());
return this;
}
/**
* Wires up and returns all component parts of the event management.
*/
async start(): Promise<void> {
this.eventBroker.subscribe(this.subscribers);
this.publishers.forEach(publisher =>
publisher.setEventBroker(this.eventBroker),
);
}
}
@@ -0,0 +1,64 @@
/*
* 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 {
createBackendModule,
loggerServiceRef,
} from '@backstage/backend-plugin-api';
import { startTestBackend } from '@backstage/backend-test-utils';
import { eventsExtensionPoint } from '@backstage/plugin-events-node';
import {
TestEventBroker,
TestEventPublisher,
TestEventSubscriber,
} from '@backstage/plugin-events-backend-test-utils';
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', ['topicA']);
const testModule = createBackendModule({
pluginId: 'events',
moduleId: 'test',
register(env) {
env.registerInit({
deps: {
events: eventsExtensionPoint,
},
async init({ events }) {
events.setEventBroker(eventBroker);
events.addPublishers(publisher);
events.addSubscribers(subscriber);
},
});
},
});
await startTestBackend({
extensionPoints: [],
services: [[loggerServiceRef, getVoidLogger()]],
features: [eventsPlugin(), testModule()],
});
expect(publisher.eventBroker).toBe(eventBroker);
expect(eventBroker.subscribed.length).toEqual(1);
expect(eventBroker.subscribed[0]).toBe(subscriber);
});
});
@@ -0,0 +1,93 @@
/*
* 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 {
createBackendPlugin,
loggerServiceRef,
loggerToWinstonLogger,
} from '@backstage/backend-plugin-api';
import {
EventBroker,
EventPublisher,
EventSubscriber,
eventsExtensionPoint,
EventsExtensionPoint,
} from '@backstage/plugin-events-node';
import { InMemoryEventBroker } from './InMemoryEventBroker';
class EventsExtensionPointImpl implements EventsExtensionPoint {
#eventBroker: EventBroker | undefined;
#publishers: EventPublisher[] = [];
#subscribers: EventSubscriber[] = [];
setEventBroker(eventBroker: EventBroker): void {
this.#eventBroker = eventBroker;
}
addPublishers(
...publishers: Array<EventPublisher | Array<EventPublisher>>
): void {
this.#publishers.push(...publishers.flat());
}
addSubscribers(
...subscribers: Array<EventSubscriber | Array<EventSubscriber>>
): void {
this.#subscribers.push(...subscribers.flat());
}
get eventBroker() {
return this.#eventBroker;
}
get publishers() {
return this.#publishers;
}
get subscribers() {
return this.#subscribers;
}
}
/**
* Events plugin
*
* @alpha
*/
export const eventsPlugin = createBackendPlugin({
id: 'events',
register(env) {
const extensionPoint = new EventsExtensionPointImpl();
env.registerExtensionPoint(eventsExtensionPoint, extensionPoint);
env.registerInit({
deps: {
logger: loggerServiceRef,
},
async init({ logger }) {
if (!extensionPoint.eventBroker) {
const winstonLogger = loggerToWinstonLogger(logger);
extensionPoint.setEventBroker(new InMemoryEventBroker(winstonLogger));
}
extensionPoint.eventBroker!.subscribe(extensionPoint.subscribers);
extensionPoint.publishers.forEach(publisher =>
publisher.setEventBroker(extensionPoint.eventBroker!),
);
},
});
},
});
@@ -0,0 +1,66 @@
/*
* 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 { TestEventSubscriber } from '@backstage/plugin-events-backend-test-utils';
import { InMemoryEventBroker } from './InMemoryEventBroker';
const logger = getVoidLogger();
describe('InMemoryEventBroker', () => {
it('passes events to interested subscribers', () => {
const subscriber1 = new TestEventSubscriber('test1', ['topicA', 'topicB']);
const subscriber2 = new TestEventSubscriber('test2', ['topicB', 'topicC']);
const eventBroker = new InMemoryEventBroker(logger);
eventBroker.subscribe(subscriber1);
eventBroker.subscribe(subscriber2);
eventBroker.publish({ topic: 'topicA', eventPayload: { test: 'topicA' } });
eventBroker.publish({ topic: 'topicB', eventPayload: { test: 'topicB' } });
eventBroker.publish({ topic: 'topicC', eventPayload: { test: 'topicC' } });
eventBroker.publish({ topic: 'topicD', eventPayload: { test: 'topicD' } });
expect(Object.keys(subscriber1.receivedEvents)).toEqual([
'topicA',
'topicB',
]);
expect(subscriber1.receivedEvents.topicA.length).toEqual(1);
expect(subscriber1.receivedEvents.topicA[0]).toEqual({
topic: 'topicA',
eventPayload: { test: 'topicA' },
});
expect(subscriber1.receivedEvents.topicB.length).toEqual(1);
expect(subscriber1.receivedEvents.topicB[0]).toEqual({
topic: 'topicB',
eventPayload: { test: 'topicB' },
});
expect(Object.keys(subscriber2.receivedEvents)).toEqual([
'topicB',
'topicC',
]);
expect(subscriber2.receivedEvents.topicB.length).toEqual(1);
expect(subscriber2.receivedEvents.topicB[0]).toEqual({
topic: 'topicB',
eventPayload: { test: 'topicB' },
});
expect(subscriber2.receivedEvents.topicC.length).toEqual(1);
expect(subscriber2.receivedEvents.topicC[0]).toEqual({
topic: 'topicC',
eventPayload: { test: 'topicC' },
});
});
});
@@ -0,0 +1,58 @@
/*
* 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 {
EventBroker,
EventParams,
EventSubscriber,
} from '@backstage/plugin-events-node';
import { Logger } from 'winston';
/**
* In-memory event broker which will pass the event to all registered subscribers
* interested in it.
* Events will not be persisted in any form.
*/
// TODO(pjungermann): add prom metrics? (see plugins/catalog-backend/src/util/metrics.ts, etc.)
export class InMemoryEventBroker implements EventBroker {
constructor(private readonly logger: Logger) {}
private readonly subscribers: {
[topic: string]: EventSubscriber[];
} = {};
async publish(params: EventParams): Promise<void> {
this.logger.debug(
`Event received: topic=${params.topic}, metadata=${JSON.stringify(
params.metadata,
)}, payload=${JSON.stringify(params.eventPayload)}`,
);
const subscribed = this.subscribers[params.topic] ?? [];
subscribed.forEach(subscriber => subscriber.onEvent(params));
}
subscribe(
...subscribers: Array<EventSubscriber | Array<EventSubscriber>>
): void {
subscribers.flat().forEach(subscriber => {
subscriber.supportsEventTopics().forEach(topic => {
this.subscribers[topic] = this.subscribers[topic] ?? [];
this.subscribers[topic].push(subscriber);
});
});
}
}
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2020 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.
*/
export {};
+1
View File
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+3
View File
@@ -0,0 +1,3 @@
# plugin-events-node
Houses types and utilities for building events-related modules.
+76
View File
@@ -0,0 +1,76 @@
## API Report File for "@backstage/plugin-events-node"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ExtensionPoint } from '@backstage/backend-plugin-api';
// @public
export interface EventBroker {
publish(params: EventParams): Promise<void>;
subscribe(
...subscribers: Array<EventSubscriber | Array<EventSubscriber>>
): void;
}
// @public (undocumented)
export interface EventParams {
eventPayload: unknown;
metadata?: Record<string, string | string[] | undefined>;
topic: string;
}
// @public
export interface EventPublisher {
// (undocumented)
setEventBroker(eventBroker: EventBroker): Promise<void>;
}
// @public
export abstract class EventRouter implements EventPublisher, EventSubscriber {
// (undocumented)
protected abstract determineDestinationTopic(
params: EventParams,
): string | undefined;
// (undocumented)
onEvent(params: EventParams): Promise<void>;
// (undocumented)
setEventBroker(eventBroker: EventBroker): Promise<void>;
// (undocumented)
abstract supportsEventTopics(): string[];
}
// @alpha (undocumented)
export interface EventsExtensionPoint {
// (undocumented)
addPublishers(
...publishers: Array<EventPublisher | Array<EventPublisher>>
): void;
// (undocumented)
addSubscribers(
...subscribers: Array<EventSubscriber | Array<EventSubscriber>>
): void;
// (undocumented)
setEventBroker(eventBroker: EventBroker): void;
}
// @alpha (undocumented)
export const eventsExtensionPoint: ExtensionPoint<EventsExtensionPoint>;
// @public
export interface EventSubscriber {
onEvent(params: EventParams): Promise<void>;
supportsEventTopics(): string[];
}
// @public
export abstract class SubTopicEventRouter extends EventRouter {
protected constructor(topic: string);
// (undocumented)
protected determineDestinationTopic(params: EventParams): string | undefined;
// (undocumented)
protected abstract determineSubTopic(params: EventParams): string | undefined;
// (undocumented)
supportsEventTopics(): string[];
}
```
+38
View File
@@ -0,0 +1,38 @@
{
"name": "@backstage/plugin-events-node",
"description": "The plugin-events-node module for @backstage/plugin-events-backend",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"alphaTypes": "dist/index.alpha.d.ts",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "node-library"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build --experimental-type-build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/backend-plugin-api": "workspace:^",
"@types/express": "^4.17.6",
"express": "^4.17.1"
},
"devDependencies": {
"@backstage/cli": "workspace:^"
},
"files": [
"alpha",
"dist"
]
}
@@ -0,0 +1,43 @@
/*
* 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 { EventParams } from './EventParams';
import { EventSubscriber } from './EventSubscriber';
/**
* 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 EventBroker {
/**
* Publishes an event for the topic.
*
* @param params - parameters for the to be published event.
*/
publish(params: EventParams): Promise<void>;
/**
* Adds new subscribers for {@link EventSubscriber#supportsEventTopics | interested topics}.
*
* @param subscribers - interested in events of specified topics.
*/
subscribe(
...subscribers: Array<EventSubscriber | Array<EventSubscriber>>
): void;
}
@@ -0,0 +1,33 @@
/*
* 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.
*/
/**
* @public
*/
export interface EventParams {
/**
* Topic for which this event should be published.
*/
topic: string;
/**
* Event payload.
*/
eventPayload: unknown;
/**
* Metadata (e.g., HTTP headers and similar for events received from external).
*/
metadata?: Record<string, string | string[] | undefined>;
}
@@ -0,0 +1,29 @@
/*
* 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 { EventBroker } from './EventBroker';
/**
* Publishes events to be consumed by subscribers for their topic.
* The events can come from different (external) sources
* like emitted themselves, received via HTTP endpoint (i.e. webhook)
* or from event brokers, queues, etc.
*
* @public
*/
export interface EventPublisher {
setEventBroker(eventBroker: EventBroker): Promise<void>;
}
@@ -0,0 +1,81 @@
/*
* 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 { EventBroker } from './EventBroker';
import { EventParams } from './EventParams';
import { EventRouter } from './EventRouter';
class TestEventRouter extends EventRouter {
protected determineDestinationTopic(params: EventParams): string | undefined {
const payload = params.eventPayload as { value?: number };
if (payload.value === undefined) {
return undefined;
}
return payload.value % 2 === 0 ? 'even' : 'odd';
}
supportsEventTopics(): string[] {
return ['my-topic'];
}
}
describe('EventRouter', () => {
const eventRouter = new TestEventRouter();
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' },
metadata,
});
expect(published).toEqual([]);
});
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 });
await eventRouter.onEvent({ topic, eventPayload: payloadOdd, metadata });
expect(published.length).toBe(2);
expect(published[0].topic).toEqual('even');
expect(published[0].eventPayload).toEqual(payloadEven);
expect(published[0].metadata).toEqual(metadata);
expect(published[1].topic).toEqual('odd');
expect(published[1].eventPayload).toEqual(payloadOdd);
expect(published[1].metadata).toEqual(metadata);
});
});
@@ -0,0 +1,55 @@
/*
* 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 { EventBroker } from './EventBroker';
import { EventParams } from './EventParams';
import { EventPublisher } from './EventPublisher';
import { EventSubscriber } from './EventSubscriber';
/**
* Subscribes to a topic and - depending on a set of conditions -
* republishes the event to another topic.
*
* @see {@link https://www.enterpriseintegrationpatterns.com/MessageRouter.html | Message Router pattern}.
* @public
*/
export abstract class EventRouter implements EventPublisher, EventSubscriber {
private eventBroker?: EventBroker;
protected abstract determineDestinationTopic(
params: EventParams,
): string | undefined;
async onEvent(params: EventParams): Promise<void> {
const topic = this.determineDestinationTopic(params);
if (!topic) {
return;
}
// republish to different topic
this.eventBroker?.publish({
...params,
topic,
});
}
async setEventBroker(eventBroker: EventBroker): Promise<void> {
this.eventBroker = eventBroker;
}
abstract supportsEventTopics(): string[];
}
@@ -0,0 +1,38 @@
/*
* 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 { EventParams } from './EventParams';
/**
* Handles received events.
* This may include triggering refreshes of catalog entities
* or other actions to react on events.
*
* @public
*/
export interface EventSubscriber {
/**
* Supported event topics like "github", "bitbucketCloud", etc.
*/
supportsEventTopics(): string[];
/**
* React on a received event.
*
* @param params - parameters for the to be received event.
*/
onEvent(params: EventParams): Promise<void>;
}
@@ -0,0 +1,67 @@
/*
* 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 { EventBroker } from './EventBroker';
import { EventParams } from './EventParams';
import { SubTopicEventRouter } from './SubTopicEventRouter';
class TestSubTopicEventRouter extends SubTopicEventRouter {
constructor() {
super('my-topic');
}
protected determineSubTopic(params: EventParams): string | undefined {
return params.metadata?.['x-my-event'] as string | undefined;
}
}
describe('SubTopicEventRouter', () => {
const eventRouter = new TestSubTopicEventRouter();
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);
expect(published[0].topic).toEqual('my-topic.test.type');
expect(published[0].eventPayload).toEqual(eventPayload);
expect(published[0].metadata).toEqual(metadata);
});
});
@@ -0,0 +1,44 @@
/*
* 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 { EventParams } from './EventParams';
import { EventRouter } from './EventRouter';
/**
* Subscribes to the provided (generic) topic
* and publishes the events under the more concrete sub-topic
* depending on the implemented logic for determining it.
* Implementing classes might use information from `metadata`
* and/or properties within the payload.
*
* @public
*/
export abstract class SubTopicEventRouter extends EventRouter {
protected constructor(private readonly topic: string) {
super();
}
protected abstract determineSubTopic(params: EventParams): string | undefined;
protected determineDestinationTopic(params: EventParams): string | undefined {
const subTopic = this.determineSubTopic(params);
return subTopic ? `${params.topic}.${subTopic}` : undefined;
}
supportsEventTopics(): string[] {
return [this.topic];
}
}
+22
View File
@@ -0,0 +1,22 @@
/*
* 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.
*/
export type { EventBroker } from './EventBroker';
export type { EventParams } from './EventParams';
export type { EventPublisher } from './EventPublisher';
export { EventRouter } from './EventRouter';
export type { EventSubscriber } from './EventSubscriber';
export { SubTopicEventRouter } from './SubTopicEventRouter';
+40
View File
@@ -0,0 +1,40 @@
/*
* 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 { createExtensionPoint } from '@backstage/backend-plugin-api';
import { EventBroker, EventPublisher, EventSubscriber } from './api';
/**
* @alpha
*/
export interface EventsExtensionPoint {
setEventBroker(eventBroker: EventBroker): void;
addPublishers(
...publishers: Array<EventPublisher | Array<EventPublisher>>
): void;
addSubscribers(
...subscribers: Array<EventSubscriber | Array<EventSubscriber>>
): void;
}
/**
* @alpha
*/
export const eventsExtensionPoint = createExtensionPoint<EventsExtensionPoint>({
id: 'events',
});
+25
View File
@@ -0,0 +1,25 @@
/*
* 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.
*/
/**
* The events-node module for `@backstage/plugin-events-backend`.
*
* @packageDocumentation
*/
export * from './api';
export type { EventsExtensionPoint } from './extensions';
export { eventsExtensionPoint } from './extensions';
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2020 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.
*/
export {};
+34
View File
@@ -5532,6 +5532,40 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/plugin-events-backend-test-utils@workspace:^, @backstage/plugin-events-backend-test-utils@workspace:plugins/events-backend-test-utils":
version: 0.0.0-use.local
resolution: "@backstage/plugin-events-backend-test-utils@workspace:plugins/events-backend-test-utils"
dependencies:
"@backstage/cli": "workspace:^"
"@backstage/plugin-events-node": "workspace:^"
languageName: unknown
linkType: soft
"@backstage/plugin-events-backend@workspace:plugins/events-backend":
version: 0.0.0-use.local
resolution: "@backstage/plugin-events-backend@workspace:plugins/events-backend"
dependencies:
"@backstage/backend-common": "workspace:^"
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-test-utils": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/plugin-events-backend-test-utils": "workspace:^"
"@backstage/plugin-events-node": "workspace:^"
winston: ^3.2.1
languageName: unknown
linkType: soft
"@backstage/plugin-events-node@workspace:^, @backstage/plugin-events-node@workspace:plugins/events-node":
version: 0.0.0-use.local
resolution: "@backstage/plugin-events-node@workspace:plugins/events-node"
dependencies:
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/cli": "workspace:^"
"@types/express": ^4.17.6
express: ^4.17.1
languageName: unknown
linkType: soft
"@backstage/plugin-explore-react@workspace:^, @backstage/plugin-explore-react@workspace:plugins/explore-react":
version: 0.0.0-use.local
resolution: "@backstage/plugin-explore-react@workspace:plugins/explore-react"