Update catalog-backend to use custom router.
Signed-off-by: Aramis Sennyey <sennyeya@amazon.com>
This commit is contained in:
committed by
Fredrik Adelöw
parent
e1962a4f0a
commit
eaefab31c6
@@ -54,6 +54,7 @@
|
||||
"@backstage/integration": "workspace:^",
|
||||
"@backstage/plugin-catalog-common": "workspace:^",
|
||||
"@backstage/plugin-catalog-node": "workspace:^",
|
||||
"@backstage/plugin-openapi-router": "workspace:^",
|
||||
"@backstage/plugin-permission-common": "workspace:^",
|
||||
"@backstage/plugin-permission-node": "workspace:^",
|
||||
"@backstage/plugin-scaffolder-common": "workspace:^",
|
||||
|
||||
@@ -23,9 +23,8 @@ import {
|
||||
stringifyEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import { InputError, NotFoundError, serializeError } from '@backstage/errors';
|
||||
import { NotFoundError, serializeError } from '@backstage/errors';
|
||||
import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import { Logger } from 'winston';
|
||||
import yn from 'yn';
|
||||
import { z } from 'zod';
|
||||
@@ -50,16 +49,8 @@ import {
|
||||
locationInput,
|
||||
validateRequestBody,
|
||||
} from './util';
|
||||
import { initialize } from 'express-openapi';
|
||||
import yaml from 'js-yaml';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
class ParsingError extends Error {
|
||||
toString() {
|
||||
return `ParsingError: ${this.message}`;
|
||||
}
|
||||
}
|
||||
import { ApiRouter, DeepWriteable } from '@backstage/plugin-openapi-router';
|
||||
import spec from './schema/openapi';
|
||||
|
||||
/**
|
||||
* Options used by {@link createRouter}.
|
||||
@@ -95,7 +86,9 @@ export async function createRouter(
|
||||
logger,
|
||||
permissionIntegrationRouter,
|
||||
} = options;
|
||||
const router = Router();
|
||||
const router = new ApiRouter<DeepWriteable<typeof spec>>(
|
||||
spec as DeepWriteable<typeof spec>,
|
||||
);
|
||||
router.use(express.json());
|
||||
|
||||
const readonlyEnabled =
|
||||
@@ -104,42 +97,26 @@ export async function createRouter(
|
||||
logger.info('Catalog is running in readonly mode');
|
||||
}
|
||||
|
||||
const validateDependency = (
|
||||
dependency: any,
|
||||
next: (req: express.Request, res: express.Response) => any,
|
||||
) => {
|
||||
return (req: express.Request, res: express.Response) => {
|
||||
if (!dependency) {
|
||||
console.log('no dependency');
|
||||
throw new NotFoundError(
|
||||
'Dependency Error',
|
||||
'Dependency not set up for this endpoint.',
|
||||
);
|
||||
}
|
||||
return next(req, res);
|
||||
};
|
||||
};
|
||||
if (refreshService) {
|
||||
router.post('/refresh', async (req, res) => {
|
||||
const refreshOptions: RefreshOptions = req.body;
|
||||
refreshOptions.authorizationToken = getBearerToken(
|
||||
req.header('authorization'),
|
||||
);
|
||||
|
||||
initialize({
|
||||
app: router as any,
|
||||
// NOTE: If using yaml you can provide a path relative to process.cwd() e.g.
|
||||
// apiDoc: './api-v1/api-doc.yml',
|
||||
apiDoc: yaml.load(
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
fs.readFileSync(path.resolve(__dirname, '../../openapi.yaml'), 'utf-8'),
|
||||
) as any,
|
||||
operations: {
|
||||
RefreshEntity: validateDependency(refreshService, async (req, res) => {
|
||||
const refreshOptions: RefreshOptions = req.body;
|
||||
refreshOptions.authorizationToken = getBearerToken(
|
||||
req.header('authorization'),
|
||||
);
|
||||
await refreshService.refresh(refreshOptions);
|
||||
res.status(200).end();
|
||||
});
|
||||
}
|
||||
|
||||
await refreshService!.refresh(refreshOptions);
|
||||
res.status(200).end();
|
||||
}),
|
||||
GetEntities: validateDependency(entitiesCatalog, async (req, res) => {
|
||||
const { entities, pageInfo } = await entitiesCatalog!.entities({
|
||||
if (permissionIntegrationRouter) {
|
||||
router.use(permissionIntegrationRouter);
|
||||
}
|
||||
|
||||
if (entitiesCatalog) {
|
||||
router
|
||||
.get('/entities', async (req, res) => {
|
||||
const { entities, pageInfo } = await entitiesCatalog.entities({
|
||||
filter: parseEntityFilterParams(req.query),
|
||||
fields: parseEntityTransformParams(req.query),
|
||||
order: parseEntityOrderParams(req.query),
|
||||
@@ -157,10 +134,10 @@ export async function createRouter(
|
||||
|
||||
// TODO(freben): encode the pageInfo in the response
|
||||
res.json(entities);
|
||||
}),
|
||||
GetEntityByUid: validateDependency(entitiesCatalog, async (req, res) => {
|
||||
})
|
||||
.get('/entities/by-uid/:uid', async (req, res) => {
|
||||
const { uid } = req.params;
|
||||
const { entities } = await entitiesCatalog!.entities({
|
||||
const { entities } = await entitiesCatalog.entities({
|
||||
filter: basicEntityFilter({ 'metadata.uid': uid }),
|
||||
authorizationToken: getBearerToken(req.header('authorization')),
|
||||
});
|
||||
@@ -168,10 +145,17 @@ export async function createRouter(
|
||||
throw new NotFoundError(`No entity with uid ${uid}`);
|
||||
}
|
||||
res.status(200).json(entities[0]);
|
||||
}),
|
||||
GetEntityByName: validateDependency(entitiesCatalog, async (req, res) => {
|
||||
})
|
||||
.delete('/entities/by-uid/:uid', async (req, res) => {
|
||||
const { uid } = req.params;
|
||||
await entitiesCatalog.removeEntityByUid(uid, {
|
||||
authorizationToken: getBearerToken(req.header('authorization')),
|
||||
});
|
||||
res.status(204).end();
|
||||
})
|
||||
.get('/entities/by-name/:kind/:namespace/:name', async (req, res) => {
|
||||
const { kind, namespace, name } = req.params;
|
||||
const { entities } = await entitiesCatalog!.entities({
|
||||
const { entities } = await entitiesCatalog.entities({
|
||||
filter: basicEntityFilter({
|
||||
kind: kind,
|
||||
'metadata.namespace': namespace,
|
||||
@@ -185,27 +169,41 @@ export async function createRouter(
|
||||
);
|
||||
}
|
||||
res.status(200).json(entities[0]);
|
||||
}),
|
||||
GetEntityAncestryByName: validateDependency(
|
||||
entitiesCatalog,
|
||||
})
|
||||
.get(
|
||||
'/entities/by-name/:kind/:namespace/:name/ancestry',
|
||||
async (req, res) => {
|
||||
const { kind, namespace, name } = req.params;
|
||||
const entityRef = stringifyEntityRef({ kind, namespace, name });
|
||||
const response = await entitiesCatalog!.entityAncestry(entityRef, {
|
||||
const response = await entitiesCatalog.entityAncestry(entityRef, {
|
||||
authorizationToken: getBearerToken(req.header('authorization')),
|
||||
});
|
||||
res.status(200).json(response);
|
||||
},
|
||||
),
|
||||
GetEntityFacets: validateDependency(entitiesCatalog, async (req, res) => {
|
||||
const response = await entitiesCatalog!.facets({
|
||||
)
|
||||
.post('/entities/by-refs', async (req, res) => {
|
||||
const request = entitiesBatchRequest(req);
|
||||
const token = getBearerToken(req.header('authorization'));
|
||||
const response = await entitiesCatalog.entitiesBatch({
|
||||
entityRefs: request.entityRefs,
|
||||
fields: parseEntityTransformParams(req.query, request.fields),
|
||||
authorizationToken: token,
|
||||
});
|
||||
res.status(200).json(response);
|
||||
})
|
||||
.get('/entity-facets', async (req, res) => {
|
||||
const response = await entitiesCatalog.facets({
|
||||
filter: parseEntityFilterParams(req.query),
|
||||
facets: parseEntityFacetParams(req.query),
|
||||
authorizationToken: getBearerToken(req.header('authorization')),
|
||||
});
|
||||
res.status(200).json(response);
|
||||
}),
|
||||
CreateLocation: validateDependency(locationService, async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (locationService) {
|
||||
router
|
||||
.post('/locations', async (req, res) => {
|
||||
const location = await validateRequestBody(req, locationInput);
|
||||
const dryRun = yn(req.query.dryRun, { default: false });
|
||||
|
||||
@@ -219,21 +217,22 @@ export async function createRouter(
|
||||
authorizationToken: getBearerToken(req.header('authorization')),
|
||||
});
|
||||
res.status(201).json(output);
|
||||
}),
|
||||
GetLocations: validateDependency(locationService, async (req, res) => {
|
||||
})
|
||||
.get('/locations', async (req, res) => {
|
||||
const locations = await locationService.listLocations({
|
||||
authorizationToken: getBearerToken(req.header('authorization')),
|
||||
});
|
||||
res.status(200).json(locations.map(l => ({ data: l })));
|
||||
}),
|
||||
GetLocation: validateDependency(locationService, async (req, res) => {
|
||||
})
|
||||
|
||||
.get('/locations/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const output = await locationService.getLocation(id, {
|
||||
authorizationToken: getBearerToken(req.header('authorization')),
|
||||
});
|
||||
res.status(200).json(output);
|
||||
}),
|
||||
DeleteLocation: validateDependency(locationService, async (req, res) => {
|
||||
})
|
||||
.delete('/locations/:id', async (req, res) => {
|
||||
disallowReadonlyMode(readonlyEnabled);
|
||||
|
||||
const { id } = req.params;
|
||||
@@ -241,115 +240,75 @@ export async function createRouter(
|
||||
authorizationToken: getBearerToken(req.header('authorization')),
|
||||
});
|
||||
res.status(204).end();
|
||||
}),
|
||||
AnalyzeLocation: validateDependency(locationService, async (req, res) => {
|
||||
const body = await validateRequestBody(
|
||||
req,
|
||||
z.object({
|
||||
location: locationInput,
|
||||
catalogFilename: z.string().optional(),
|
||||
}),
|
||||
);
|
||||
const schema = z.object({
|
||||
});
|
||||
}
|
||||
|
||||
if (locationAnalyzer) {
|
||||
router.post('/analyze-location', async (req, res) => {
|
||||
const body = await validateRequestBody(
|
||||
req,
|
||||
z.object({
|
||||
location: locationInput,
|
||||
catalogFilename: z.string().optional(),
|
||||
});
|
||||
const output = await locationAnalyzer!.analyzeLocation(
|
||||
schema.parse(body),
|
||||
);
|
||||
res.status(200).json(output);
|
||||
}),
|
||||
ValidateEntity: validateDependency(orchestrator, async (req, res) => {
|
||||
const bodySchema = z.object({
|
||||
entity: z.unknown(),
|
||||
location: z.string(),
|
||||
});
|
||||
}),
|
||||
);
|
||||
const schema = z.object({
|
||||
location: locationInput,
|
||||
catalogFilename: z.string().optional(),
|
||||
});
|
||||
const output = await locationAnalyzer.analyzeLocation(schema.parse(body));
|
||||
res.status(200).json(output);
|
||||
});
|
||||
}
|
||||
|
||||
let body: z.infer<typeof bodySchema>;
|
||||
let entity: Entity;
|
||||
let location: { type: string; target: string };
|
||||
try {
|
||||
body = await validateRequestBody(req, bodySchema);
|
||||
entity = validateEntityEnvelope(body.entity);
|
||||
location = parseLocationRef(body.location);
|
||||
if (location.type !== 'url')
|
||||
throw new TypeError(
|
||||
`Invalid location ref ${body.location}, only 'url:<target>' is supported, e.g. url:https://host/path`,
|
||||
);
|
||||
} catch (err) {
|
||||
return res.status(400).json({
|
||||
errors: [serializeError(err)],
|
||||
});
|
||||
}
|
||||
if (orchestrator) {
|
||||
router.post('/validate-entity', async (req, res) => {
|
||||
const bodySchema = z.object({
|
||||
entity: z.unknown(),
|
||||
location: z.string(),
|
||||
});
|
||||
|
||||
const processingResult = await orchestrator!.process({
|
||||
entity: {
|
||||
...entity,
|
||||
metadata: {
|
||||
...entity.metadata,
|
||||
annotations: {
|
||||
[ANNOTATION_LOCATION]: body.location,
|
||||
[ANNOTATION_ORIGIN_LOCATION]: body.location,
|
||||
...entity.metadata.annotations,
|
||||
},
|
||||
let body: z.infer<typeof bodySchema>;
|
||||
let entity: Entity;
|
||||
let location: { type: string; target: string };
|
||||
try {
|
||||
body = await validateRequestBody(req, bodySchema);
|
||||
entity = validateEntityEnvelope(body.entity);
|
||||
location = parseLocationRef(body.location);
|
||||
if (location.type !== 'url')
|
||||
throw new TypeError(
|
||||
`Invalid location ref ${body.location}, only 'url:<target>' is supported, e.g. url:https://host/path`,
|
||||
);
|
||||
} catch (err) {
|
||||
return res.status(400).json({
|
||||
errors: [serializeError(err)],
|
||||
});
|
||||
}
|
||||
|
||||
const processingResult = await orchestrator.process({
|
||||
entity: {
|
||||
...entity,
|
||||
metadata: {
|
||||
...entity.metadata,
|
||||
annotations: {
|
||||
[ANNOTATION_LOCATION]: body.location,
|
||||
[ANNOTATION_ORIGIN_LOCATION]: body.location,
|
||||
...entity.metadata.annotations,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!processingResult.ok)
|
||||
res.status(400).json({
|
||||
errors: processingResult.errors.map(e => serializeError(e)),
|
||||
});
|
||||
|
||||
if (!processingResult.ok)
|
||||
res.status(400).json({
|
||||
errors: processingResult.errors.map(e => serializeError(e)),
|
||||
});
|
||||
return res.status(200).end();
|
||||
}),
|
||||
DeleteEntityByUid: validateDependency(
|
||||
entitiesCatalog,
|
||||
async (req, res) => {
|
||||
const { uid } = req.params;
|
||||
await entitiesCatalog!.removeEntityByUid(uid, {
|
||||
authorizationToken: getBearerToken(req.header('authorization')),
|
||||
});
|
||||
res.status(204).end();
|
||||
},
|
||||
),
|
||||
GetEntitiesByRefs: validateDependency(
|
||||
entitiesCatalog,
|
||||
async (req, res) => {
|
||||
const request = entitiesBatchRequest(req);
|
||||
const token = getBearerToken(req.header('authorization'));
|
||||
const response = await entitiesCatalog!.entitiesBatch({
|
||||
entityRefs: request.entityRefs,
|
||||
fields: parseEntityTransformParams(req.query, request.fields),
|
||||
authorizationToken: token,
|
||||
});
|
||||
res.status(200).json(response);
|
||||
},
|
||||
),
|
||||
},
|
||||
enableObjectCoercion: true,
|
||||
errorMiddleware: errorHandler(),
|
||||
errorTransformer: openapiError => {
|
||||
const error = openapiError as {
|
||||
errorCode: string;
|
||||
path: string;
|
||||
message: string;
|
||||
};
|
||||
// eslint-disable-next-line default-case
|
||||
switch (error.errorCode) {
|
||||
case 'type.openapi.requestValidation':
|
||||
throw new InputError(
|
||||
`Invalid field ${error.path}`,
|
||||
new ParsingError(error.message),
|
||||
);
|
||||
}
|
||||
return {};
|
||||
},
|
||||
});
|
||||
|
||||
if (permissionIntegrationRouter) {
|
||||
router.use(permissionIntegrationRouter);
|
||||
return res.status(200).end();
|
||||
});
|
||||
}
|
||||
return router;
|
||||
|
||||
router.use(errorHandler());
|
||||
return router.build();
|
||||
}
|
||||
|
||||
function getBearerToken(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,8 @@
|
||||
"json-schema-to-ts": "^2.6.2",
|
||||
"openapi-types": "^12.1.0",
|
||||
"openapi3-ts": "^3.1.2",
|
||||
"ts-node": "^10.9.1"
|
||||
"ts-node": "^10.9.1",
|
||||
"winston": "^3.8.2",
|
||||
"yn": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types';
|
||||
import { IRouter, Router } from 'express';
|
||||
import { Router } from 'express';
|
||||
import core, { ParamsDictionary } from 'express-serve-static-core';
|
||||
import { FromSchema, JSONSchema7 } from 'json-schema-to-ts';
|
||||
import {
|
||||
DocPath,
|
||||
DocPathMethod,
|
||||
DocPathTemplate,
|
||||
MethodAwareDocPath,
|
||||
@@ -29,7 +28,9 @@ import {
|
||||
} from './types';
|
||||
import { ResponseSchemas } from './types/response';
|
||||
|
||||
type DeepWriteable<T> = { -readonly [P in keyof T]: DeepWriteable<T[P]> };
|
||||
export type DeepWriteable<T> = {
|
||||
-readonly [P in keyof T]: DeepWriteable<T[P]>;
|
||||
};
|
||||
|
||||
const doc = {
|
||||
openapi: '3.1.0',
|
||||
@@ -253,7 +254,7 @@ type ConvertAll<T, R extends ReadonlyArray<unknown> = []> = T extends [
|
||||
? ConvertAll<Rest, [...R, FromSchema<First>]>
|
||||
: R;
|
||||
|
||||
type ResponseToJsonSchema<
|
||||
type ResponseBodyToJsonSchema<
|
||||
Doc extends RequiredDoc,
|
||||
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
|
||||
Method extends DocPathMethod<Doc, Path>,
|
||||
@@ -261,6 +262,14 @@ type ResponseToJsonSchema<
|
||||
TuplifyUnion<ValueOf<ResponseSchemas<Doc, Path, Method>>>
|
||||
>[number];
|
||||
|
||||
type RequestBodyToJsonSchema<
|
||||
Doc extends RequiredDoc,
|
||||
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
|
||||
Method extends DocPathMethod<Doc, Path>,
|
||||
> = ConvertAll<
|
||||
TuplifyUnion<ValueOf<RequestBodySchema<Doc, Path, Method>>>
|
||||
>[number];
|
||||
|
||||
type DocRequestHandler<
|
||||
Doc extends RequiredDoc,
|
||||
Path extends DocPathTemplate<Doc>,
|
||||
@@ -268,13 +277,13 @@ type DocRequestHandler<
|
||||
> = core.RequestHandler<
|
||||
core.ParamsDictionary,
|
||||
// From https://stackoverflow.com/questions/71393738/typescript-intersection-not-union-type-from-json-schema.
|
||||
ResponseToJsonSchema<Doc, Path, Method>,
|
||||
RequestBodySchema<Doc, Path, Method>,
|
||||
ResponseBodyToJsonSchema<Doc, Path, Method>,
|
||||
RequestBodyToJsonSchema<Doc, Path, Method>,
|
||||
ParsedQs,
|
||||
Record<string, string>
|
||||
>;
|
||||
|
||||
export default class ApiRouter<Doc extends RequiredDoc> {
|
||||
export class ApiRouter<Doc extends RequiredDoc> {
|
||||
private _router = Router();
|
||||
|
||||
constructor(private spec: OpenAPIV3_1.Document | OpenAPIV3.Document) {}
|
||||
@@ -372,7 +381,7 @@ export async function createRouter(options: RouterOptions) {
|
||||
router.get('/pets/:uid', (req, res) => {
|
||||
res.json({
|
||||
id: 1,
|
||||
name: req.params['uid'],
|
||||
name: req.params.uid,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Pulled from https://github.com/varanauskas/oatx.
|
||||
*/
|
||||
|
||||
@@ -1,2 +1,17 @@
|
||||
/*
|
||||
* 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 * from './common';
|
||||
export * from './requests';
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Pulled from https://github.com/varanauskas/oatx.
|
||||
*/
|
||||
|
||||
import type { ReferenceObject, RequestBodyObject } from 'openapi3-ts';
|
||||
import type {
|
||||
ComponentRef,
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Pulled from https://github.com/varanauskas/oatx.
|
||||
*/
|
||||
|
||||
import type { ReferenceObject, ResponseObject } from 'openapi3-ts';
|
||||
import type {
|
||||
ComponentRef,
|
||||
|
||||
@@ -5353,6 +5353,7 @@ __metadata:
|
||||
"@backstage/integration": "workspace:^"
|
||||
"@backstage/plugin-catalog-common": "workspace:^"
|
||||
"@backstage/plugin-catalog-node": "workspace:^"
|
||||
"@backstage/plugin-openapi-router": "workspace:^"
|
||||
"@backstage/plugin-permission-common": "workspace:^"
|
||||
"@backstage/plugin-permission-node": "workspace:^"
|
||||
"@backstage/plugin-scaffolder-common": "workspace:^"
|
||||
@@ -7446,7 +7447,7 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@backstage/plugin-openapi-router@workspace:plugins/openapi-router-common":
|
||||
"@backstage/plugin-openapi-router@workspace:^, @backstage/plugin-openapi-router@workspace:plugins/openapi-router-common":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/plugin-openapi-router@workspace:plugins/openapi-router-common"
|
||||
dependencies:
|
||||
@@ -7456,6 +7457,8 @@ __metadata:
|
||||
openapi-types: ^12.1.0
|
||||
openapi3-ts: ^3.1.2
|
||||
ts-node: ^10.9.1
|
||||
winston: ^3.8.2
|
||||
yn: ^5.0.0
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
@@ -40279,7 +40282,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"winston@npm:^3.2.1":
|
||||
"winston@npm:^3.2.1, winston@npm:^3.8.2":
|
||||
version: 3.8.2
|
||||
resolution: "winston@npm:3.8.2"
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user