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:
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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',
|
||||
});
|
||||
@@ -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';
|
||||
@@ -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 {};
|
||||
Reference in New Issue
Block a user