diff --git a/packages/backend-openapi-utils/src/index.ts b/packages/backend-openapi-utils/src/index.ts index 320ad75e25..6930ada0d6 100644 --- a/packages/backend-openapi-utils/src/index.ts +++ b/packages/backend-openapi-utils/src/index.ts @@ -36,5 +36,9 @@ export type { RequestMatcherByModelAndPathParams, } from './types/express'; export type { PathTemplate } from './types/common'; -export { createValidatedOpenApiRouter, getOpenApiSpecRoute } from './stub'; export { wrapInOpenApiTestServer, wrapServer } from './testUtils'; +export { + createValidatedOpenApiRouter, + getOpenApiSpecRoute, + createValidatedOpenApiRouterFromGeneratedEndpointMap, +} from './stub'; diff --git a/packages/backend-openapi-utils/src/stub.ts b/packages/backend-openapi-utils/src/stub.ts index 0f62bf348c..615c32cb31 100644 --- a/packages/backend-openapi-utils/src/stub.ts +++ b/packages/backend-openapi-utils/src/stub.ts @@ -16,7 +16,7 @@ import PromiseRouter from 'express-promise-router'; import { ApiRouter } from './router'; -import { RequiredDoc } from './types'; +import { EndpointMap, RequiredDoc, TypedRouter } from './types'; import { ErrorRequestHandler, RequestHandler, @@ -24,6 +24,7 @@ import { Request, Response, json, + Router, } from 'express'; import { InputError } from '@backstage/errors'; import { middleware as OpenApiValidator } from 'express-openapi-validator'; @@ -58,19 +59,19 @@ export function getOpenApiSpecRoute(baseUrl: string) { } /** - * Create a new OpenAPI router with some default middleware. + * Create a router with validation middleware. This is used by typing methods to create an + * "OpenAPI router" with all of the expected validation + metadata. * @param spec - Your OpenAPI spec imported as a JSON object. * @param validatorOptions - `openapi-express-validator` options to override the defaults. * @returns A new express router with validation middleware. - * @public */ -export function createValidatedOpenApiRouter( - spec: T, +function createRouterWithValidation( + spec: RequiredDoc, options?: { validatorOptions?: Partial['0']>; middleware?: RequestHandler[]; }, -) { +): Router { const router = PromiseRouter(); router.use(options?.middleware || getDefaultRouterMiddleware()); @@ -152,6 +153,41 @@ export function createValidatedOpenApiRouter( } res.json(mergeOutput.output); }); - - return router as ApiRouter; + return router; +} + +/** + * Create a new OpenAPI router with some default middleware. + * @param spec - Your OpenAPI spec imported as a JSON object. + * @param validatorOptions - `openapi-express-validator` options to override the defaults. + * @returns A new express router with validation middleware. + * @public + */ +export function createValidatedOpenApiRouter( + spec: T, + options?: { + validatorOptions?: Partial['0']>; + middleware?: RequestHandler[]; + }, +) { + return createRouterWithValidation(spec, options) as ApiRouter; +} + +/** + * Create a new OpenAPI router with some default middleware. + * @param spec - Your OpenAPI spec imported as a JSON object. + * @param validatorOptions - `openapi-express-validator` options to override the defaults. + * @returns A new express router with validation middleware. + * @public + */ +export function createValidatedOpenApiRouterFromGeneratedEndpointMap< + T extends EndpointMap, +>( + spec: RequiredDoc, + options?: { + validatorOptions?: Partial['0']>; + middleware?: RequestHandler[]; + }, +) { + return createRouterWithValidation(spec, options) as TypedRouter; } diff --git a/packages/backend-openapi-utils/src/types/generated.ts b/packages/backend-openapi-utils/src/types/generated.ts new file mode 100644 index 0000000000..31c1f7c28f --- /dev/null +++ b/packages/backend-openapi-utils/src/types/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. + */ +import { Router } from 'express'; +import type core from 'express-serve-static-core'; +import { PathTemplate, ValueOf } from './common'; + +export type EndpointMap = Record< + string, + { query?: object; body?: object; response: object | void; path?: object } +>; + +type UnknownIfVoid = T extends void ? unknown : T; + +// OpenAPI generator doesn't emit regular lowercase 'delete'. +type HttpMethods = 'all' | 'put' | 'get' | 'post' | '_delete'; + +type PathSchema< + Doc extends EndpointMap, + Endpoint extends DocEndpoint, + Method extends DocEndpointMethod, +> = `#${Method}|${Endpoint}` extends keyof Doc + ? 'path' extends keyof Doc[`#${Method}|${Endpoint}`] + ? Doc[`#${Method}|${Endpoint}`]['path'] + : never + : never; +type RequestBody< + Doc extends EndpointMap, + Endpoint extends DocEndpoint, + Method extends DocEndpointMethod, +> = `#${Method}|${Endpoint}` extends keyof Doc + ? 'body' extends keyof Doc[`#${Method}|${Endpoint}`] + ? Doc[`#${Method}|${Endpoint}`]['body'] + : unknown + : unknown; +type ResponseBody< + Doc extends EndpointMap, + Endpoint extends DocEndpoint, + Method extends DocEndpointMethod, +> = `#${Method}|${Endpoint}` extends keyof Doc + ? 'response' extends keyof Doc[`#${Method}|${Endpoint}`] + ? UnknownIfVoid + : unknown + : unknown; +type QuerySchema< + Doc extends EndpointMap, + Endpoint extends DocEndpoint, + Method extends DocEndpointMethod, +> = `#${Method}|${Endpoint}` extends keyof Doc + ? 'query' extends keyof Doc[`#${Method}|${Endpoint}`] + ? Doc[`#${Method}|${Endpoint}`]['query'] + : never + : never; + +/** + * Typed express request handler. + * @public + */ +export type EndpointMapRequestHandler< + Doc extends EndpointMap, + Path extends DocEndpoint, + Method extends DocEndpointMethod, +> = core.RequestHandler< + PathSchema, + ResponseBody, + RequestBody, + QuerySchema, + Record +>; + +/** + * Typed express error handler / request handler union type. + * @public + */ +export type EndpointMapRequestHandlerParams< + Doc extends EndpointMap, + Path extends DocEndpoint, + Method extends DocEndpointMethod, +> = core.RequestHandlerParams< + PathSchema, + ResponseBody, + RequestBody, + QuerySchema, + Record +>; + +type DocEndpoint = ValueOf<{ + [Template in keyof Doc]: Template extends `#${string}|${infer Endpoint}` + ? Endpoint + : never; +}>; + +type DocEndpointMethod< + Doc extends EndpointMap, + Endpoint extends DocEndpoint, +> = ValueOf<{ + [Template in keyof Doc]: Template extends `#${infer Method}|${Endpoint}` + ? Method + : never; +}>; + +type MethodAwareDocEndpoints< + Doc extends EndpointMap, + Endpoint extends DocEndpoint, + Method extends DocEndpointMethod, +> = ValueOf<{ + [Template in keyof Doc]: Template extends `#${Method}|${infer E}` + ? E extends DocEndpoint + ? PathTemplate + : never + : never; +}>; + +type DocEndpointTemplate = PathTemplate< + DocEndpoint +>; + +export type TemplateToDocEndpoint< + Doc extends EndpointMap, + Path extends DocEndpointTemplate, +> = ValueOf<{ + [Template in DocEndpoint]: Path extends PathTemplate