diff --git a/plugins/events-backend/package.json b/plugins/events-backend/package.json index dc7c2b0110..3160e6d494 100644 --- a/plugins/events-backend/package.json +++ b/plugins/events-backend/package.json @@ -44,6 +44,7 @@ "scripts": { "build": "backstage-cli package build", "clean": "backstage-cli package clean", + "generate": "backstage-repo-tools package schema openapi generate --server", "lint": "backstage-cli package lint", "prepack": "backstage-cli package prepack", "postpack": "backstage-cli package postpack", @@ -52,6 +53,7 @@ }, "dependencies": { "@backstage/backend-common": "^0.25.0", + "@backstage/backend-openapi-utils": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", @@ -70,6 +72,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-events-backend-test-utils": "workspace:^", + "@backstage/repo-tools": "workspace:^", "supertest": "^7.0.0" }, "configSchema": "config.d.ts" diff --git a/plugins/events-backend/src/schema/openapi.generated.ts b/plugins/events-backend/src/schema/openapi.generated.ts new file mode 100644 index 0000000000..7d17ad2bf6 --- /dev/null +++ b/plugins/events-backend/src/schema/openapi.generated.ts @@ -0,0 +1,289 @@ +/* + * 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. + */ + +// ****************************************************************** +// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * +// ****************************************************************** +import { createValidatedOpenApiRouter } from '@backstage/backend-openapi-utils'; + +export const spec = { + openapi: '3.0.3', + info: { + title: 'events', + version: '1', + description: 'The Backstage backend plugin that powers the events system.', + license: { + name: 'Apache-2.0', + url: 'http://www.apache.org/licenses/LICENSE-2.0.html', + }, + contact: {}, + }, + servers: [ + { + url: '/', + }, + ], + components: { + examples: {}, + headers: {}, + parameters: { + subscriptionId: { + name: 'subscriptionId', + in: 'path', + required: true, + allowReserved: true, + schema: { + type: 'string', + }, + }, + }, + requestBodies: {}, + responses: { + ErrorResponse: { + description: 'An error response from the backend.', + content: { + 'application/json; charset=utf-8': { + schema: { + $ref: '#/components/schemas/Error', + }, + }, + }, + }, + }, + schemas: { + Event: { + type: 'object', + required: ['topic', 'payload'], + properties: { + topic: { + type: 'string', + description: 'The topic that the event is published on', + }, + payload: { + $ref: '#/components/schemas/JsonObject', + description: 'The event payload', + }, + }, + }, + Error: { + type: 'object', + properties: { + error: { + type: 'object', + properties: { + name: { + type: 'string', + }, + message: { + type: 'string', + }, + }, + required: ['name', 'message'], + }, + request: { + type: 'object', + properties: { + method: { + type: 'string', + }, + url: { + type: 'string', + }, + }, + required: ['method', 'url'], + }, + response: { + type: 'object', + properties: { + statusCode: { + type: 'number', + }, + }, + required: ['statusCode'], + }, + }, + required: ['error', 'request', 'response'], + }, + JsonObject: { + type: 'object', + properties: {}, + additionalProperties: {}, + }, + }, + securitySchemes: { + JWT: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }, + }, + }, + paths: { + '/hub/events': { + post: { + operationId: 'PostEvent', + description: 'Publish a new event', + responses: { + default: { + $ref: '#/components/responses/ErrorResponse', + }, + }, + security: [ + {}, + { + JWT: [], + }, + ], + requestBody: { + required: true, + content: { + 'application/json': { + schema: { + type: 'object', + required: ['event'], + properties: { + event: { + $ref: '#/components/schemas/Event', + }, + subscriptionIds: { + type: 'array', + description: + 'The IDs of subscriptions that have already received this event', + items: { + type: 'string', + }, + }, + }, + }, + examples: { + 'Publishing a simple Event': { + value: { + event: { + topic: 'test-topic', + payload: { + myData: 'foo', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + '/hub/subscriptions/{subscriptionId}': { + put: { + operationId: 'PutSubscription', + description: + 'Ensures that the subscription exists with the provided configuration', + responses: { + '201': { + description: 'The subscription exists or was created successfully', + }, + default: { + $ref: '#/components/responses/ErrorResponse', + }, + }, + security: [ + {}, + { + JWT: [], + }, + ], + parameters: [ + { + $ref: '#/components/parameters/subscriptionId', + }, + ], + requestBody: { + required: true, + content: { + 'application/json': { + schema: { + type: 'object', + required: ['topics'], + properties: { + topics: { + type: 'array', + description: 'The topics to subscribe to', + items: { + type: 'string', + }, + }, + }, + }, + examples: { + 'Subscribing to a single topic': { + value: { + topics: ['test-topic'], + }, + }, + }, + }, + }, + }, + }, + }, + '/hub/subscriptions/{subscriptionId}/events': { + get: { + operationId: 'GetSubscriptionEvents', + description: 'Get new events for the provided subscription', + responses: { + '200': { + description: 'New events', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + events: { + type: 'array', + items: { + $ref: '#/components/schemas/Event', + }, + }, + }, + required: ['results'], + }, + }, + }, + }, + '201': { + description: 'Block poll response, new events are available', + }, + default: { + $ref: '#/components/responses/ErrorResponse', + }, + }, + security: [ + {}, + { + JWT: [], + }, + ], + parameters: [ + { + $ref: '#/components/parameters/subscriptionId', + }, + ], + }, + }, + }, +} as const; +export const createOpenApiRouter = async ( + options?: Parameters['1'], +) => createValidatedOpenApiRouter(spec, options); diff --git a/plugins/events-backend/src/schema/openapi.yaml b/plugins/events-backend/src/schema/openapi.yaml new file mode 100644 index 0000000000..1adee01ab5 --- /dev/null +++ b/plugins/events-backend/src/schema/openapi.yaml @@ -0,0 +1,185 @@ +openapi: 3.0.3 +info: + title: events + version: '1' + description: The Backstage backend plugin that powers the events system. + license: + name: Apache-2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + contact: {} +servers: + - url: / +components: + examples: {} + headers: {} + parameters: + subscriptionId: + name: subscriptionId + in: path + required: true + allowReserved: true + schema: + type: string + requestBodies: {} + responses: + ErrorResponse: + description: An error response from the backend. + content: + application/json; charset=utf-8: + schema: + $ref: '#/components/schemas/Error' + schemas: + Event: + type: object + required: + - topic + - payload + properties: + topic: + type: string + description: The topic that the event is published on + payload: + $ref: '#/components/schemas/JsonObject' + description: The event payload + + Error: + type: object + properties: + error: + type: object + properties: + name: + type: string + message: + type: string + required: + - name + - message + request: + type: object + properties: + method: + type: string + url: + type: string + required: + - method + - url + response: + type: object + properties: + statusCode: + type: number + required: + - statusCode + required: + - error + - request + - response + + JsonObject: + type: object + properties: {} + # Free form object. + additionalProperties: {} + securitySchemes: + JWT: + type: http + scheme: bearer + bearerFormat: JWT +paths: + /hub/events: + post: + operationId: PostEvent + description: Publish a new event + responses: + default: + $ref: '#/components/responses/ErrorResponse' + security: + - {} + - JWT: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - event + properties: + event: + $ref: '#/components/schemas/Event' + subscriptionIds: + type: array + description: The IDs of subscriptions that have already received this event + items: + type: string + examples: + Publishing a simple Event: + value: + event: + topic: test-topic + payload: + myData: foo + + /hub/subscriptions/{subscriptionId}: + put: + operationId: PutSubscription + description: Ensures that the subscription exists with the provided configuration + responses: + '201': + description: The subscription exists or was created successfully + default: + $ref: '#/components/responses/ErrorResponse' + security: + - {} + - JWT: [] + parameters: + - $ref: '#/components/parameters/subscriptionId' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - topics + properties: + topics: + type: array + description: The topics to subscribe to + items: + type: string + examples: + Subscribing to a single topic: + value: + topics: + - test-topic + + /hub/subscriptions/{subscriptionId}/events: + get: + operationId: GetSubscriptionEvents + description: Get new events for the provided subscription + responses: + '200': + description: New events + content: + application/json: + schema: + type: object + properties: + events: + type: array + items: + $ref: '#/components/schemas/Event' + required: + - results + '201': + description: Block poll response, new events are available + default: + $ref: '#/components/responses/ErrorResponse' + security: + - {} + - JWT: [] + parameters: + - $ref: '#/components/parameters/subscriptionId' diff --git a/plugins/events-backend/src/service/EventsPlugin.ts b/plugins/events-backend/src/service/EventsPlugin.ts index ecb234066d..b7892a5079 100644 --- a/plugins/events-backend/src/service/EventsPlugin.ts +++ b/plugins/events-backend/src/service/EventsPlugin.ts @@ -97,7 +97,7 @@ export const eventsPlugin = createBackendPlugin({ http.bind(eventsRouter); const hub = await EventHub.create({ logger, httpAuth }); - eventsRouter.use('/hub', hub.handler()); + router.use(hub.handler()); router.use(eventsRouter); router.addAuthPolicy({ diff --git a/plugins/events-backend/src/service/hub/EventHub.ts b/plugins/events-backend/src/service/hub/EventHub.ts index 584abffc9b..d20401c98f 100644 --- a/plugins/events-backend/src/service/hub/EventHub.ts +++ b/plugins/events-backend/src/service/hub/EventHub.ts @@ -20,7 +20,7 @@ import { HttpAuthService, LoggerService, } from '@backstage/backend-plugin-api'; -import express, { Handler } from 'express'; +import { Handler } from 'express'; import Router from 'express-promise-router'; import { Socket } from 'net'; import { STATUS_CODES } from 'http'; @@ -30,6 +30,8 @@ import { fromZodError } from 'zod-validation-error'; import { JsonObject, JsonValue } from '@backstage/types'; import { serializeError } from '@backstage/errors'; import { EventParams } from '@backstage/plugin-events-node'; +import { spec, createOpenApiRouter } from '../../schema/openapi.generated'; +import { internal } from '@backstage/backend-openapi-utils'; /* @@ -395,15 +397,23 @@ export class EventHub { const hub = new EventHub(server, router, logger, httpAuth); // WS - router.get('/connect', hub.#handleGetConnect); + router.get('/hub/connect', hub.#handleGetConnect); - router.use(express.json()); + const apiRouter = await createOpenApiRouter(); - router.post('/events', hub.#handlePostEvents); + router.use(apiRouter); + + apiRouter.post('/hub/events', hub.#handlePostEvents); // Long-polling - router.get('/subscriptions/:id/events', hub.#handleGetSubscription); - router.put('/subscriptions/:id', hub.#handlePutSubscription); + apiRouter.get( + '/hub/subscriptions/:subscriptionId/events', + hub.#handleGetSubscription, + ); + apiRouter.put( + '/hub/subscriptions/:subscriptionId', + hub.#handlePutSubscription, + ); return hub; } @@ -528,14 +538,18 @@ export class EventHub { } }; - #handlePostEvents: Handler = async (req, res) => { + #handlePostEvents: internal.DocRequestHandler< + typeof spec, + '/hub/events', + 'post' + > = async (req, res) => { const credentials = await this.#httpAuth.credentials(req, { allow: ['service'], }); await this.#store.publish({ params: { - topic: req.body.topic, - eventPayload: req.body.payload, + topic: req.body.event.topic, + eventPayload: req.body.event.payload, }, subscriberIds: [], }); @@ -545,11 +559,15 @@ export class EventHub { res.status(201).end(); }; - #handleGetSubscription: Handler = async (req, res) => { + #handleGetSubscription: internal.DocRequestHandler< + typeof spec, + '/hub/subscriptions/{subscriptionId}/events', + 'get' + > = async (req, res) => { const credentials = await this.#httpAuth.credentials(req, { allow: ['service'], }); - const id = req.params.id; + const id = req.params.subscriptionId; const { events } = await this.#store.readSubscription(id); @@ -568,11 +586,15 @@ export class EventHub { }); }; - #handlePutSubscription: Handler = async (req, res) => { + #handlePutSubscription: internal.DocRequestHandler< + typeof spec, + '/hub/subscriptions/{subscriptionId}', + 'put' + > = async (req, res) => { const credentials = await this.#httpAuth.credentials(req, { allow: ['service'], }); - const id = req.params.id; + const id = req.params.subscriptionId; await this.#store.upsertSubscription(id, req.body.topics); diff --git a/yarn.lock b/yarn.lock index f6a430f9af..d1024c0d96 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6139,6 +6139,7 @@ __metadata: dependencies: "@backstage/backend-common": ^0.25.0 "@backstage/backend-defaults": "workspace:^" + "@backstage/backend-openapi-utils": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" @@ -6146,6 +6147,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/plugin-events-backend-test-utils": "workspace:^" "@backstage/plugin-events-node": "workspace:^" + "@backstage/repo-tools": "workspace:^" "@backstage/types": "workspace:^" "@types/express": ^4.17.6 express: ^4.17.1