Add the ability to view the catalog's spec from the Backstage interface.

Signed-off-by: Aramis <sennyeyaramis@gmail.com>
Signed-off-by: Aramis Sennyey <aramiss@spotify.com>
This commit is contained in:
Aramis Sennyey
2023-04-25 16:52:49 -04:00
committed by Aramis Sennyey
parent d8b27e89ba
commit 785fb1ea75
19 changed files with 644 additions and 18 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend-module-openapi-spec': patch
---
Adds a new catalog module for ingesting Backstage plugin OpenAPI specs into the catalog for display as an API entity.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-openapi-utils': patch
---
Adds a new route, `/openapi.json` to validated routers for displaying their full OpenAPI spec in a standard endpoint.
+7
View File
@@ -50,6 +50,7 @@ backend:
allow:
- host: example.com
- host: '*.mozilla.org'
- host: localhost:7007
# workingDirectory: /tmp # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir
# See README.md in the proxy-backend plugin for information on the configuration format
@@ -464,3 +465,9 @@ stackstorm:
permission:
enabled: true
openapi:
plugins:
- catalog
- search
- todo
+2
View File
@@ -34,6 +34,8 @@
"@backstage/plugin-azure-devops-backend": "workspace:^",
"@backstage/plugin-badges-backend": "workspace:^",
"@backstage/plugin-catalog-backend": "workspace:^",
"@backstage/plugin-catalog-backend-module-openapi": "workspace:^",
"@backstage/plugin-catalog-backend-module-openapi-spec": "workspace:^",
"@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "workspace:^",
"@backstage/plugin-catalog-backend-module-unprocessed": "workspace:^",
"@backstage/plugin-devtools-backend": "workspace:^",
+1
View File
@@ -43,6 +43,7 @@ backend.add(import('@backstage/plugin-scaffolder-backend/alpha'));
backend.add(import('@backstage/plugin-search-backend-module-catalog/alpha'));
backend.add(import('@backstage/plugin-search-backend-module-explore/alpha'));
backend.add(import('@backstage/plugin-search-backend-module-techdocs/alpha'));
backend.add(import('@backstage/plugin-catalog-backend-module-openapi-spec'));
backend.add(import('@backstage/plugin-search-backend/alpha'));
backend.add(import('@backstage/plugin-techdocs-backend/alpha'));
backend.add(import('@backstage/plugin-todo-backend'));
@@ -248,6 +248,9 @@ type FullMap<
},
> = RequiredMap<T> & OptionalMap<T>;
// @public
export function getOpenApiSpecRoute(baseUrl: string): string;
// @public (undocumented)
interface HeaderObject extends ParameterObject {
// (undocumented)
@@ -442,6 +445,9 @@ type ObjectWithContentSchema<
? SchemaRef<Doc, Object['content']['application/json']['schema']>
: never;
// @public
export const OPENAPI_SPEC_ROUTE = '/openapi.json';
// @public (undocumented)
type OptionalMap<
T extends {
@@ -37,6 +37,8 @@
"dist"
],
"dependencies": {
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/errors": "workspace:^",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5",
@@ -45,6 +47,7 @@
"express-promise-router": "^4.1.0",
"json-schema-to-ts": "^2.6.2",
"lodash": "^4.17.21",
"openapi-merge": "^1.3.2",
"openapi3-ts": "^3.1.2"
}
}
@@ -0,0 +1,21 @@
/*
* Copyright 2023 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 route that all OpenAPI specs should be served from.
* @public
*/
export const OPENAPI_SPEC_ROUTE = '/openapi.json';
+2 -1
View File
@@ -31,4 +31,5 @@ export type {
PathParameters,
} from './utility';
export type { ApiRouter } from './router';
export { createValidatedOpenApiRouter } from './stub';
export { createValidatedOpenApiRouter, getOpenApiSpecRoute } from './stub';
export * from './constants';
+62 -13
View File
@@ -14,11 +14,12 @@
* limitations under the License.
*/
import { createValidatedOpenApiRouter } from './stub';
import { createValidatedOpenApiRouter, getOpenApiSpecRoute } from './stub';
import express from 'express';
import request from 'supertest';
import singlePathSpec from './___fixtures__/single-path';
import { Response } from './utility';
import { OPENAPI_SPEC_ROUTE } from './constants';
describe('createRouter', () => {
const pet: Response<typeof singlePathSpec, '/pet/:petId', 'get'> = {
@@ -28,6 +29,33 @@ describe('createRouter', () => {
photoUrls: [],
};
const specs = [singlePathSpec];
const ONCE_NESTED_ROUTER_PREFIX = '/pet-store';
const TWICE_NESTED_ROUTER_PREFIX = `/api`;
const routers = specs.flatMap(spec => {
const router = createValidatedOpenApiRouter(spec);
const unnestedApp = express();
unnestedApp.use('/', router);
const onceNestedRouter = express.Router();
onceNestedRouter.use(`${ONCE_NESTED_ROUTER_PREFIX}`, router);
const onceNestedApp = express();
onceNestedApp.use(`/`, onceNestedRouter);
const twiceNestedApp = express();
twiceNestedApp.use(`${TWICE_NESTED_ROUTER_PREFIX}`, onceNestedRouter);
return [
['', unnestedApp, router],
[ONCE_NESTED_ROUTER_PREFIX, onceNestedApp, router],
[
`${TWICE_NESTED_ROUTER_PREFIX}${ONCE_NESTED_ROUTER_PREFIX}`,
twiceNestedApp,
router,
],
] as const;
});
it('does NOT override originalUrl and basePath after execution', async () => {
expect.assertions(2);
const router = createValidatedOpenApiRouter(singlePathSpec);
@@ -61,19 +89,40 @@ describe('createRouter', () => {
expect(routerGetFn).toHaveBeenCalledTimes(1);
});
it('handles coercing parameters correctly', async () => {
expect.assertions(1);
const router = createValidatedOpenApiRouter(singlePathSpec);
router.get('/pet/:petId', (req, res) => {
expect(typeof req.params.petId).toBe('integer');
res.json(pet);
});
it.each(routers)(
'%s handles coercing parameters correctly',
async (prefix, app, router) => {
expect.assertions(1);
router.get('/pet/:petId', (req, res) => {
expect(typeof req.params.petId).toBe('integer');
res.send(pet);
});
const apiRouter = express.Router();
apiRouter.use('/pet-store', router);
const appRouter = express();
appRouter.use('/api', apiRouter);
await request(app).get(`${prefix}/pet/1`);
},
);
await request(appRouter).get('/api/pet-store/pet/1');
it.each(routers)(
'%s adds the openapi spec to the router',
async (prefix, app) => {
const response = await request(app)
.get(`${prefix}${OPENAPI_SPEC_ROUTE}`)
.expect(200);
expect(response.body).toHaveProperty('paths');
Object.keys(response.body.paths).forEach(key => {
const specKey = key.replace(prefix, '');
expect(singlePathSpec.paths).toHaveProperty(specKey);
expect(response.body.paths[key]).toEqual(
(singlePathSpec.paths as any)[specKey],
);
});
},
);
});
describe('getOpenApiSpecRoute', () => {
it('handles expected values', () => {
expect(getOpenApiSpecRoute('/api/test')).toEqual('/api/test/openapi.json');
expect(getOpenApiSpecRoute('api/test')).toEqual('api/test/openapi.json');
});
});
+39 -2
View File
@@ -27,6 +27,8 @@ import {
} from 'express';
import { InputError } from '@backstage/errors';
import { middleware as OpenApiValidator } from 'express-openapi-validator';
import { OPENAPI_SPEC_ROUTE } from './constants';
import { isErrorResult, merge } from 'openapi-merge';
type PropertyOverrideRequest = Request & {
[key: symbol]: string;
@@ -45,6 +47,16 @@ export function getDefaultRouterMiddleware() {
return [json()];
}
/**
* Given a base url for a plugin, find the given OpenAPI spec for that plugin.
* @param baseUrl - Plugin base url.
* @returns OpenAPI spec route for the base url.
* @public
*/
export function getOpenApiSpecRoute(baseUrl: string) {
return `${baseUrl}${OPENAPI_SPEC_ROUTE}`;
}
/**
* Create a new OpenAPI router with some default middleware.
* @param spec - Your OpenAPI spec imported as a JSON object.
@@ -59,7 +71,7 @@ export function createValidatedOpenApiRouter<T extends RequiredDoc>(
middleware?: RequestHandler[];
},
) {
const router = PromiseRouter() as ApiRouter<typeof spec>;
const router = PromiseRouter();
router.use(options?.middleware || getDefaultRouterMiddleware());
/**
@@ -116,5 +128,30 @@ export function createValidatedOpenApiRouter<T extends RequiredDoc>(
// Any errors from the middleware get through here.
router.use(validatorErrorTransformer());
return router;
router.get(OPENAPI_SPEC_ROUTE, async (req, res) => {
const mergeOutput = merge([
{
oas: spec as any,
pathModification: {
/**
* Get the route that this OpenAPI spec is hosted on. The other
* approach of using the discovery API increases the router constructor
* significantly and since we're just looking for path and not full URL,
* this works.
*
* If we wanted to add a list of servers, there may be a case for adding
* discovery API to get an exhaustive list of upstream servers, but that's
* also not currently supported.
*/
prepend: req.originalUrl.replace(OPENAPI_SPEC_ROUTE, ''),
},
},
]);
if (isErrorResult(mergeOutput)) {
throw new InputError('Invalid spec defined');
}
res.json(mergeOutput.output);
});
return router as ApiRouter<typeof spec>;
}
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
@@ -0,0 +1,30 @@
# catalog-backend-module-openapi-spec
## Summary
This module installs an entity provider that exports a single entity, your Backstage instance documentation, which merges as many backend plugins as you have defined in the config value `openapi.plugins`.
## Notes
- This only works with the new backend system.
- This requires populating a new config value `openapi.plugins` with an array of plugin IDs. The entity processor expo
## Installation
To your new backend file, add
```ts title="packages/backend/src/index.ts"
backend.add(import('@backstage/plugin-catalog-backend-module-openapi-spec'));
```
Add a list of plugins to your config like,
```yaml title="app-config.yaml"
openapi:
plugins:
- catalog
- todo
- search
```
We will attempt to load each plugin's OpenAPI spec hosted at `${pluginRoute}/openapi.json`. These are automatically added if you are using `@backstage/backend-openapi-utils`'s `createValidatedOpenApiRouter`.
@@ -0,0 +1,20 @@
## API Report File for "@backstage/plugin-catalog-backend-module-openapi-spec"
> 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';
// @public (undocumented)
export const catalogModuleInternalOpenApiSpec: () => BackendFeature;
// @public (undocumented)
export type MetaApiDocsPluginOptions = {
exampleOption: boolean;
};
// @public (undocumented)
export const metaOpenApiDocsPluginId = 'meta-api-docs';
// (No @packageDocumentation comment for this package)
```
@@ -0,0 +1,54 @@
{
"name": "@backstage/plugin-catalog-backend-module-openapi-spec",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"private": true,
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "backend-plugin-module"
},
"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/backend-common": "workspace:^",
"@backstage/backend-openapi-utils": "workspace:^",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/backend-tasks": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/plugin-catalog-node": "workspace:^",
"@types/express": "*",
"cross-fetch": "^3.1.5",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"lodash": "^4.17.21",
"openapi-merge": "^1.3.2",
"uuid": "^9.0.0",
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/catalog-model": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@types/supertest": "^2.0.8",
"msw": "^1.0.0",
"openapi3-ts": "^3.1.2",
"supertest": "^6.2.4"
},
"files": [
"dist"
]
}
@@ -0,0 +1,244 @@
/*
* Copyright 2023 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 type { ApiEntity } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { ForwardedError } from '@backstage/errors';
import {
EntityProvider,
EntityProviderConnection,
} from '@backstage/plugin-catalog-node';
import { merge, isErrorResult } from 'openapi-merge';
import { getOpenApiSpecRoute } from '@backstage/backend-openapi-utils';
import type {
OpenAPIObject,
OperationObject,
PathItemObject,
} from 'openapi3-ts';
import fetch from 'cross-fetch';
import { DiscoveryService, LoggerService } from '@backstage/backend-plugin-api';
import * as uuid from 'uuid';
import { PluginTaskScheduler, TaskRunner } from '@backstage/backend-tasks';
const HTTP_VERBS: (keyof PathItemObject)[] = [
'get',
'post',
'put',
'delete',
'patch',
'trace',
'options',
'head',
];
const addTagsToSpec = (spec: OpenAPIObject, tag: string) => {
Object.values(spec?.paths).forEach((path: PathItemObject) => {
HTTP_VERBS.forEach(verb => {
if (verb in path) {
if (!('tags' in path[verb])) {
(path[verb] as OperationObject).tags = [];
}
if (!(path[verb] as OperationObject).tags?.includes(tag)) {
(path[verb] as OperationObject).tags?.push(tag);
}
}
});
});
};
const mergeSpecs = async ({
baseUrl,
specs,
}: {
baseUrl: string;
specs: OpenAPIObject[];
}) => {
const mergeResult = merge([
// Add the full API information as the first item for other items to merge against it with.
{
oas: {
openapi: '3.0.3',
info: {
title: 'Backstage API',
version: '1',
},
servers: [{ url: baseUrl }],
paths: {},
},
},
// For each plugin, load its spec and the known endpoint that it sits under.
...specs.map(
spec =>
({
oas: spec,
// Weird typing differences between this package and the client package's openapi 3.
} as any),
),
]);
if (isErrorResult(mergeResult)) {
throw new ForwardedError(
`${mergeResult.message} (${mergeResult.type})`,
mergeResult,
);
} else {
return mergeResult.output;
}
};
const loadSpecs = async ({
baseUrl,
discovery,
plugins,
logger,
}: {
baseUrl: string;
plugins: string[];
discovery: DiscoveryService;
logger: LoggerService;
}) => {
const specs: OpenAPIObject[] = [];
for (const pluginId of plugins) {
const url = await discovery.getExternalBaseUrl(pluginId);
const openApiUrl = getOpenApiSpecRoute(url);
const response = await fetch(openApiUrl);
if (response.ok) {
const spec = await response.json();
addTagsToSpec(spec, pluginId);
specs.push(spec);
} else if (response.status === 404) {
logger.error(
`Plugin=${pluginId} does not have an OpenAPI spec at '${openApiUrl}'.`,
);
} else {
logger.error(
`Failed to load spec for plugin=${pluginId} at ${openApiUrl}. Error (${
response.status
}): ${response.body ? await response.text() : response.statusText}`,
);
}
}
return mergeSpecs({ baseUrl, specs });
};
export class InternalOpenApiDocumentationProvider implements EntityProvider {
private connection?: EntityProviderConnection;
private readonly scheduleFn: () => Promise<void>;
constructor(
public readonly config: Config,
public readonly discovery: DiscoveryService,
public readonly logger: LoggerService,
taskRunner: TaskRunner,
) {
this.scheduleFn = this.createScheduleFn(taskRunner);
}
static fromConfig(
config: Config,
options: {
discovery: DiscoveryService;
logger: LoggerService;
schedule: PluginTaskScheduler;
},
) {
const taskRunner = options.schedule.createScheduledTaskRunner({
frequency: {
minutes: 1,
},
timeout: {
minutes: 1,
},
});
return new InternalOpenApiDocumentationProvider(
config,
options.discovery,
options.logger,
taskRunner,
);
}
/** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.getProviderName} */
getProviderName() {
return `InternalOpenApiDocumentationProvider`;
}
/** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.connect} */
async connect(connection: EntityProviderConnection) {
this.connection = connection;
return await this.scheduleFn();
}
private createScheduleFn(taskRunner: TaskRunner): () => Promise<void> {
return async () => {
const taskId = `${this.getProviderName()}:refresh`;
return taskRunner.run({
id: taskId,
fn: async () => {
const logger = this.logger.child({
class:
InternalOpenApiDocumentationProvider.prototype.constructor.name,
taskId,
taskInstanceId: uuid.v4(),
});
try {
await this.refresh(logger);
} catch (error) {
logger.error(`${this.getProviderName()} refresh failed`, error);
}
},
});
};
}
async refresh(logger: LoggerService) {
const pluginsToMerge = this.config.getStringArray('openapi.plugins');
logger.info(`Loading specs from from ${pluginsToMerge}.`);
const documentationEntity: ApiEntity = {
apiVersion: 'backstage.io/v1beta1',
kind: 'API',
metadata: {
name: 'INTERNAL_instance_openapi_doc',
title: 'Your Backstage Instance documentation',
annotations: {
'backstage.io/managed-by-location':
'internal-package:@backstage/plugin-catalog-backend-module-openapi-spec',
'backstage.io/managed-by-origin-location':
'internal-package:@backstage/plugin-catalog-backend-module-openapi-spec',
},
},
spec: {
type: 'openapi',
lifecycle: 'production',
owner: 'backstage',
definition: JSON.stringify(
await loadSpecs({
baseUrl: this.config.getString('backend.baseUrl'),
discovery: this.discovery,
plugins: pluginsToMerge,
logger,
}),
),
},
};
await this.connection?.applyMutation({
type: 'full',
entities: [
{
entity: documentationEntity,
locationKey: 'internal-api-doc',
},
],
});
}
}
@@ -0,0 +1,62 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
coreServices,
createBackendModule,
} from '@backstage/backend-plugin-api';
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
import { InternalOpenApiDocumentationProvider } from './InternalOpenApiDocumentationProvider';
/**
* @public
*/
export type MetaApiDocsPluginOptions = { exampleOption: boolean };
/**
* @public
*/
export const metaOpenApiDocsPluginId = 'meta-api-docs';
/**
* @public
*/
export const catalogModuleInternalOpenApiSpec = createBackendModule({
moduleId: metaOpenApiDocsPluginId,
pluginId: 'catalog',
register(env) {
env.registerInit({
deps: {
catalog: catalogProcessingExtensionPoint,
config: coreServices.rootConfig,
discovery: coreServices.discovery,
scheduler: coreServices.scheduler,
logger: coreServices.logger,
},
async init({ catalog, config, discovery, scheduler, logger }) {
console.log('testing');
catalog.addEntityProvider(
InternalOpenApiDocumentationProvider.fromConfig(config, {
discovery,
schedule: scheduler,
logger,
}),
);
},
});
},
});
export default catalogModuleInternalOpenApiSpec;
@@ -0,0 +1,16 @@
/*
* Copyright 2023 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 {};
+64 -2
View File
@@ -3544,7 +3544,9 @@ __metadata:
version: 0.0.0-use.local
resolution: "@backstage/backend-openapi-utils@workspace:packages/backend-openapi-utils"
dependencies:
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/config": "workspace:^"
"@backstage/errors": "workspace:^"
"@types/express": ^4.17.6
"@types/express-serve-static-core": ^4.17.5
@@ -3553,6 +3555,7 @@ __metadata:
express-promise-router: ^4.1.0
json-schema-to-ts: ^2.6.2
lodash: ^4.17.21
openapi-merge: ^1.3.2
openapi3-ts: ^3.1.2
supertest: ^6.1.3
languageName: unknown
@@ -5633,7 +5636,36 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/plugin-catalog-backend-module-openapi@workspace:plugins/catalog-backend-module-openapi":
"@backstage/plugin-catalog-backend-module-openapi-spec@workspace:^, @backstage/plugin-catalog-backend-module-openapi-spec@workspace:plugins/catalog-backend-module-openapi-spec":
version: 0.0.0-use.local
resolution: "@backstage/plugin-catalog-backend-module-openapi-spec@workspace:plugins/catalog-backend-module-openapi-spec"
dependencies:
"@backstage/backend-common": "workspace:^"
"@backstage/backend-openapi-utils": "workspace:^"
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-tasks": "workspace:^"
"@backstage/catalog-model": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/config": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/plugin-catalog-node": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@types/express": "*"
"@types/supertest": ^2.0.8
cross-fetch: ^3.1.5
express: ^4.17.1
express-promise-router: ^4.1.0
lodash: ^4.17.21
msw: ^1.0.0
openapi-merge: ^1.3.2
openapi3-ts: ^3.1.2
supertest: ^6.2.4
uuid: ^9.0.0
yn: ^4.0.0
languageName: unknown
linkType: soft
"@backstage/plugin-catalog-backend-module-openapi@workspace:^, @backstage/plugin-catalog-backend-module-openapi@workspace:plugins/catalog-backend-module-openapi":
version: 0.0.0-use.local
resolution: "@backstage/plugin-catalog-backend-module-openapi@workspace:plugins/catalog-backend-module-openapi"
dependencies:
@@ -20154,6 +20186,16 @@ __metadata:
languageName: node
linkType: hard
"atlassian-openapi@npm:^1.0.8":
version: 1.0.17
resolution: "atlassian-openapi@npm:1.0.17"
dependencies:
jsonpointer: ^5.0.0
urijs: ^1.19.10
checksum: 372a454e8f5e000e016e261b2151019f0b3dabfad908593c71aae23ebd5b9998c374ae3c0bf0d85dd55dbcca94d5d93e38edf02346d0bd2cb34d3fd20968ddaf
languageName: node
linkType: hard
"atomic-sleep@npm:^1.0.0":
version: 1.0.0
resolution: "atomic-sleep@npm:1.0.0"
@@ -25645,6 +25687,8 @@ __metadata:
"@backstage/plugin-azure-devops-backend": "workspace:^"
"@backstage/plugin-badges-backend": "workspace:^"
"@backstage/plugin-catalog-backend": "workspace:^"
"@backstage/plugin-catalog-backend-module-openapi": "workspace:^"
"@backstage/plugin-catalog-backend-module-openapi-spec": "workspace:^"
"@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "workspace:^"
"@backstage/plugin-catalog-backend-module-unprocessed": "workspace:^"
"@backstage/plugin-devtools-backend": "workspace:^"
@@ -34240,6 +34284,17 @@ __metadata:
languageName: node
linkType: hard
"openapi-merge@npm:^1.3.2":
version: 1.3.2
resolution: "openapi-merge@npm:1.3.2"
dependencies:
atlassian-openapi: ^1.0.8
lodash: ^4.17.15
ts-is-present: ^1.1.1
checksum: 53284a563270177422db8c7536544913c133dfc5cc7058a1043f3092b5aa997b8224a83c59569d18620f94ccf0a014fcb735e22941a9259b2c60861002f01638
languageName: node
linkType: hard
"openapi-sampler@npm:^1.2.1":
version: 1.2.1
resolution: "openapi-sampler@npm:1.2.1"
@@ -40922,6 +40977,13 @@ __metadata:
languageName: node
linkType: hard
"ts-is-present@npm:^1.1.1":
version: 1.2.2
resolution: "ts-is-present@npm:1.2.2"
checksum: 3620ecf48219d0dd108e493260a207f4733d8e39a18dffec23c7ed2b1ef2aba7158d0dfafe36f3f27d0092472535a5e474ce04ade54e972e64b2b6329d20ab0b
languageName: node
linkType: hard
"ts-log@npm:^2.2.3":
version: 2.2.5
resolution: "ts-log@npm:2.2.5"
@@ -41674,7 +41736,7 @@ __metadata:
languageName: node
linkType: hard
"urijs@npm:^1.19.11":
"urijs@npm:^1.19.10, urijs@npm:^1.19.11":
version: 1.19.11
resolution: "urijs@npm:1.19.11"
checksum: f9b95004560754d30fd7dbee44b47414d662dc9863f1cf5632a7c7983648df11d23c0be73b9b4f9554463b61d5b0a520b70df9e1ee963ebb4af02e6da2cc80f3