diff --git a/workspaces/linguist/.changeset/fast-seahorses-sleep.md b/workspaces/linguist/.changeset/fast-seahorses-sleep.md new file mode 100644 index 0000000000..c930db5c50 --- /dev/null +++ b/workspaces/linguist/.changeset/fast-seahorses-sleep.md @@ -0,0 +1,5 @@ +--- +'@backstage-community/plugin-linguist-backend': minor +--- + +Added openapi spec for linguist backend diff --git a/workspaces/linguist/app-config.yaml b/workspaces/linguist/app-config.yaml index f62fd7c89c..9e6a3d538b 100644 --- a/workspaces/linguist/app-config.yaml +++ b/workspaces/linguist/app-config.yaml @@ -51,7 +51,12 @@ catalog: target: ../../examples/org.yaml rules: - allow: [User, Group] - + providers: + backstageOpenapi: + plugins: + - catalog + - search + - linguist linguist: linguistJsOptions: categories: ['programming'] diff --git a/workspaces/linguist/packages/backend/package.json b/workspaces/linguist/packages/backend/package.json index bc67afbcc4..67ebc2ff54 100644 --- a/workspaces/linguist/packages/backend/package.json +++ b/workspaces/linguist/packages/backend/package.json @@ -31,6 +31,7 @@ "@backstage/plugin-auth-backend-module-guest-provider": "^0.2.3", "@backstage/plugin-auth-node": "^0.5.5", "@backstage/plugin-catalog-backend": "^1.29.0", + "@backstage/plugin-catalog-backend-module-backstage-openapi": "^0.4.2", "@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "^0.2.3", "@backstage/plugin-permission-backend": "^0.5.52", "@backstage/plugin-permission-backend-module-allow-all-policy": "^0.2.3", diff --git a/workspaces/linguist/packages/backend/src/index.ts b/workspaces/linguist/packages/backend/src/index.ts index c2957f4185..ea40d98b4b 100644 --- a/workspaces/linguist/packages/backend/src/index.ts +++ b/workspaces/linguist/packages/backend/src/index.ts @@ -40,6 +40,11 @@ backend.add( ), ); +// openapi plugin +backend.add( + import('@backstage/plugin-catalog-backend-module-backstage-openapi'), +); + // permission plugin backend.add(import('@backstage/plugin-permission-backend/alpha')); backend.add( diff --git a/workspaces/linguist/plugins/linguist-backend/README.md b/workspaces/linguist/plugins/linguist-backend/README.md index 9ab4f20240..1c5e703498 100644 --- a/workspaces/linguist/plugins/linguist-backend/README.md +++ b/workspaces/linguist/plugins/linguist-backend/README.md @@ -54,6 +54,10 @@ Here's how to get the backend up and running: 4. Now run `yarn start-backend` from the repo root 5. Finally open `http://localhost:7007/api/linguist/health` in a browser and it should return `{"status":"ok"}` +## Linguist Backend API + +The linguist backend provides API documentation through OpenAPI. To view the OpenAPI spec for your backstage instance, consider installing [`@backstage/plugin-catalog-backend-module-backstage-openapi`](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-backstage-openapi/README.md). + ## Plugin Options The Linguist backend has various plugin options that you can provide in the `app-config.yaml` file that will allow you to configure various aspects of how it works. The following sections go into the details of these options diff --git a/workspaces/linguist/plugins/linguist-backend/package.json b/workspaces/linguist/plugins/linguist-backend/package.json index 656593661c..1a3a398995 100644 --- a/workspaces/linguist/plugins/linguist-backend/package.json +++ b/workspaces/linguist/plugins/linguist-backend/package.json @@ -32,6 +32,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", @@ -41,6 +42,7 @@ "dependencies": { "@backstage-community/plugin-linguist-common": "workspace:^", "@backstage/backend-defaults": "^0.6.1", + "@backstage/backend-openapi-utils": "^0.4.0", "@backstage/backend-plugin-api": "^1.1.0", "@backstage/catalog-client": "^1.9.0", "@backstage/catalog-model": "^1.7.2", @@ -62,6 +64,7 @@ "devDependencies": { "@backstage/backend-test-utils": "^1.2.0", "@backstage/cli": "^0.29.4", + "@backstage/repo-tools": "^0.12.0", "@types/fs-extra": "^11.0.0", "@types/node-fetch": "^2.5.12", "@types/supertest": "^6.0.0", diff --git a/workspaces/linguist/plugins/linguist-backend/src/schema/openapi.generated.ts b/workspaces/linguist/plugins/linguist-backend/src/schema/openapi.generated.ts new file mode 100644 index 0000000000..3422e8d52e --- /dev/null +++ b/workspaces/linguist/plugins/linguist-backend/src/schema/openapi.generated.ts @@ -0,0 +1,188 @@ +/* + * 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: 'linguist', + description: + 'This API powers the backend for the linguist plugins of backstage.', + version: '1.0', + license: { + name: 'Apache-2.0', + url: 'http://www.apache.org/licenses/LICENSE-2.0.html', + }, + contact: {}, + }, + servers: [ + { + url: '/', + }, + ], + paths: { + '/health': { + get: { + description: 'Checks if the linguist backend is hooked up properly', + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + status: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + }, + '/entity-languages': { + get: { + description: + 'Returns the language breakdown of the passed in entityRef.', + parameters: [ + { + in: 'query', + description: + 'Reference passed in the format :/', + example: 'component:default/my-component', + name: 'entityRef', + required: true, + schema: { + type: 'string', + }, + }, + ], + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Languages', + }, + }, + }, + }, + '500': { + $ref: '#/components/responses/ServerError', + }, + }, + }, + }, + }, + components: { + examples: {}, + headers: {}, + parameters: {}, + requestBodies: {}, + responses: { + ServerError: { + description: 'Error', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + error: { + type: 'object', + properties: { + name: { + type: 'string', + example: 'Error', + }, + message: { + type: 'string', + example: 'No entityRef was provided', + }, + }, + }, + }, + }, + }, + }, + }, + }, + schemas: { + LanguageType: { + type: 'string', + enum: ['programming', 'data', 'markup', 'prose'], + example: 'programming', + }, + Language: { + type: 'object', + properties: { + name: { + type: 'string', + example: 'java', + }, + percentage: { + type: 'number', + example: 20.03, + }, + bytes: { + type: 'number', + example: 3000, + }, + type: { + $ref: '#/components/schemas/LanguageType', + }, + color: { + type: 'string', + example: '#f1e05a', + }, + }, + }, + Languages: { + type: 'object', + properties: { + languageCount: { + type: 'number', + example: 3, + }, + totalBytes: { + type: 'number', + example: 10000, + }, + processedDate: { + type: 'string', + example: '2024-11-15T18:17:29.460Z', + }, + breakdown: { + type: 'array', + items: { + $ref: '#/components/schemas/Language', + }, + }, + }, + }, + }, + }, +} as const; +export const createOpenApiRouter = async ( + options?: Parameters['1'], +) => createValidatedOpenApiRouter(spec, options); diff --git a/workspaces/linguist/plugins/linguist-backend/src/schema/openapi.yaml b/workspaces/linguist/plugins/linguist-backend/src/schema/openapi.yaml new file mode 100644 index 0000000000..2b0a319131 --- /dev/null +++ b/workspaces/linguist/plugins/linguist-backend/src/schema/openapi.yaml @@ -0,0 +1,109 @@ +openapi: '3.0.3' +info: + title: linguist + description: This API powers the backend for the linguist plugins of backstage. + version: '1.0' + license: + name: Apache-2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + contact: {} +servers: + - url: / +paths: + /health: + get: + description: Checks if the linguist backend is hooked up properly + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + status: + type: string + /entity-languages: + get: + description: Returns the language breakdown of the passed in entityRef. + parameters: + - in: query + description: Reference passed in the format :/ + example: component:default/my-component + name: entityRef + required: true + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Languages' + '500': + $ref: '#/components/responses/ServerError' +components: + examples: {} + headers: {} + parameters: {} + requestBodies: {} + responses: + ServerError: + description: Error + content: + application/json: + schema: + type: object + properties: + error: + type: object + properties: + name: + type: string + example: Error + message: + type: string + example: No entityRef was provided + schemas: + LanguageType: + type: string + enum: + - programming + - data + - markup + - prose + example: programming + Language: + type: object + properties: + name: + type: string + example: java + percentage: + type: number + example: 20.03 + bytes: + type: number + example: 3000 + type: + $ref: '#/components/schemas/LanguageType' + color: + type: string + example: '#f1e05a' + Languages: + type: object + properties: + languageCount: + type: number + example: 3 + totalBytes: + type: number + example: 10000 + processedDate: + type: string + example: 2024-11-15T18:17:29.460Z + breakdown: + type: array + items: + $ref: '#/components/schemas/Language' diff --git a/workspaces/linguist/plugins/linguist-backend/src/service/router.test.ts b/workspaces/linguist/plugins/linguist-backend/src/service/router.test.ts index 8f74385b82..323b504a86 100644 --- a/workspaces/linguist/plugins/linguist-backend/src/service/router.test.ts +++ b/workspaces/linguist/plugins/linguist-backend/src/service/router.test.ts @@ -16,7 +16,9 @@ import express from 'express'; import request from 'supertest'; +import { Server } from 'http'; import { createRouter } from './router'; +import { wrapServer } from '@backstage/backend-openapi-utils'; import { LinguistBackendApi } from '../api'; import { mockServices, TestDatabases } from '@backstage/backend-test-utils'; import { UrlReaderService } from '@backstage/backend-plugin-api'; @@ -36,7 +38,7 @@ const databases = TestDatabases.create(); describe('createRouter', () => { let linguistBackendApi: jest.Mocked; - let app: express.Express; + let app: express.Express | Server; beforeEach(() => { jest.resetAllMocks(); @@ -55,7 +57,7 @@ describe('createRouter', () => { config: mockServices.rootConfig(), auth: mockServices.auth(), }); - app = express().use(router); + app = await wrapServer(express().use(router)); }); it('returns ok', async () => { const response = await request(app).get('/health'); diff --git a/workspaces/linguist/plugins/linguist-backend/src/service/router.ts b/workspaces/linguist/plugins/linguist-backend/src/service/router.ts index 53083f7b8b..fca9c8253a 100644 --- a/workspaces/linguist/plugins/linguist-backend/src/service/router.ts +++ b/workspaces/linguist/plugins/linguist-backend/src/service/router.ts @@ -23,7 +23,7 @@ import { UrlReaderService, } from '@backstage/backend-plugin-api'; import express from 'express'; -import Router from 'express-promise-router'; +import { createOpenApiRouter } from '../schema/openapi.generated'; import { LinguistBackendApi } from '../api'; import { LinguistBackendDatabase } from '../db'; import { HumanDuration, JsonObject } from '@backstage/types'; @@ -110,17 +110,16 @@ export async function createRouter( }); } - const router = Router(); - router.use(express.json()); + const apiRouter = await createOpenApiRouter(); - router.get('/health', (_, response) => { + apiRouter.get('/health', (_, response) => { response.send({ status: 'ok' }); }); /** - * /entity-languages?entity=component:default/my-component + * /entity-languages?entityRef=component:default/my-component */ - router.get('/entity-languages', async (req, res) => { + apiRouter.get('/entity-languages', async (req, res) => { const { entityRef: entityRef } = req.query; if (!entityRef) { @@ -134,7 +133,7 @@ export async function createRouter( }); const middleware = MiddlewareFactory.create({ logger, config }); + apiRouter.use(middleware.error()); - router.use(middleware.error()); - return router; + return apiRouter; } diff --git a/workspaces/linguist/yarn.lock b/workspaces/linguist/yarn.lock index 5b267d7181..51f93cc537 100644 --- a/workspaces/linguist/yarn.lock +++ b/workspaces/linguist/yarn.lock @@ -2779,6 +2779,7 @@ __metadata: dependencies: "@backstage-community/plugin-linguist-common": "workspace:^" "@backstage/backend-defaults": ^0.6.1 + "@backstage/backend-openapi-utils": ^0.4.0 "@backstage/backend-plugin-api": ^1.1.0 "@backstage/backend-test-utils": ^1.2.0 "@backstage/catalog-client": ^1.9.0 @@ -2787,6 +2788,7 @@ __metadata: "@backstage/config": ^1.3.1 "@backstage/errors": ^1.2.6 "@backstage/plugin-catalog-node": ^1.15.0 + "@backstage/repo-tools": ^0.12.0 "@backstage/types": ^1.2.0 "@types/express": "*" "@types/fs-extra": ^11.0.0 @@ -4304,6 +4306,24 @@ __metadata: languageName: node linkType: hard +"@backstage/plugin-catalog-backend-module-backstage-openapi@npm:^0.4.2": + version: 0.4.3 + resolution: "@backstage/plugin-catalog-backend-module-backstage-openapi@npm:0.4.3" + dependencies: + "@backstage/backend-openapi-utils": ^0.4.0 + "@backstage/backend-plugin-api": ^1.1.0 + "@backstage/catalog-model": ^1.7.2 + "@backstage/config": ^1.3.1 + "@backstage/errors": ^1.2.6 + "@backstage/plugin-catalog-node": ^1.15.0 + cross-fetch: ^4.0.0 + lodash: ^4.17.21 + openapi-merge: ^1.3.2 + uuid: ^11.0.0 + checksum: dbe83e681e8b7ddceec0226984bc9336d648d084b681d39d745c1139063db220fd917e58acef97d9f1caff605448b8f40fafbfc41961f898b51ebd0671d4f51a + languageName: node + linkType: hard + "@backstage/plugin-catalog-backend-module-scaffolder-entity-model@npm:^0.2.3": version: 0.2.3 resolution: "@backstage/plugin-catalog-backend-module-scaffolder-entity-model@npm:0.2.3" @@ -15034,6 +15054,7 @@ __metadata: "@backstage/plugin-auth-backend-module-guest-provider": ^0.2.3 "@backstage/plugin-auth-node": ^0.5.5 "@backstage/plugin-catalog-backend": ^1.29.0 + "@backstage/plugin-catalog-backend-module-backstage-openapi": ^0.4.2 "@backstage/plugin-catalog-backend-module-scaffolder-entity-model": ^0.2.3 "@backstage/plugin-permission-backend": ^0.5.52 "@backstage/plugin-permission-backend-module-allow-all-policy": ^0.2.3