From 8eda7722a3d18dc2c679433573055e5003d53bfc Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Wed, 22 Feb 2023 15:30:50 -0500 Subject: [PATCH] Updating definitions to standardize to express type definitions. Signed-off-by: Aramis Sennyey --- plugins/catalog-backend/openapi.yaml | 49 +++ .../src/service/schema/openapi.ts | 83 +++- plugins/openapi-router-common/package.json | 2 +- plugins/openapi-router-common/src/example.ts | 41 ++ plugins/openapi-router-common/src/router.ts | 359 +----------------- .../src/schema/petstore.ts | 181 +++++++++ .../src/standaloneServer.ts | 2 +- .../openapi-router-common/src/types/common.ts | 36 +- .../src/types/express.ts | 86 +++++ .../src/types/requests.ts | 14 +- .../src/types/response.ts | 14 +- yarn.lock | 4 +- 12 files changed, 518 insertions(+), 353 deletions(-) create mode 100644 plugins/openapi-router-common/src/example.ts create mode 100644 plugins/openapi-router-common/src/schema/petstore.ts create mode 100644 plugins/openapi-router-common/src/types/express.ts diff --git a/plugins/catalog-backend/openapi.yaml b/plugins/catalog-backend/openapi.yaml index 037b4633b0..5feacb8599 100644 --- a/plugins/catalog-backend/openapi.yaml +++ b/plugins/catalog-backend/openapi.yaml @@ -480,6 +480,28 @@ components: - message - name description: The serialized form of an Error. + EntitiesQueryResponse: + type: object + properties: + items: + type: array + items: + - $ref: '#/components/schemas/Entity' + description: |- + The list of entities paginated by a specific filter. + totalItems: + type: number + pageInfo: + type: object + properties: + nextCursor: + type: string + description: |- + Base64 encoded database query for the next page. + prevCursor: + type: string + description: |- + Base64 encoded database query for the previous page. securitySchemes: JWT: type: http @@ -661,6 +683,33 @@ paths: type: array items: type: string + /entities/by-query: + get: + operationId: GetEntitiesByQuery + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/EntitiesQueryResponse' + security: + - {} + - JWT: [] + parameters: + - in: query + name: fields + required: false + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + type: string /entity-facets: get: operationId: GetEntityFacets diff --git a/plugins/catalog-backend/src/service/schema/openapi.ts b/plugins/catalog-backend/src/service/schema/openapi.ts index 81e585ee01..a759d50c59 100644 --- a/plugins/catalog-backend/src/service/schema/openapi.ts +++ b/plugins/catalog-backend/src/service/schema/openapi.ts @@ -352,6 +352,7 @@ export default { { type: 'object', properties: { + properties: null, links: { type: 'array', items: { @@ -404,9 +405,10 @@ export default { 'A globally unique ID for the entity.\nThis field can not be set by the user at creation time, and the server\nwill reject an attempt to do so. The field will be populated in read\noperations. The field can (optionally) be specified when performing\nupdate or delete operations, but the server is free to reject requests\nthat do so in such a way that it breaks semantics.', }, }, + description: + 'Metadata fields common to all versions/kinds of entity.', }, ], - description: 'Metadata fields common to all versions/kinds of entity.', }, RecursivePartial_Entity_: { type: 'object', @@ -443,7 +445,7 @@ export default { description: { type: 'string', description: - 'A text to show to the user to inform about the choices made. Like, it could say\n"Found a CODEOWNERS file that covers this target, so we suggest leaving this\nfield empty; which would currently make it owned by X" where X is taken from the\ncodeowners file.', + 'A text to show to the user to inform about the choices made. Like, it could say\n"Found a CODEOWNERS file that covers this target, so we suggest leaving this \nfield empty; which would currently make it owned by X" where X is taken from the\ncodeowners file.', }, value: { type: 'string', @@ -549,6 +551,37 @@ export default { ], description: 'The serialized form of an Error.', }, + EntitiesQueryResponse: { + type: 'object', + properties: { + items: { + type: 'array', + items: [ + { + $ref: '#/components/schemas/Entity', + }, + ], + description: 'The list of entities paginated by a specific filter.', + }, + totalItems: { + type: 'number', + }, + pageInfo: { + type: 'object', + properties: { + nextCursor: { + type: 'string', + description: 'Base64 encoded database query for the next page.', + }, + prevCursor: { + type: 'string', + description: + 'Base64 encoded database query for the previous page.', + }, + }, + }, + }, + }, }, securitySchemes: { JWT: { @@ -855,6 +888,52 @@ export default { }, }, }, + '/entities/by-query': { + get: { + operationId: 'GetEntitiesByQuery', + responses: { + '200': { + description: 'Ok', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/EntitiesQueryResponse', + }, + }, + }, + }, + }, + security: [ + {}, + { + JWT: [], + }, + ], + parameters: [ + { + in: 'query', + name: 'fields', + required: false, + schema: { + type: 'string', + }, + }, + ], + requestBody: { + required: true, + content: { + 'application/json': { + schema: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, '/entity-facets': { get: { operationId: 'GetEntityFacets', diff --git a/plugins/openapi-router-common/package.json b/plugins/openapi-router-common/package.json index 06a4c4e8ee..3c0857a5c7 100644 --- a/plugins/openapi-router-common/package.json +++ b/plugins/openapi-router-common/package.json @@ -30,7 +30,7 @@ "dist" ], "dependencies": { - "express": "^4.18.2", + "express": "^4.17.1", "json-schema-to-ts": "^2.6.2", "openapi-types": "^12.1.0", "openapi3-ts": "^3.1.2", diff --git a/plugins/openapi-router-common/src/example.ts b/plugins/openapi-router-common/src/example.ts new file mode 100644 index 0000000000..d2dba3298a --- /dev/null +++ b/plugins/openapi-router-common/src/example.ts @@ -0,0 +1,41 @@ +/* + * 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 express, { Router } from 'express'; +import { ApiRouter, DeepWriteable } from './router'; +import doc from './schema/petstore'; + +interface RouterOptions {} + +export async function createRouter( + options: RouterOptions, +): Promise { + console.log(options); + const router = Router() as ApiRouter>; + + router.get('/pets/:uid', (req, res) => { + res.json({ + id: 1, + name: req.params.uid, + }); + }); + + // router.get('/pet') will complain with a TS error + + router.post('/pets', (_, res) => { + res.send(); + }); + return router; +} diff --git a/plugins/openapi-router-common/src/router.ts b/plugins/openapi-router-common/src/router.ts index 62e116727b..16b627e62e 100644 --- a/plugins/openapi-router-common/src/router.ts +++ b/plugins/openapi-router-common/src/router.ts @@ -13,356 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ErrorRequestHandler, RequestHandler, Router } from 'express'; -import core, { ParamsDictionary } from 'express-serve-static-core'; -import { FromSchema, JSONSchema7 } from 'json-schema-to-ts'; -import { - DocPathMethod, - DocPathTemplate, - MethodAwareDocPath, - PathTemplate, - RequestBodySchema, - RequiredDoc, - ValueOf, -} from './types'; -import { ResponseSchemas } from './types/response'; +import { Router } from 'express'; +import { RequiredDoc } from './types'; +import { DocRequestMatcher } from './types/express'; export type DeepWriteable = { -readonly [P in keyof T]: DeepWriteable; }; - -const doc = { - openapi: '3.1.0', - info: { - version: '1.0.0', - title: 'Swagger Petstore', - license: { - name: 'MIT', - url: 'https://opensource.org/licenses/MIT', - }, - }, - servers: [ - { - url: 'http://petstore.swagger.io/v1', - }, - ], - paths: { - '/pets': { - get: { - summary: 'List all pets', - operationId: 'listPets', - tags: ['pets'], - parameters: [ - { - name: 'limit', - in: 'query', - description: 'How many items to return at one time (max 100)', - required: false, - schema: { - type: 'integer', - format: 'int32', - }, - }, - ], - responses: { - '200': { - description: 'A paged array of pets', - headers: { - 'x-next': { - description: 'A link to the next page of responses', - schema: { - type: 'string', - }, - }, - }, - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Pets', - }, - }, - }, - }, - default: { - description: 'unexpected error', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Error', - }, - }, - }, - }, - }, - }, - post: { - summary: 'Create a pet', - operationId: 'createPets', - tags: ['pets'], - responses: { - '201': { - description: 'Null response', - }, - default: { - description: 'unexpected error', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Error', - }, - }, - }, - }, - }, - }, - }, - '/pets/{petId}': { - get: { - summary: 'Info for a specific pet', - operationId: 'showPetById', - tags: ['pets'], - parameters: [ - { - name: 'petId', - in: 'path', - required: true, - description: 'The id of the pet to retrieve', - schema: { - type: 'string', - }, - }, - ], - responses: { - '200': { - description: 'Expected response to a valid request', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Pet', - }, - }, - }, - }, - default: { - description: 'unexpected error', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Error', - }, - }, - }, - }, - }, - }, - }, - }, - components: { - schemas: { - Pet: { - type: 'object', - required: ['id', 'name'], - properties: { - id: { - type: 'integer', - format: 'int64', - }, - name: { - type: 'string', - }, - tag: { - type: 'string', - }, - }, - }, - Pets: { - type: 'array', - items: { - $ref: '#/components/schemas/Pet', - }, - }, - Error: { - type: 'object', - required: ['code', 'message'], - properties: { - code: { - type: 'integer', - format: 'int32', - }, - message: { - type: 'string', - }, - }, - }, - }, - }, -} as const; - -type RemoveTail< - S extends string, - Tail extends string, -> = S extends `${infer P}${Tail}` ? P : S; -type GetRouteParameter = RemoveTail< - RemoveTail, `-${string}`>, - `.${string}` ->; -export type RouteParameters = string extends Route - ? ParamsDictionary - : Route extends `${string}(${string}` - ? ParamsDictionary - : Route extends `${string}:${infer Rest}` - ? (GetRouteParameter extends never - ? ParamsDictionary - : GetRouteParameter extends `${infer ParamName}?` - ? { [P in ParamName]?: string } - : { [P in GetRouteParameter]: string }) & - (Rest extends `${GetRouteParameter}${infer Next}` - ? RouteParameters - : unknown) - : {}; -interface ParsedQs { - [key: string]: undefined | string | string[] | ParsedQs | ParsedQs[]; -} - -// oh boy don't do this -type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( - k: infer I, -) => void - ? I - : never; -type LastOf = UnionToIntersection< - T extends any ? () => T : never -> extends () => infer R - ? R - : never; - -// TS4.0+ -type Push = [...T, V]; - -// TS4.1+ -type TuplifyUnion< - T, - L = LastOf, - N = [T] extends [never] ? true : false, -> = true extends N ? [] : Push>, L>; - -type ConvertAll = []> = T extends [ - infer First extends JSONSchema7, - ...infer Rest, -] - ? ConvertAll]> - : R; - -type ResponseBodyToJsonSchema< - Doc extends RequiredDoc, - Path extends PathTemplate>, - Method extends DocPathMethod, -> = ConvertAll< - TuplifyUnion>> ->[number]; - -type RequestBodyToJsonSchema< - Doc extends RequiredDoc, - Path extends PathTemplate>, - Method extends DocPathMethod, -> = ConvertAll< - TuplifyUnion>> ->[number]; -export type RequestHandlerParams< - P, - ResBody, - ReqBody, - ReqQuery, - LocalsObj extends Record, -> = - | RequestHandler - | ErrorRequestHandler; - -type DocRequestHandler< - Doc extends RequiredDoc, - Path extends DocPathTemplate, - Method extends keyof Doc['paths'][Path], -> = core.RequestHandler< - core.ParamsDictionary, - // From https://stackoverflow.com/questions/71393738/typescript-intersection-not-union-type-from-json-schema. - ResponseBodyToJsonSchema, - RequestBodyToJsonSchema, - ParsedQs, - Record ->; - -export interface ApiRouterMatcher< - Doc extends RequiredDoc, - Path extends DocPathTemplate, - Method extends DocPathMethod, - T, -> { - ( - path: MethodAwareDocPath, - ...handlers: Array> - ): T; -} - export interface ApiRouter extends Router { - get, 'get'>>( - path: Path, - ...handlers: DocRequestHandler[] - ): this; + get: DocRequestMatcher; - post, 'post'>>( - path: Path, - ...handlers: DocRequestHandler[] - ): this; + post: DocRequestMatcher; - all, 'all'>>( - path: Path, - ...handlers: DocRequestHandler[] - ): this; + all: DocRequestMatcher; - put, 'put'>>( - path: Path, - ...handlers: DocRequestHandler[] - ): this; - delete, 'delete'>>( - path: Path, - ...handlers: DocRequestHandler[] - ): this; - patch, 'patch'>>( - path: Path, - ...handlers: DocRequestHandler[] - ): this; - options< - Path extends MethodAwareDocPath, 'options'>, - >( - path: Path, - ...handlers: DocRequestHandler[] - ): this; - head, 'head'>>( - path: Path, - ...handlers: DocRequestHandler[] - ): this; -} - -interface RouterOptions {} - -export async function createRouter(options: RouterOptions) { - console.log(options); - const router = Router() as ApiRouter>; - - router.get('/pets/:uid', (req, res) => { - res.json({ - id: 1, - name: req.params.uid, - }); - }); - - // router.get('/pet') will complain with a TS error - - router.post('/pets', (req, res) => { - res.send({ - message: req.path, - code: 1, - }); - }); - return router; + put: DocRequestMatcher; + + delete: DocRequestMatcher; + + patch: DocRequestMatcher; + + options: DocRequestMatcher; + + head: DocRequestMatcher; } diff --git a/plugins/openapi-router-common/src/schema/petstore.ts b/plugins/openapi-router-common/src/schema/petstore.ts new file mode 100644 index 0000000000..308e3925e0 --- /dev/null +++ b/plugins/openapi-router-common/src/schema/petstore.ts @@ -0,0 +1,181 @@ +/* + * 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 default { + openapi: '3.1.0', + info: { + version: '1.0.0', + title: 'Swagger Petstore', + license: { + name: 'MIT', + url: 'https://opensource.org/licenses/MIT', + }, + }, + servers: [ + { + url: 'http://petstore.swagger.io/v1', + }, + ], + paths: { + '/pets': { + get: { + summary: 'List all pets', + operationId: 'listPets', + tags: ['pets'], + parameters: [ + { + name: 'limit', + in: 'query', + description: 'How many items to return at one time (max 100)', + required: false, + schema: { + type: 'integer', + format: 'int32', + }, + }, + ], + responses: { + '200': { + description: 'A paged array of pets', + headers: { + 'x-next': { + description: 'A link to the next page of responses', + schema: { + type: 'string', + }, + }, + }, + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Pets', + }, + }, + }, + }, + default: { + description: 'unexpected error', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error', + }, + }, + }, + }, + }, + }, + post: { + summary: 'Create a pet', + operationId: 'createPets', + tags: ['pets'], + responses: { + '201': { + description: 'Null response', + }, + default: { + description: 'unexpected error', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error', + }, + }, + }, + }, + }, + }, + }, + '/pets/{petId}': { + get: { + summary: 'Info for a specific pet', + operationId: 'showPetById', + tags: ['pets'], + parameters: [ + { + name: 'petId', + in: 'path', + required: true, + description: 'The id of the pet to retrieve', + schema: { + type: 'string', + }, + }, + ], + responses: { + '200': { + description: 'Expected response to a valid request', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Pet', + }, + }, + }, + }, + default: { + description: 'unexpected error', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error', + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Pet: { + type: 'object', + required: ['id', 'name'], + properties: { + id: { + type: 'integer', + format: 'int64', + }, + name: { + type: 'string', + }, + tag: { + type: 'string', + }, + }, + }, + Pets: { + type: 'array', + items: { + $ref: '#/components/schemas/Pet', + }, + }, + Error: { + type: 'object', + required: ['code', 'message'], + properties: { + code: { + type: 'integer', + format: 'int32', + }, + message: { + type: 'string', + }, + }, + }, + }, + }, +} as const; diff --git a/plugins/openapi-router-common/src/standaloneServer.ts b/plugins/openapi-router-common/src/standaloneServer.ts index aef8f006ff..f7c2ec2c61 100644 --- a/plugins/openapi-router-common/src/standaloneServer.ts +++ b/plugins/openapi-router-common/src/standaloneServer.ts @@ -17,7 +17,7 @@ import { Server } from 'http'; import { Logger } from 'winston'; import { createServiceBuilder } from '@backstage/backend-common'; -import { createRouter } from './router'; +import { createRouter } from './example'; export interface ServerOptions { port: number; diff --git a/plugins/openapi-router-common/src/types/common.ts b/plugins/openapi-router-common/src/types/common.ts index f514c88d13..834f1b49f5 100644 --- a/plugins/openapi-router-common/src/types/common.ts +++ b/plugins/openapi-router-common/src/types/common.ts @@ -18,6 +18,7 @@ * Pulled from https://github.com/varanauskas/oatx. */ +import { FromSchema, JSONSchema7 } from 'json-schema-to-ts'; import type { ContentObject, OpenAPIObject, @@ -25,7 +26,8 @@ import type { } from 'openapi3-ts'; export type RequiredDoc = Pick; -export type PathDoc = { paths: Record }; + +export type PathDoc = Pick; /** * Get value types of `T` @@ -129,4 +131,36 @@ export type ObjectWithContentSchema< Object extends { content?: ContentObject }, > = Object['content'] extends ContentObject ? SchemaRef + : unknown; + +/** + * From https://stackoverflow.com/questions/71393738/typescript-intersection-not-union-type-from-json-schema. + * + * StackOverflow says not to do this, but union types aren't possible any other way. + */ + +type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( + k: infer I, +) => void + ? I : never; +type LastOf = UnionToIntersection< + T extends any ? () => T : never +> extends () => infer R + ? R + : never; + +type Push = [...T, V]; + +export type TuplifyUnion< + T, + L = LastOf, + N = [T] extends [never] ? true : false, +> = true extends N ? [] : Push>, L>; + +export type ConvertAll = []> = T extends [ + infer First extends JSONSchema7, + ...infer Rest, +] + ? ConvertAll]> + : R; diff --git a/plugins/openapi-router-common/src/types/express.ts b/plugins/openapi-router-common/src/types/express.ts new file mode 100644 index 0000000000..055235af99 --- /dev/null +++ b/plugins/openapi-router-common/src/types/express.ts @@ -0,0 +1,86 @@ +/* + * 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 { Application } from 'express'; +import core from 'express-serve-static-core'; +import { DocPathTemplate, MethodAwareDocPath, RequiredDoc } from './common'; +import { RequestBodyToJsonSchema } from './requests'; +import { ResponseBodyToJsonSchema } from './response'; + +interface ParsedQs { + [key: string]: undefined | string | string[] | ParsedQs | ParsedQs[]; +} + +export type DocRequestHandler< + Doc extends RequiredDoc, + Path extends DocPathTemplate, + Method extends keyof Doc['paths'][Path], +> = core.RequestHandler< + core.ParamsDictionary, + ResponseBodyToJsonSchema, + RequestBodyToJsonSchema, + ParsedQs, + Record +>; + +export type DocRequestHandlerParams< + Doc extends RequiredDoc, + Path extends DocPathTemplate, + Method extends keyof Doc['paths'][Path], +> = core.RequestHandlerParams< + core.ParamsDictionary, + ResponseBodyToJsonSchema, + RequestBodyToJsonSchema, + ParsedQs, + Record +>; + +export type PathParams = string | RegExp | Array; + +export interface DocRequestMatcher< + Doc extends RequiredDoc, + T, + Method extends + | 'all' + | 'get' + | 'post' + | 'put' + | 'delete' + | 'patch' + | 'options' + | 'head' = any, +> { + , Method>>( + path: Path, + ...handlers: DocRequestHandler[] + ): T; + , Method>>( + path: Path, + ...handlers: Array> + ): T; + , Method>>( + path: Path, + ...handlers: Array> + ): T; + , Method>>( + path: PathParams, + ...handlers: Array> + ): T; + , Method>>( + path: PathParams, + ...handlers: Array> + ): T; + (path: PathParams, subApplication: Application): T; +} diff --git a/plugins/openapi-router-common/src/types/requests.ts b/plugins/openapi-router-common/src/types/requests.ts index 832cf34956..ae05a27537 100644 --- a/plugins/openapi-router-common/src/types/requests.ts +++ b/plugins/openapi-router-common/src/types/requests.ts @@ -28,6 +28,10 @@ import type { DocPath, DocPathMethod, DocPathTemplate, + PathTemplate, + ConvertAll, + TuplifyUnion, + ValueOf, } from './common'; type RequestBody< @@ -50,4 +54,12 @@ export type RequestBodySchema< Method extends DocPathMethod, > = RequestBody, Method> extends RequestBodyObject ? ObjectWithContentSchema, Method>> - : never; + : unknown; + +export type RequestBodyToJsonSchema< + Doc extends RequiredDoc, + Path extends PathTemplate>, + Method extends DocPathMethod, +> = ConvertAll< + TuplifyUnion>> +>[number]; diff --git a/plugins/openapi-router-common/src/types/response.ts b/plugins/openapi-router-common/src/types/response.ts index 93ec8a61fa..5c01a70673 100644 --- a/plugins/openapi-router-common/src/types/response.ts +++ b/plugins/openapi-router-common/src/types/response.ts @@ -28,6 +28,10 @@ import type { DocPath, DocPathMethod, DocPathTemplate, + PathTemplate, + ConvertAll, + ValueOf, + TuplifyUnion, } from './common'; type Response< @@ -81,5 +85,13 @@ export type ResponseSchemas< Doc, Responses, Method>[StatusCode] > - : never; + : unknown; }; + +export type ResponseBodyToJsonSchema< + Doc extends RequiredDoc, + Path extends PathTemplate>, + Method extends DocPathMethod, +> = ConvertAll< + TuplifyUnion>> +>[number]; diff --git a/yarn.lock b/yarn.lock index fd223f444f..a3a011d2ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7809,7 +7809,7 @@ __metadata: resolution: "@backstage/plugin-openapi-router@workspace:plugins/openapi-router-common" dependencies: "@backstage/cli": "workspace:^" - express: ^4.18.2 + express: ^4.17.1 json-schema-to-ts: ^2.6.2 openapi-types: ^12.1.0 openapi3-ts: ^3.1.2 @@ -24164,7 +24164,7 @@ __metadata: languageName: node linkType: hard -"express@npm:^4.17.1, express@npm:^4.17.3, express@npm:^4.18.1, express@npm:^4.18.2": +"express@npm:^4.17.1, express@npm:^4.17.3, express@npm:^4.18.1": version: 4.18.2 resolution: "express@npm:4.18.2" dependencies: