Update api report and shift openapi-utils to a devDependency.

Signed-off-by: Aramis Sennyey <sennyeya@amazon.com>
This commit is contained in:
Aramis Sennyey
2023-03-16 12:13:17 -04:00
committed by Fredrik Adelöw
parent 4efe0a0877
commit abd4ff4cee
17 changed files with 94 additions and 1118 deletions
+1 -1
View File
@@ -77,7 +77,7 @@ components:
required: false
schema:
type: integer
minimum: 1
minimum: 0
limit:
name: limit
in: query
+1 -1
View File
@@ -46,7 +46,6 @@
},
"dependencies": {
"@backstage/backend-common": "workspace:^",
"@backstage/backend-openapi-utils": "workspace:^",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/catalog-client": "workspace:^",
"@backstage/catalog-model": "workspace:^",
@@ -85,6 +84,7 @@
"zod": "^3.21.4"
},
"devDependencies": {
"@backstage/backend-openapi-utils": "workspace:^",
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/plugin-permission-common": "workspace:^",
@@ -50,7 +50,7 @@ import {
locationInput,
validateRequestBody,
} from './util';
import { ApiRouter } from '@backstage/backend-openapi-utils';
import type { ApiRouter } from '@backstage/backend-openapi-utils';
import spec from '../../schema/openapi';
/**
+18 -28
View File
@@ -1,55 +1,45 @@
# @backstage/plugin-openapi-router
# @backstage/openapi-utils
## Purpose
## Summary
This package is meant to provide a typed Express router for an OpenAPI spec. Specs must be converted to JSON and then copied to a Typescript file.
This package is meant to provide a typed Express router for an OpenAPI spec. Based on the [oatx](https://github.com/varanauskas/oatx) library and adapted to override Express values.
## Getting Started
### Configuration
In your plugin's `schema/openapi.ts`,
1. Run `yarn --cwd <package-dir> backstage-cli package schema:openapi:generate` to translate your `openapi.yaml` to a new Typescript file in `schema/openapi.ts`. In the case of projects that require linting + a license header, you will need to do this manually.
2. In your plugin's `src/service/createRouter.ts`,
```ts
export default {
// If your spec is in YAML, convert it to JSON, then paste it here.
// If your spec is in JSON, just paste it here.
} as const;
```
In your plugin's `service/createRouter.ts`,
```ts
import {ApiRouter} from `@backstage/plugin-openapi-router`;
import spec from './schema/openapi'
import {ApiRouter} from `@backstage/backend-openapi-utils`;
import spec from '../../schema/openapi'
...
export function createRouter(){
const router = Router() as ApiRouter<typeof spec>
const router = Router() as ApiRouter<typeof spec>;
...
}
```
### Limitations
1. OpenAPI definitions must be converted to Typescript files
From [#32063](https://github.com/microsoft/TypeScript/issues/32063), we cannot import JSON `as const`. If we could, this would allow us to force all specs to be JSON and then just import from a spec.
2. `as const` makes all fields `readonly`
To ensure a good DX of using a simple imported JSON spec, we want to remove any type issues between `readonly` arrays and mutable arrays. Typescript does not allow them to be compared, so converting all imports from the `openapi3-ts` library to `readonly` is important.
1. `as const` makes all fields `readonly`
To ensure a good DX of using a simple imported JSON spec, we want to remove any type issues between `readonly` arrays and mutable arrays. Typescript does not allow them to be compared, so converting all imports from the `openapi3-ts` library to `readonly` is important. This is achieved through the `ImmutableObject` type in `types/immutable.ts`.
```tsx
```ts
...
// We want an interface like this,
Router() as ApiRouter<typeof spec>
// Not an interface like this,
Router() as ApiRouter<DeepWriteable<typeof spec>>
...
```
we need to type all internals of this package as `Immutable<T>`.
## Future Work
### Runtime validation
Using a package like [`express-openapi-validator`](https://www.npmjs.com/package/express-openapi-validator), would allow us to remove [validation of request bodies with `AJV`](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/src/service/util.ts#L58).
### PR-time verification.
1. Verify that spec file matches the router input/output.
Using a package like [`express-openapi-validator`](https://www.npmjs.com/package/express-openapi-validator), would allow us to remove [validation of request bodies with `AJV`](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/src/service/util.ts#L58). However, `AJV` currently doesn't have support for OpenAPI 3.1 and `express-openapi-validator` enforces full URL matching for paths, meaning it cannot be mounted at the router level.
+10 -581
View File
@@ -3,599 +3,28 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import type { ContentObject } from 'openapi3-ts';
import core from 'express-serve-static-core';
import { FromSchema } from 'json-schema-to-ts';
import { JSONSchema7 } from 'json-schema-to-ts';
import type { OpenAPIObject } from 'openapi3-ts';
import type { ParameterObject } from 'openapi3-ts';
import type { ReferenceObject } from 'openapi3-ts';
import type { RequestBodyObject } from 'openapi3-ts';
import type { ResponseObject } from 'openapi3-ts';
import { Router } from 'express';
import type { SchemaObject } from 'openapi3-ts';
// @public
export interface ApiRouter<Doc extends RequiredDoc> extends Router {
export interface ApiRouter<Doc extends core.Spec> extends Router {
// (undocumented)
all: DocRequestMatcher<Doc, this, 'all'>;
all: core.RequestMatcher<Doc, this, 'all'>;
// (undocumented)
delete: DocRequestMatcher<Doc, this, 'delete'>;
delete: core.RequestMatcher<Doc, this, 'delete'>;
// (undocumented)
get: DocRequestMatcher<Doc, this, 'get'>;
get: core.RequestMatcher<Doc, this, 'get'>;
// (undocumented)
head: DocRequestMatcher<Doc, this, 'head'>;
head: core.RequestMatcher<Doc, this, 'head'>;
// (undocumented)
options: DocRequestMatcher<Doc, this, 'options'>;
options: core.RequestMatcher<Doc, this, 'options'>;
// (undocumented)
patch: DocRequestMatcher<Doc, this, 'patch'>;
patch: core.RequestMatcher<Doc, this, 'patch'>;
// (undocumented)
post: DocRequestMatcher<Doc, this, 'post'>;
post: core.RequestMatcher<Doc, this, 'post'>;
// (undocumented)
put: DocRequestMatcher<Doc, this, 'put'>;
put: core.RequestMatcher<Doc, this, 'put'>;
}
// @public (undocumented)
export type ComponentRef<
Doc extends RequiredDoc,
Type extends ComponentTypes<Doc>,
Ref extends ImmutableReferenceObject,
> = Ref extends {
$ref: `#/components/${Type}/${infer Name}`;
}
? Name extends keyof Doc['components'][Type]
? Doc['components'][Type][Name] extends ImmutableReferenceObject
? ComponentRef<Doc, Type, Doc['components'][Type][Name]>
: Doc['components'][Type][Name]
: never
: never;
// @public (undocumented)
export type ComponentTypes<Doc extends RequiredDoc> = Extract<
keyof Doc['components'],
string
>;
// @public (undocumented)
export type ConvertAll<T, R extends ReadonlyArray<unknown> = []> = T extends [
infer First extends JSONSchema7,
...infer Rest,
]
? ConvertAll<Rest, [...R, FromSchema<First>]>
: R;
// @public (undocumented)
export interface CookieObject extends ParameterObject {
// (undocumented)
in: 'cookie';
// (undocumented)
style?: 'form';
}
// @public (undocumented)
export type CookieSchema<
Doc extends RequiredDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
Method extends DocPathMethod<Doc, Path>,
> = ParametersSchema<Doc, DocPath<Doc, Path>, Method, ImmutableCookieObject>;
// @public (undocumented)
export type DiscriminateUnion<T, K extends keyof T, V extends T[K]> = Extract<
T,
Record<K, V>
>;
// @public (undocumented)
export type DocOperation<
Doc extends RequiredDoc,
Path extends keyof Doc['paths'],
Method extends keyof Doc['paths'][Path],
> = Doc['paths'][Path][Method];
// @public (undocumented)
export type DocParameter<
Doc extends RequiredDoc,
Path extends Extract<keyof Doc['paths'], string>,
Method extends keyof Doc['paths'][Path],
Parameter extends keyof Doc['paths'][Path][Method]['parameters'],
> = DocOperation<
Doc,
Path,
Method
>['parameters'][Parameter] extends ImmutableReferenceObject
? 'parameters' extends ComponentTypes<Doc>
? ComponentRef<
Doc,
'parameters',
DocOperation<Doc, Path, Method>['parameters'][Parameter]
>
: never
: DocOperation<Doc, Path, Method>['parameters'][Parameter];
// @public (undocumented)
export type DocParameters<
Doc extends RequiredDoc,
Path extends Extract<keyof Doc['paths'], string>,
Method extends keyof Doc['paths'][Path],
> = DocOperation<Doc, Path, Method>['parameters'] extends ReadonlyArray<any>
? {
[Index in keyof DocOperation<
Doc,
Path,
Method
>['parameters']]: DocParameter<Doc, Path, Method, Index>;
}
: never;
// @public
export type DocPath<
Doc extends PathDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
> = ValueOf<{
[Template in Extract<
keyof Doc['paths'],
string
>]: Path extends PathTemplate<Template> ? Template : never;
}>;
// @public (undocumented)
export type DocPathMethod<
Doc extends Pick<RequiredDoc, 'paths'>,
Path extends DocPathTemplate<Doc>,
> = keyof Doc['paths'][DocPath<Doc, Path>];
// @public (undocumented)
export type DocPathTemplate<Doc extends PathDoc> = PathTemplate<
Extract<keyof Doc['paths'], string>
>;
// @public
export type DocRequestHandler<
Doc extends RequiredDoc,
Path extends DocPathTemplate<Doc>,
Method extends keyof Doc['paths'][Path],
> = core.RequestHandler<
PathSchema<Doc, Path, Method>,
ResponseBodyToJsonSchema<Doc, Path, Method>,
RequestBodyToJsonSchema<Doc, Path, Method>,
QuerySchema<Doc, Path, Method>,
Record<string, string>
>;
// @public
export type DocRequestHandlerParams<
Doc extends RequiredDoc,
Path extends DocPathTemplate<Doc>,
Method extends keyof Doc['paths'][Path],
> = core.RequestHandlerParams<
PathSchema<Doc, Path, Method>,
ResponseBodyToJsonSchema<Doc, Path, Method>,
RequestBodyToJsonSchema<Doc, Path, Method>,
QuerySchema<Doc, Path, Method>,
Record<string, string>
>;
// @public
export interface DocRequestMatcher<
Doc extends RequiredDoc,
T,
Method extends
| 'all'
| 'get'
| 'post'
| 'put'
| 'delete'
| 'patch'
| 'options'
| 'head',
> {
// (undocumented)
<Path extends MethodAwareDocPath<Doc, DocPathTemplate<Doc>, Method>>(
path: Path,
...handlers: Array<DocRequestHandler<Doc, Path, Method>>
): T;
// (undocumented)
<Path extends MethodAwareDocPath<Doc, DocPathTemplate<Doc>, Method>>(
path: Path,
...handlers: Array<DocRequestHandlerParams<Doc, Path, Method>>
): T;
}
// @public (undocumented)
export type Filter<T, U> = T extends U ? T : never;
// @public (undocumented)
export type FullMap<
T extends {
[key: string]: any;
},
> = RequiredMap<T> & OptionalMap<T>;
// @public (undocumented)
export interface HeaderObject extends ParameterObject {
// (undocumented)
in: 'header';
// (undocumented)
style: 'simple';
}
// @public (undocumented)
export type HeaderSchema<
Doc extends RequiredDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
Method extends DocPathMethod<Doc, Path>,
> = ParametersSchema<Doc, DocPath<Doc, Path>, Method, ImmutableHeaderObject>;
// @public
export type Immutable<T> = T extends
| Function
| boolean
| number
| string
| null
| undefined
? T
: T extends Map<infer K, infer V>
? ReadonlyMap<Immutable<K>, Immutable<V>>
: T extends Set<infer S>
? ReadonlySet<Immutable<S>>
: {
readonly [P in keyof T]: Immutable<T[P]>;
};
// @public (undocumented)
export type ImmutableContentObject = ImmutableObject<ContentObject>;
// @public (undocumented)
export type ImmutableCookieObject = ImmutableObject<CookieObject>;
// @public (undocumented)
export type ImmutableHeaderObject = ImmutableObject<HeaderObject>;
// @public (undocumented)
export type ImmutableObject<T> = {
readonly [K in keyof T]: Immutable<T[K]>;
};
// @public (undocumented)
export type ImmutableOpenAPIObject = ImmutableObject<OpenAPIObject>;
// @public (undocumented)
export type ImmutableParameterObject = ImmutableObject<ParameterObject>;
// @public (undocumented)
export type ImmutablePathObject = ImmutableObject<PathObject>;
// @public (undocumented)
export type ImmutableQueryObject = ImmutableObject<QueryObject>;
// @public (undocumented)
export type ImmutableReferenceObject = ImmutableObject<ReferenceObject>;
// @public (undocumented)
export type ImmutableRequestBodyObject = ImmutableObject<RequestBodyObject>;
// @public (undocumented)
export type ImmutableResponseObject = ImmutableObject<ResponseObject>;
// @public (undocumented)
export type ImmutableSchemaObject = ImmutableObject<SchemaObject>;
// @public (undocumented)
export type LastOf<T> = UnionToIntersection<
T extends any ? () => T : never
> extends () => infer R
? R
: never;
// @public (undocumented)
export type MapDiscriminatedUnion<
T extends Record<K, string>,
K extends keyof T,
> = {
[V in T[K]]: DiscriminateUnion<T, K, V>;
};
// @public (undocumented)
export type MapToSchema<
Doc extends RequiredDoc,
T extends Record<string, ImmutableParameterObject>,
> = {
[V in keyof T]: NonNullable<T[V]> extends ImmutableParameterObject
? ParameterSchema<Doc, NonNullable<T[V]>['schema']>
: never;
};
// @public (undocumented)
export type MethodAwareDocPath<
Doc extends PathDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
Method extends keyof Doc['paths'][Path],
> = ValueOf<{
[Template in Extract<
keyof Doc['paths'],
string
>]: Path extends PathTemplate<Template>
? Method extends DocPathMethod<Doc, Path>
? PathTemplate<Template>
: never
: never;
}>;
// @public (undocumented)
export type ObjectWithContentSchema<
Doc extends RequiredDoc,
Object extends {
content?: ImmutableContentObject;
},
> = Object['content'] extends ImmutableContentObject
? SchemaRef<Doc, Object['content']['application/json']['schema']>
: never;
// @public (undocumented)
export type OptionalMap<
T extends {
[key: string]: any;
},
> = {
[P in Exclude<PickOptionalKeys<T>, undefined>]?: NonNullable<T[P]>;
};
// @public (undocumented)
export type ParameterSchema<
Doc extends RequiredDoc,
Schema extends ImmutableParameterObject['schema'],
> = ResolveDocParameterSchema<Doc, Schema> extends infer R
? R extends ImmutableSchemaObject
? R extends JSONSchema7
? FromSchema<R>
: never
: never
: never;
// @public (undocumented)
export type ParametersSchema<
Doc extends RequiredDoc,
Path extends Extract<keyof Doc['paths'], string>,
Method extends keyof Doc['paths'][Path],
FilterType extends ImmutableParameterObject,
> = number extends keyof DocParameters<Doc, Path, Method>
? MapToSchema<
Doc,
FullMap<
MapDiscriminatedUnion<
Filter<DocParameters<Doc, Path, Method>[number], FilterType>,
'name'
>
>
>
: never;
// @public
export interface ParsedQs {
// (undocumented)
[key: string]: undefined | string | string[] | ParsedQs | ParsedQs[];
}
// @public (undocumented)
export type PathDoc = Pick<ImmutableOpenAPIObject, 'paths'>;
// @public (undocumented)
export interface PathObject extends ParameterObject {
// (undocumented)
in: 'path';
// (undocumented)
style?: 'simple' | 'label' | 'matrix';
}
// @public (undocumented)
export type PathSchema<
Doc extends RequiredDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
Method extends DocPathMethod<Doc, Path>,
> = ParametersSchema<Doc, DocPath<Doc, Path>, Method, ImmutablePathObject>;
// @public
export type PathTemplate<Path extends string> =
Path extends `${infer Prefix}{${infer PathName}}${infer Suffix}`
? `${Prefix}:${PathName}${PathTemplate<Suffix>}`
: Path;
// @public (undocumented)
export type PickOptionalKeys<
T extends {
[key: string]: any;
},
> = {
[K in keyof T]: true extends T[K]['required'] ? never : K;
}[keyof T];
// @public (undocumented)
export type PickRequiredKeys<
T extends {
[key: string]: any;
},
> = {
[K in keyof T]: true extends T[K]['required'] ? K : never;
}[keyof T];
// @public (undocumented)
export type Push<T extends any[], V> = [...T, V];
// @public (undocumented)
export interface QueryObject extends ParameterObject {
// (undocumented)
in: 'query';
// (undocumented)
style?: 'form' | 'deepObject' | 'pipeDelimited' | 'spaceDelimited';
}
// @public (undocumented)
export type QuerySchema<
Doc extends RequiredDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
Method extends DocPathMethod<Doc, Path>,
> = ParametersSchema<Doc, DocPath<Doc, Path>, Method, ImmutableQueryObject>;
// @public (undocumented)
export type RequestBody<
Doc extends RequiredDoc,
Path extends Extract<keyof Doc['paths'], string>,
Method extends keyof Doc['paths'][Path],
> = DocOperation<
Doc,
Path,
Method
>['requestBody'] extends ImmutableReferenceObject
? 'requestBodies' extends ComponentTypes<Doc>
? ComponentRef<
Doc,
'requestBodies',
DocOperation<Doc, Path, Method>['requestBody']
>
: never
: DocOperation<Doc, Path, Method>['requestBody'];
// @public (undocumented)
export type RequestBodySchema<
Doc extends RequiredDoc,
Path extends DocPathTemplate<Doc>,
Method extends DocPathMethod<Doc, Path>,
> = RequestBody<
Doc,
DocPath<Doc, Path>,
Method
> extends ImmutableRequestBodyObject
? ObjectWithContentSchema<Doc, RequestBody<Doc, DocPath<Doc, Path>, Method>>
: never;
// @public
export type RequestBodyToJsonSchema<
Doc extends RequiredDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
Method extends DocPathMethod<Doc, Path>,
> = ToTypeSafe<RequestBodySchema<Doc, Path, Method>>;
// @public
export type RequiredDoc = Pick<ImmutableOpenAPIObject, 'paths' | 'components'>;
// @public (undocumented)
export type RequiredMap<
T extends {
[key: string]: any;
},
> = {
[P in Exclude<PickRequiredKeys<T>, undefined>]: NonNullable<T[P]>;
};
// @public (undocumented)
export type ResolveDocParameterSchema<
Doc extends RequiredDoc,
Schema extends ImmutableParameterObject['schema'],
> = Schema extends ImmutableReferenceObject
? 'parameters' extends ComponentTypes<Doc>
? ComponentRef<Doc, 'parameters', Schema>
: never
: Schema;
// @public (undocumented)
type Response_2<
Doc extends RequiredDoc,
Path extends keyof Doc['paths'],
Method extends keyof Doc['paths'][Path],
StatusCode extends keyof Doc['paths'][Path]['responses'],
> = DocOperation<
Doc,
Path,
Method
>['responses'][StatusCode] extends ImmutableReferenceObject
? 'responses' extends ComponentTypes<Doc>
? ComponentRef<
Doc,
'responses',
DocOperation<Doc, Path, Method>['responses'][StatusCode]
>
: never
: DocOperation<Doc, Path, Method>['responses'][StatusCode];
export { Response_2 as Response };
// @public
export type ResponseBodyToJsonSchema<
Doc extends RequiredDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
Method extends DocPathMethod<Doc, Path>,
> = ToTypeSafe<ValueOf<ResponseSchemas<Doc, Path, Method>>>;
// @public (undocumented)
export type Responses<
Doc extends RequiredDoc,
Path extends keyof Doc['paths'],
Method extends keyof Doc['paths'][Path],
> = {
[StatusCode in keyof DocOperation<
Doc,
Path,
Method
>['responses']]: Response_2<Doc, Path, Method, StatusCode>;
};
// @public (undocumented)
export type ResponseSchema<
Doc extends RequiredDoc,
Object extends ImmutableResponseObject,
> = ObjectWithContentSchema<Doc, Object>;
// @public (undocumented)
export type ResponseSchemas<
Doc extends RequiredDoc,
Path extends DocPathTemplate<Doc>,
Method extends DocPathMethod<Doc, Path>,
> = {
[StatusCode in keyof Responses<Doc, DocPath<Doc, Path>, Method>]: Responses<
Doc,
DocPath<Doc, Path>,
Method
>[StatusCode] extends ImmutableResponseObject
? ResponseSchema<
Doc,
Responses<Doc, DocPath<Doc, Path>, Method>[StatusCode]
>
: never;
};
// @public (undocumented)
export type SchemaRef<Doc extends RequiredDoc, Schema> = Schema extends {
$ref: `#/components/schemas/${infer Name}`;
}
? 'schemas' extends keyof Doc['components']
? Name extends keyof Doc['components']['schemas']
? SchemaRef<Doc, Doc['components']['schemas'][Name]>
: never
: never
: {
[Key in keyof Schema]: SchemaRef<Doc, Schema[Key]>;
};
// @public (undocumented)
export type ToTypeSafe<T> = UnknownIfNever<ConvertAll<TuplifyUnion<T>>[number]>;
// @public (undocumented)
export type TuplifyUnion<
T,
L = LastOf<T>,
N = [T] extends [never] ? true : false,
> = true extends N ? [] : Push<TuplifyUnion<Exclude<T, L>>, L>;
// @public
export type UnionToIntersection<U> = (
U extends any ? (k: U) => void : never
) extends (k: infer I) => void
? I
: never;
// @public (undocumented)
export type UnknownIfNever<P> = [P] extends [never] ? unknown : P;
// @public
export type ValueOf<T> = T[keyof T];
export namespace core {}
```
+1 -1
View File
@@ -31,7 +31,7 @@
],
"dependencies": {
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.33",
"@types/express-serve-static-core": "^4.17.5",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"json-schema-to-ts": "^2.6.2",
-50
View File
@@ -1,50 +0,0 @@
/*
* 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 from 'express';
import Router from 'express-promise-router';
import { ApiRouter } from './router';
import doc from './schema/petstore';
interface RouterOptions {}
export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
console.log(options);
const router = Router() as ApiRouter<typeof doc>;
router.get('/pets/:petId', (_, res) => {
res.json({
id: 1,
name: 'test',
});
});
router.get('/pets', (_, res) => {
res.json([
{
id: 1,
tag: '123',
name: 'test',
},
]);
});
router.post('/pets', (_, res) => {
res.send();
});
return router;
}
+10 -10
View File
@@ -14,26 +14,26 @@
* limitations under the License.
*/
import { Router } from 'express';
import { RequiredDoc, DocRequestMatcher } from './types';
import { core } from './types';
/**
* Typed Express router based on an OpenAPI 3.1 spec.
* @public
*/
export interface ApiRouter<Doc extends RequiredDoc> extends Router {
get: DocRequestMatcher<Doc, this, 'get'>;
export interface ApiRouter<Doc extends core.Spec> extends Router {
get: core.RequestMatcher<Doc, this, 'get'>;
post: DocRequestMatcher<Doc, this, 'post'>;
post: core.RequestMatcher<Doc, this, 'post'>;
all: DocRequestMatcher<Doc, this, 'all'>;
all: core.RequestMatcher<Doc, this, 'all'>;
put: DocRequestMatcher<Doc, this, 'put'>;
put: core.RequestMatcher<Doc, this, 'put'>;
delete: DocRequestMatcher<Doc, this, 'delete'>;
delete: core.RequestMatcher<Doc, this, 'delete'>;
patch: DocRequestMatcher<Doc, this, 'patch'>;
patch: core.RequestMatcher<Doc, this, 'patch'>;
options: DocRequestMatcher<Doc, this, 'options'>;
options: core.RequestMatcher<Doc, this, 'options'>;
head: DocRequestMatcher<Doc, this, 'head'>;
head: core.RequestMatcher<Doc, this, 'head'>;
}
@@ -1,183 +0,0 @@
/*
* 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',
},
},
additionalProperties: false,
},
Pets: {
type: 'array',
items: {
$ref: '#/components/schemas/Pet',
},
},
Error: {
type: 'object',
required: ['code', 'message'],
properties: {
code: {
type: 'integer',
format: 'int32',
},
message: {
type: 'string',
},
},
additionalProperties: false,
},
},
},
} as const;
+3 -83
View File
@@ -27,20 +27,13 @@ import {
/**
* Basic OpenAPI spec with paths and components properties enforced.
*
* @public
*/
export type RequiredDoc = Pick<ImmutableOpenAPIObject, 'paths' | 'components'>;
/**
* @public
*/
export type PathDoc = Pick<ImmutableOpenAPIObject, 'paths'>;
/**
* Get value types of `T`
*
* @public
* Get value types of `T`.
*/
export type ValueOf<T> = T[keyof T];
@@ -49,13 +42,9 @@ export type ValueOf<T> = T[keyof T];
*
* @example
* ```ts
* const path = PathTemplate<"/posts/{postId}/comments/{commentId}"> = "/posts/1/comments/2"const pathWithParams: PathTemplate<"/posts/{postId}/comments/{commentId}"> = "/posts/1/comments/2";
* const path: PathTemplate<"/posts/{postId}/comments/{commentId}"> = "/posts/:postId/comments/:commentId";
* const pathWithoutParams: PathTemplate<"/posts/comments"> = "/posts/comments";
* ```
*
*
*
* @public
*/
export type PathTemplate<Path extends string> =
Path extends `${infer Prefix}{${infer PathName}}${infer Suffix}`
@@ -72,11 +61,9 @@ export type PathTemplate<Path extends string> =
* "/posts/comments": {},
* }
* };
* const specPathWithParams: DocPath<typeof spec, "/posts/1/comments/2"> = "/posts/{postId}/comments/{commentId}";
* const specPathWithParams: DocPath<typeof spec, "/posts/:postId/comments/:commentId"> = "/posts/{postId}/comments/{commentId}";
* const specPathWithoutParams: DocPath<typeof spec, "/posts/comments"> = "/posts/comments";
* ```
*
* @public
*/
export type DocPath<
Doc extends PathDoc,
@@ -88,24 +75,15 @@ export type DocPath<
>]: Path extends PathTemplate<Template> ? Template : never;
}>;
/**
* @public
*/
export type DocPathTemplate<Doc extends PathDoc> = PathTemplate<
Extract<keyof Doc['paths'], string>
>;
/**
* @public
*/
export type DocPathMethod<
Doc extends Pick<RequiredDoc, 'paths'>,
Path extends DocPathTemplate<Doc>,
> = keyof Doc['paths'][DocPath<Doc, Path>];
/**
* @public
*/
export type MethodAwareDocPath<
Doc extends PathDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
@@ -121,26 +99,17 @@ export type MethodAwareDocPath<
: never;
}>;
/**
* @public
*/
export type DocOperation<
Doc extends RequiredDoc,
Path extends keyof Doc['paths'],
Method extends keyof Doc['paths'][Path],
> = Doc['paths'][Path][Method];
/**
* @public
*/
export type ComponentTypes<Doc extends RequiredDoc> = Extract<
keyof Doc['components'],
string
>;
/**
* @public
*/
export type ComponentRef<
Doc extends RequiredDoc,
Type extends ComponentTypes<Doc>,
@@ -153,9 +122,6 @@ export type ComponentRef<
: never
: never;
/**
* @public
*/
export type SchemaRef<Doc extends RequiredDoc, Schema> = Schema extends {
$ref: `#/components/schemas/${infer Name}`;
}
@@ -166,9 +132,6 @@ export type SchemaRef<Doc extends RequiredDoc, Schema> = Schema extends {
: never
: { [Key in keyof Schema]: SchemaRef<Doc, Schema[Key]> };
/**
* @public
*/
export type ObjectWithContentSchema<
Doc extends RequiredDoc,
Object extends { content?: ImmutableContentObject },
@@ -180,7 +143,6 @@ export type ObjectWithContentSchema<
* From {@link 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.
* @public
*/
export type UnionToIntersection<U> = (
U extends any ? (k: U) => void : never
@@ -188,32 +150,20 @@ export type UnionToIntersection<U> = (
? I
: never;
/**
* @public
*/
export type LastOf<T> = UnionToIntersection<
T extends any ? () => T : never
> extends () => infer R
? R
: never;
/**
* @public
*/
export type Push<T extends any[], V> = [...T, V];
/**
* @public
*/
export type TuplifyUnion<
T,
L = LastOf<T>,
N = [T] extends [never] ? true : false,
> = true extends N ? [] : Push<TuplifyUnion<Exclude<T, L>>, L>;
/**
* @public
*/
export type ConvertAll<T, R extends ReadonlyArray<unknown> = []> = T extends [
infer First extends JSONSchema7,
...infer Rest,
@@ -221,27 +171,15 @@ export type ConvertAll<T, R extends ReadonlyArray<unknown> = []> = T extends [
? ConvertAll<Rest, [...R, FromSchema<First>]>
: R;
/**
* @public
*/
export type UnknownIfNever<P> = [P] extends [never] ? unknown : P;
/**
* @public
*/
export type ToTypeSafe<T> = UnknownIfNever<ConvertAll<TuplifyUnion<T>>[number]>;
/**
* @public
*/
export type DiscriminateUnion<T, K extends keyof T, V extends T[K]> = Extract<
T,
Record<K, V>
>;
/**
* @public
*/
export type MapDiscriminatedUnion<
T extends Record<K, string>,
K extends keyof T,
@@ -249,41 +187,23 @@ export type MapDiscriminatedUnion<
[V in T[K]]: DiscriminateUnion<T, K, V>;
};
/**
* @public
*/
export type PickOptionalKeys<T extends { [key: string]: any }> = {
[K in keyof T]: true extends T[K]['required'] ? never : K;
}[keyof T];
/**
* @public
*/
export type PickRequiredKeys<T extends { [key: string]: any }> = {
[K in keyof T]: true extends T[K]['required'] ? K : never;
}[keyof T];
/**
* @public
*/
export type OptionalMap<T extends { [key: string]: any }> = {
[P in Exclude<PickOptionalKeys<T>, undefined>]?: NonNullable<T[P]>;
};
/**
* @public
*/
export type RequiredMap<T extends { [key: string]: any }> = {
[P in Exclude<PickRequiredKeys<T>, undefined>]: NonNullable<T[P]>;
};
/**
* @public
*/
export type FullMap<T extends { [key: string]: any }> = RequiredMap<T> &
OptionalMap<T>;
/**
* @public
*/
export type Filter<T, U> = T extends U ? T : never;
+2 -13
View File
@@ -19,19 +19,10 @@ import { PathSchema, QuerySchema } from './params';
import { RequestBodyToJsonSchema } from './requests';
import { ResponseBodyToJsonSchema } from './responses';
/**
* Pulled from the express library.
* @public
*/
export interface ParsedQs {
[key: string]: undefined | string | string[] | ParsedQs | ParsedQs[];
}
/**
* Typed express request handler.
* @public
*/
export type DocRequestHandler<
type DocRequestHandler<
Doc extends RequiredDoc,
Path extends DocPathTemplate<Doc>,
Method extends keyof Doc['paths'][Path],
@@ -45,9 +36,8 @@ export type DocRequestHandler<
/**
* Typed express error handler / request handler union type.
* @public
*/
export type DocRequestHandlerParams<
type DocRequestHandlerParams<
Doc extends RequiredDoc,
Path extends DocPathTemplate<Doc>,
Method extends keyof Doc['paths'][Path],
@@ -61,7 +51,6 @@ export type DocRequestHandlerParams<
/**
* Superset of the express router path matcher that enforces typed request and response bodies.
* @public
*/
export interface DocRequestMatcher<
Doc extends RequiredDoc,
+4 -53
View File
@@ -31,7 +31,6 @@ import type {
/**
* From {@link https://github.com/microsoft/TypeScript/issues/13923#issuecomment-653675557}, allows
* us to convert from `as const` to the various OpenAPI types documented in `openapi3-ts`.
* @public
*/
export type Immutable<T> = T extends
| Function
@@ -47,94 +46,46 @@ export type Immutable<T> = T extends
? ReadonlySet<Immutable<S>>
: { readonly [P in keyof T]: Immutable<T[P]> };
/**
* @public
*/
export type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]> };
/**
* @public
*/
export type ImmutableReferenceObject = ImmutableObject<ReferenceObject>;
/**
* @public
*/
export type ImmutableOpenAPIObject = ImmutableObject<OpenAPIObject>;
/**
* @public
*/
export type ImmutableContentObject = ImmutableObject<ContentObject>;
/**
* @public
*/
export type ImmutableRequestBodyObject = ImmutableObject<RequestBodyObject>;
/**
* @public
*/
export type ImmutableResponseObject = ImmutableObject<ResponseObject>;
/**
* @public
*/
export type ImmutableParameterObject = ImmutableObject<ParameterObject>;
/**
* @public
*/
export interface HeaderObject extends ParameterObject {
interface HeaderObject extends ParameterObject {
in: 'header';
style: 'simple';
}
/**
* @public
*/
export type ImmutableHeaderObject = ImmutableObject<HeaderObject>;
/**
* @public
*/
export interface CookieObject extends ParameterObject {
interface CookieObject extends ParameterObject {
in: 'cookie';
style?: 'form';
}
/**
* @public
*/
export type ImmutableCookieObject = ImmutableObject<CookieObject>;
/**
* @public
*/
export interface QueryObject extends ParameterObject {
interface QueryObject extends ParameterObject {
in: 'query';
style?: 'form' | 'deepObject' | 'pipeDelimited' | 'spaceDelimited';
}
/**
* @public
*/
export type ImmutableQueryObject = ImmutableObject<QueryObject>;
/**
* @public
*/
export interface PathObject extends ParameterObject {
interface PathObject extends ParameterObject {
in: 'path';
style?: 'simple' | 'label' | 'matrix';
}
/**
* @public
*/
export type ImmutablePathObject = ImmutableObject<PathObject>;
/**
* @public
*/
export type ImmutableSchemaObject = ImmutableObject<SchemaObject>;
+30 -6
View File
@@ -13,9 +13,33 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './common';
export * from './express';
export * from './requests';
export * from './responses';
export * from './immutable';
export * from './params';
import type { RequiredDoc } from './common';
import type { DocRequestMatcher } from './express';
/**
* @public
*/
export namespace core {
/**
* @internal
*/
export type Spec = RequiredDoc;
/**
* @internal
*/
export type RequestMatcher<
Doc extends RequiredDoc,
T,
Method extends
| 'all'
| 'get'
| 'post'
| 'put'
| 'delete'
| 'patch'
| 'options'
| 'head',
> = DocRequestMatcher<Doc, T, Method>;
}
+6 -36
View File
@@ -37,10 +37,7 @@ import {
} from './common';
import { FromSchema, JSONSchema7 } from 'json-schema-to-ts';
/**
* @public
*/
export type DocParameter<
type DocParameter<
Doc extends RequiredDoc,
Path extends Extract<keyof Doc['paths'], string>,
Method extends keyof Doc['paths'][Path],
@@ -59,10 +56,7 @@ export type DocParameter<
: never
: DocOperation<Doc, Path, Method>['parameters'][Parameter];
/**
* @public
*/
export type DocParameters<
type DocParameters<
Doc extends RequiredDoc,
Path extends Extract<keyof Doc['paths'], string>,
Method extends keyof Doc['paths'][Path],
@@ -76,10 +70,7 @@ export type DocParameters<
}
: never;
/**
* @public
*/
export type ResolveDocParameterSchema<
type ResolveDocParameterSchema<
Doc extends RequiredDoc,
Schema extends ImmutableParameterObject['schema'],
> = Schema extends ImmutableReferenceObject
@@ -88,10 +79,7 @@ export type ResolveDocParameterSchema<
: never
: Schema;
/**
* @public
*/
export type ParameterSchema<
type ParameterSchema<
Doc extends RequiredDoc,
Schema extends ImmutableParameterObject['schema'],
> = ResolveDocParameterSchema<Doc, Schema> extends infer R
@@ -102,10 +90,7 @@ export type ParameterSchema<
: never
: never;
/**
* @public
*/
export type MapToSchema<
type MapToSchema<
Doc extends RequiredDoc,
T extends Record<string, ImmutableParameterObject>,
> = {
@@ -114,10 +99,7 @@ export type MapToSchema<
: never;
};
/**
* @public
*/
export type ParametersSchema<
type ParametersSchema<
Doc extends RequiredDoc,
Path extends Extract<keyof Doc['paths'], string>,
Method extends keyof Doc['paths'][Path],
@@ -134,36 +116,24 @@ export type ParametersSchema<
>
: never;
/**
* @public
*/
export type HeaderSchema<
Doc extends RequiredDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
Method extends DocPathMethod<Doc, Path>,
> = ParametersSchema<Doc, DocPath<Doc, Path>, Method, ImmutableHeaderObject>;
/**
* @public
*/
export type CookieSchema<
Doc extends RequiredDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
Method extends DocPathMethod<Doc, Path>,
> = ParametersSchema<Doc, DocPath<Doc, Path>, Method, ImmutableCookieObject>;
/**
* @public
*/
export type PathSchema<
Doc extends RequiredDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
Method extends DocPathMethod<Doc, Path>,
> = ParametersSchema<Doc, DocPath<Doc, Path>, Method, ImmutablePathObject>;
/**
* @public
*/
export type QuerySchema<
Doc extends RequiredDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
+2 -9
View File
@@ -35,10 +35,7 @@ import {
ImmutableRequestBodyObject,
} from './immutable';
/**
* @public
*/
export type RequestBody<
type RequestBody<
Doc extends RequiredDoc,
Path extends Extract<keyof Doc['paths'], string>,
Method extends keyof Doc['paths'][Path],
@@ -56,10 +53,7 @@ export type RequestBody<
: never
: DocOperation<Doc, Path, Method>['requestBody'];
/**
* @public
*/
export type RequestBodySchema<
type RequestBodySchema<
Doc extends RequiredDoc,
Path extends DocPathTemplate<Doc>,
Method extends DocPathMethod<Doc, Path>,
@@ -73,7 +67,6 @@ export type RequestBodySchema<
/**
* Transform the OpenAPI request body schema to a typesafe JSON schema.
* @public
*/
export type RequestBodyToJsonSchema<
Doc extends RequiredDoc,
+4 -17
View File
@@ -33,10 +33,7 @@ import type {
} from './common';
import { ImmutableReferenceObject, ImmutableResponseObject } from './immutable';
/**
* @public
*/
export type Response<
type Response<
Doc extends RequiredDoc,
Path extends keyof Doc['paths'],
Method extends keyof Doc['paths'][Path],
@@ -55,10 +52,7 @@ export type Response<
: never
: DocOperation<Doc, Path, Method>['responses'][StatusCode];
/**
* @public
*/
export type Responses<
type Responses<
Doc extends RequiredDoc,
Path extends keyof Doc['paths'],
Method extends keyof Doc['paths'][Path],
@@ -71,18 +65,12 @@ export type Responses<
>;
};
/**
* @public
*/
export type ResponseSchema<
type ResponseSchema<
Doc extends RequiredDoc,
Object extends ImmutableResponseObject,
> = ObjectWithContentSchema<Doc, Object>;
/**
* @public
*/
export type ResponseSchemas<
type ResponseSchemas<
Doc extends RequiredDoc,
Path extends DocPathTemplate<Doc>,
Method extends DocPathMethod<Doc, Path>,
@@ -101,7 +89,6 @@ export type ResponseSchemas<
/**
* Transform the OpenAPI request body schema to a typesafe JSON schema.
* @public
*/
export type ResponseBodyToJsonSchema<
Doc extends RequiredDoc,
+1 -45
View File
@@ -3833,7 +3833,7 @@ __metadata:
dependencies:
"@backstage/cli": "workspace:^"
"@types/express": ^4.17.6
"@types/express-serve-static-core": ^4.17.33
"@types/express-serve-static-core": ^4.17.5
express: ^4.17.1
express-promise-router: ^4.1.0
json-schema-to-ts: ^2.6.2
@@ -15804,17 +15804,6 @@ __metadata:
languageName: node
linkType: hard
"@types/express-serve-static-core@npm:*, @types/express-serve-static-core@npm:^4.17.18, @types/express-serve-static-core@npm:^4.17.31, @types/express-serve-static-core@npm:^4.17.33, @types/express-serve-static-core@npm:^4.17.5":
version: 4.17.33
resolution: "@types/express-serve-static-core@npm:4.17.33"
dependencies:
"@types/node": "*"
"@types/qs": "*"
"@types/range-parser": "*"
checksum: dce580d16b85f207445af9d4053d66942b27d0c72e86153089fa00feee3e96ae336b7bedb31ed4eea9e553c99d6dd356ed6e0928f135375d9f862a1a8015adf2
languageName: node
linkType: hard
"@types/express-serve-static-core@npm:*, @types/express-serve-static-core@npm:^4.17.30, @types/express-serve-static-core@npm:^4.17.31, @types/express-serve-static-core@npm:^4.17.5":
version: 4.17.33
resolution: "@types/express-serve-static-core@npm:4.17.33"
@@ -15868,18 +15857,6 @@ __metadata:
languageName: node
linkType: hard
"@types/express@npm:4.17.14":
version: 4.17.14
resolution: "@types/express@npm:4.17.14"
dependencies:
"@types/body-parser": "*"
"@types/express-serve-static-core": ^4.17.18
"@types/qs": "*"
"@types/serve-static": "*"
checksum: 15c1af46d02de834e4a225eccaa9d85c0370fdbb3ed4e1bc2d323d24872309961542b993ae236335aeb3e278630224a6ea002078d39e651d78a3b0356b1eaa79
languageName: node
linkType: hard
"@types/fs-extra@npm:^9.0.1, @types/fs-extra@npm:^9.0.3, @types/fs-extra@npm:^9.0.5, @types/fs-extra@npm:^9.0.6":
version: 9.0.13
resolution: "@types/fs-extra@npm:9.0.13"
@@ -20732,13 +20709,6 @@ __metadata:
languageName: node
linkType: hard
"content-type@npm:~1.0.4":
version: 1.0.4
resolution: "content-type@npm:1.0.4"
checksum: 3d93585fda985d1554eca5ebd251994327608d2e200978fdbfba21c0c679914d5faf266d17027de44b34a72c7b0745b18584ecccaa7e1fdfb6a68ac7114f12e0
languageName: node
linkType: hard
"content-type@npm:~1.0.4, content-type@npm:~1.0.5":
version: 1.0.5
resolution: "content-type@npm:1.0.5"
@@ -38831,20 +38801,6 @@ __metadata:
languageName: node
linkType: hard
"ts-log@npm:^2.1.4":
version: 2.2.5
resolution: "ts-log@npm:2.2.5"
checksum: 28f78ab15b8555d56c089dbc243327d8ce4331219956242a29fc4cb3bad6bb0cb8234dd17a292381a1b1dba99a7e4849a2181b2e1a303e8247e9f4ca4e284f2d
languageName: node
linkType: hard
"ts-log@npm:^2.1.4, ts-log@npm:^2.2.3":
version: 2.2.5
resolution: "ts-log@npm:2.2.5"
checksum: 28f78ab15b8555d56c089dbc243327d8ce4331219956242a29fc4cb3bad6bb0cb8234dd17a292381a1b1dba99a7e4849a2181b2e1a303e8247e9f4ca4e284f2d
languageName: node
linkType: hard
"ts-log@npm:^2.2.3":
version: 2.2.3
resolution: "ts-log@npm:2.2.3"