diff --git a/.changeset/big-tips-heal.md b/.changeset/big-tips-heal.md new file mode 100644 index 0000000000..a223cda256 --- /dev/null +++ b/.changeset/big-tips-heal.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-client': minor +--- + +Internal update to use the updated generated code from `backstage-cli package schema openapi generate --client-package ...`. diff --git a/.changeset/fresh-jeans-relate.md b/.changeset/fresh-jeans-relate.md new file mode 100644 index 0000000000..a8f6ad8e0b --- /dev/null +++ b/.changeset/fresh-jeans-relate.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': minor +--- + +**BREAKING**: Updates ESLint config to ignore all generated source code under `src/**/generated/**/*.ts`. diff --git a/.changeset/healthy-tables-wonder.md b/.changeset/healthy-tables-wonder.md new file mode 100644 index 0000000000..be6da63de0 --- /dev/null +++ b/.changeset/healthy-tables-wonder.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog-backend': minor +'@backstage/plugin-search-backend': minor +--- + +Internal update to use the new generated server types from `backstage-cli package schema openapi generate --server`. diff --git a/.changeset/silver-ears-grin.md b/.changeset/silver-ears-grin.md new file mode 100644 index 0000000000..104c18719e --- /dev/null +++ b/.changeset/silver-ears-grin.md @@ -0,0 +1,5 @@ +--- +'@backstage/repo-tools': minor +--- + +`backstage-repo-tools package schema openapi generate --server` now generates complete TS interfaces for all request/response objects in your OpenAPI schema. This fixes an edge case around recursive schemas and standardizes both the generated client and server to have similar generated types. diff --git a/.changeset/ten-pears-unite.md b/.changeset/ten-pears-unite.md new file mode 100644 index 0000000000..75f7e2cc22 --- /dev/null +++ b/.changeset/ten-pears-unite.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-openapi-utils': minor +--- + +Adds a new `createValidatedOpenApiRouterFromGeneratedEndpointMap` function that uses the new static server generation in `backstage-cli package schema openapi generate --server` to create a typed express router. diff --git a/docs/openapi/01-getting-started.md b/docs/openapi/01-getting-started.md index 156df9bd17..ce14cf6a7f 100644 --- a/docs/openapi/01-getting-started.md +++ b/docs/openapi/01-getting-started.md @@ -35,10 +35,10 @@ There are two required npm packages before we start, 1. `@backstage/repo-tools`, this package contains all OpenAPI-related commands for your plugins. We will be using this throughout the tutorial. 2. `@useoptic/optic`, this package is a dependency of `@backstage/repo-tools` but is only required for OpenAPI-related commands. -Further, for generating the client a `java` binary has to be available on your PATH. - You should install both of the above packages in the _root_ of your workspace. +Further, a `java` binary has to be available on your PATH. + ## Storing your OpenAPI specification You should create a new folder, `src/schema` in your backend plugin to store your OpenAPI (and any other) specifications. For example, if you're adding a specification to the catalog plugin, you would add a `src/schema` folder to `plugins/catalog-backend`, making a `plugins/catalog-backend/src/schema` directory. This directory should have an `openapi.yaml` file inside. @@ -47,12 +47,14 @@ You should create a new folder, `src/schema` in your backend plugin to store you ## Generating a typed express router from a spec -Run `yarn backstage-repo-tools package schema openapi generate --server` from the directory with your plugin. This will create an `openapi.generated.ts` file in the `src/schema` directory that contains the OpenAPI schema as well as a generated express router with types. You should add this command to your `package.json` for future use, and you can combine both the server generation and the client generation below, like so, `yarn backstage-repo-tools package schema openapi generate --server --client-package ` +Run `yarn backstage-repo-tools package schema openapi generate --server` from the directory with your plugin. This will create a `router.ts` file in the `src/schema/openapi/generated` directory that contains the OpenAPI schema as well as a factory function for a generated express router with types that match your schema. + +You should add this command to your `package.json` for future use and you can combine both the server generation and the client generation below like so, `yarn backstage-repo-tools package schema openapi generate --server --client-package ` Use it like so, update your `router.ts` or `createRouter.ts` file with the following content, ```diff -+ import { createOpenApiRouter } from '../schema/openapi.generated'; ++ import { createOpenApiRouter } from '../schema/openapi'; - import Router from 'express-promise-router'; ... @@ -65,12 +67,12 @@ export async function createRouter( ## Generating a typed client from a spec -From your current backend plugin directory, run `yarn backstage-repo-tools package schema openapi generate --client-package `. `` is a new directory and npm package that you should create. The general pattern is `plugins/-client` or if you want to co-locate this with your other shared types, use `plugins/-common`. You should add this command to your `package.json` for future use. +From your current backend plugin directory, run `yarn backstage-repo-tools package schema openapi generate --client-package `. `` is a new directory and npm package that you should create. The general pattern is to add a new entry point to your plugin's common package, `plugins/-common/client`. You should add this command to your `package.json` for future use. -The generated client will have a directory `src/generated` that exports a `DefaultApiClient` class and all generated types. You can use the client like so, +The generated client will have a directory `src/schema/openapi/generated` that exports a `DefaultApiClient` class and all generated types. You can use the client like so, ```diff -+ import { DefaultApiClient } from './generated'; ++ import { DefaultApiClient } from '../schema/openapi/generated'; export class CatalogClient implements CatalogApi { + private readonly apiClient: DefaultApiClient; diff --git a/docs/openapi/generate-client.md b/docs/openapi/generate-client.md index b8acc331e5..29756dcad2 100644 --- a/docs/openapi/generate-client.md +++ b/docs/openapi/generate-client.md @@ -20,9 +20,7 @@ info: ### Generating your client -1. Run `yarn backstage-repo-tools package schema openapi generate client --client-package `. This will create a new folder in `/src/generated` to house the generated content. -2. You should use the generated files as follows, - -- `apis/DefaultApi.client.ts` - this is the client that you should use. It has types for all of the various operations on your API. -- `models/*` - These are the types generated from your OpenAPI file, ideally you should not need to use these directly and can instead use the inferred types from `apis/DefaultApi.client.ts`. -- everything else is directory specific and shouldn't be touched. +1. Run `yarn backstage-repo-tools package schema openapi generate --client-package `. This will create a new folder in `/src/schema/openapi/generated` to house the generated content. +2. You should not need to import anything from subfolders of the `src/schema/openapi/generated` parent folder, everything you should require will be accessible from the `src/schema/openapi/generated/index.ts` file. Of note, +3. `DefaultApiClient` - this is the client that you can use to access your specific spec. +4. Any request or response types - these will be available from the index and should match the names in your spec. diff --git a/packages/backend-openapi-utils/report.api.md b/packages/backend-openapi-utils/report.api.md index 2f660cd0d6..399705a9e4 100644 --- a/packages/backend-openapi-utils/report.api.md +++ b/packages/backend-openapi-utils/report.api.md @@ -102,12 +102,45 @@ export function createValidatedOpenApiRouter( }, ): ApiRouter; +// @public +export function createValidatedOpenApiRouterFromGeneratedEndpointMap< + T extends EndpointMap, +>( + spec: RequiredDoc, + options?: { + validatorOptions?: Partial['0']>; + middleware?: RequestHandler[]; + }, +): TypedRouter; + // @public (undocumented) type DiscriminateUnion = Extract< T, Record >; +// @public (undocumented) +type DocEndpoint = ValueOf<{ + [Template in keyof Doc]: Template extends `#${string}|${infer Endpoint}` + ? Endpoint + : never; +}>; + +// @public (undocumented) +type DocEndpointMethod< + Doc extends EndpointMap, + Endpoint extends DocEndpoint, +> = ValueOf<{ + [Template in keyof Doc]: Template extends `#${infer Method}|${Endpoint}` + ? Method + : never; +}>; + +// @public (undocumented) +type DocEndpointTemplate = PathTemplate< + DocEndpoint +>; + // @public (undocumented) type DocOperation< Doc extends RequiredDoc, @@ -239,6 +272,85 @@ interface DocRequestMatcher< ): T; } +// @public (undocumented) +type EndpointMap = Record< + string, + { + query?: object; + body?: object; + response?: object | void; + path?: object; + } +>; + +// @public +type EndpointMapRequestHandler< + Doc extends EndpointMap, + Path extends DocEndpoint, + Method extends DocEndpointMethod, +> = core.RequestHandler< + StaticPathParamsSchema, + StaticResponseSchema, + StaticRequestBodySchema, + StaticQueryParamsSchema, + Record +>; + +// @public +type EndpointMapRequestHandlerParams< + Doc extends EndpointMap, + Path extends DocEndpoint, + Method extends DocEndpointMethod, +> = core.RequestHandlerParams< + StaticPathParamsSchema, + StaticResponseSchema, + StaticRequestBodySchema, + StaticQueryParamsSchema, + Record +>; + +// @public +interface EndpointMapRequestMatcher< + Doc extends EndpointMap, + T, + Method extends HttpMethods, +> { + // (undocumented) + < + TPath extends MethodAwareDocEndpoints< + Doc, + DocEndpoint, + Method & DocEndpointMethod> + >, + TMethod extends Method & + DocEndpointMethod>, + >( + path: TPath, + ...handlers: Array< + EndpointMapRequestHandler, TMethod> + > + ): T; + // (undocumented) + < + TPath extends MethodAwareDocEndpoints< + Doc, + DocEndpoint, + Method & DocEndpointMethod> + >, + TMethod extends Method & + DocEndpointMethod>, + >( + path: TPath, + ...handlers: Array< + EndpointMapRequestHandlerParams< + Doc, + TemplateToDocEndpoint, + TMethod + > + > + ): T; +} + // @public (undocumented) type Filter = T extends U ? T : never; @@ -278,6 +390,9 @@ type HeaderSchema< Method extends DocPathMethod, > = ParametersSchema; +// @public (undocumented) +type HttpMethods = 'all' | 'put' | 'get' | 'post' | '_delete'; + // @public type Immutable = T extends | Function @@ -401,6 +516,21 @@ declare namespace internal { Response_3 as Response, ResponseSchemas, ResponseBodyToJsonSchema, + EndpointMap, + HttpMethods, + StaticPathParamsSchema, + StaticRequestBodySchema, + StaticResponseSchema, + StaticQueryParamsSchema, + EndpointMapRequestHandler, + EndpointMapRequestHandlerParams, + DocEndpoint, + DocEndpointMethod, + MethodAwareDocEndpoints, + DocEndpointTemplate, + TemplateToDocEndpoint, + EndpointMapRequestMatcher, + TypedRouter, }; } export { internal }; @@ -427,6 +557,19 @@ type MapToSchema< : never; }; +// @public (undocumented) +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; +}>; + // @public (undocumented) type MethodAwareDocPath< Doc extends PathDoc, @@ -513,7 +656,7 @@ type PathSchema< > = ParametersSchema; // @public -type PathTemplate = +export type PathTemplate = Path extends `${infer Prefix}{${infer PathName}}${infer Suffix}` ? `${Prefix}:${PathName}${PathTemplate}` : Path; @@ -684,6 +827,60 @@ type SchemaRef = Schema extends { [Key in keyof Schema]: SchemaRef; }; +// @public (undocumented) +type StaticPathParamsSchema< + 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; + +// @public (undocumented) +type StaticQueryParamsSchema< + 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; + +// @public (undocumented) +type StaticRequestBodySchema< + 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; + +// @public (undocumented) +type StaticResponseSchema< + Doc extends EndpointMap, + Endpoint extends DocEndpoint, + Method extends DocEndpointMethod, +> = `#${Method}|${Endpoint}` extends keyof Doc + ? 'response' extends keyof Doc[`#${Method}|${Endpoint}`] + ? Doc[`#${Method}|${Endpoint}`]['response'] + : unknown + : unknown; + +// @public (undocumented) +type TemplateToDocEndpoint< + Doc extends EndpointMap, + Path extends DocEndpointTemplate, +> = ValueOf<{ + [Template in DocEndpoint]: Path extends PathTemplate