Merge pull request #15667 from sennyeya/express-openapi-test

[PRFC] OpenAPI Typings for Routers
This commit is contained in:
Fredrik Adelöw
2023-04-06 15:03:15 +02:00
committed by GitHub
32 changed files with 3717 additions and 149 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Updates and moves OpenAPI spec to `src/schema/openapi.yaml` and uses `ApiRouter` type from `@backstage/backend-openapi-utils` to handle automatic types from the OpenAPI spec file.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/repo-tools': minor
---
Adding two new commands to support OpenAPI spec writing, `schema:openapi:generate` to generate the Typescript file that `@backstage/backend-openapi-utils` needs for typing and `schema:openapi:verify` to verify that this file exists and matches your `src/schema/openapi.yaml` file.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-openapi-utils': patch
---
New plugin! Primary focus is to support types on `Router`s in backend packages. Developers can use the `ApiRouter` from this package in their packages to support a typed experience based on their OpenAPI specs. The `ApiRouter` supports request bodies, response bodies, query parameters and path parameters, as well as full path-based context of the above. This means no more guessing on an endpoint like `req.post('/not-my-route', (req, res)=>{res.send(req.body.badparam)})`. Typescript would catch `/not-my-route`, `req.body.badparam`, `res.send(req.body.badparam)`.
+3
View File
@@ -92,6 +92,9 @@ jobs:
- name: verify api reference
run: node scripts/verify-api-reference.js
- name: verify openapi yaml file matches generated ts file
run: yarn backstage-repo-tools schema:openapi:verify
- name: verify doc links
run: node scripts/verify-links.js
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+45
View File
@@ -0,0 +1,45 @@
# @backstage/openapi-utils
## Summary
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
1. Run `yarn --cwd <package-dir> backstage-cli package schema:openapi:generate` to translate your `src/schema/openapi.yaml` to a new Typescript file in `src/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
import {ApiRouter} from `@backstage/backend-openapi-utils`;
import spec from '../schema/openapi'
...
export function createRouter(){
const router = Router() as ApiRouter<typeof spec>;
...
}
```
### Limitations
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`.
```ts
...
// We want an interface like this,
Router() as ApiRouter<typeof spec>
// Not an interface like this,
Router() as ApiRouter<DeepWriteable<typeof spec>>
...
```
## 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). 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.
@@ -0,0 +1,644 @@
## API Report File for "@backstage/backend-openapi-utils"
> 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 type 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 {
// (undocumented)
all: DocRequestMatcher<Doc, this, 'all'>;
// (undocumented)
delete: DocRequestMatcher<Doc, this, 'delete'>;
// (undocumented)
get: DocRequestMatcher<Doc, this, 'get'>;
// (undocumented)
head: DocRequestMatcher<Doc, this, 'head'>;
// (undocumented)
options: DocRequestMatcher<Doc, this, 'options'>;
// (undocumented)
patch: DocRequestMatcher<Doc, this, 'patch'>;
// (undocumented)
post: DocRequestMatcher<Doc, this, 'post'>;
// (undocumented)
put: DocRequestMatcher<Doc, this, 'put'>;
}
// @public (undocumented)
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)
type ComponentTypes<Doc extends RequiredDoc> = Extract<
keyof Doc['components'],
string
>;
// @public (undocumented)
type ConvertAll<T, R extends ReadonlyArray<unknown> = []> = T extends [
infer First extends JSONSchema7,
...infer Rest,
]
? ConvertAll<Rest, [...R, FromSchema<First>]>
: R;
// @public (undocumented)
interface CookieObject extends ParameterObject {
// (undocumented)
in: 'cookie';
// (undocumented)
style?: 'form';
}
// @public (undocumented)
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)
type DiscriminateUnion<T, K extends keyof T, V extends T[K]> = Extract<
T,
Record<K, V>
>;
// @public (undocumented)
type DocOperation<
Doc extends RequiredDoc,
Path extends keyof Doc['paths'],
Method extends keyof Doc['paths'][Path],
> = Doc['paths'][Path][Method];
// @public (undocumented)
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)
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
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)
type DocPathMethod<
Doc extends Pick<RequiredDoc, 'paths'>,
Path extends DocPathTemplate<Doc>,
> = keyof Doc['paths'][DocPath<Doc, Path>];
// @public (undocumented)
type DocPathTemplate<Doc extends PathDoc> = PathTemplate<
Extract<keyof Doc['paths'], string>
>;
// @public
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
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
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)
type Filter<T, U> = T extends U ? T : never;
// @public (undocumented)
type FullMap<
T extends {
[key: string]: any;
},
> = RequiredMap<T> & OptionalMap<T>;
// @public (undocumented)
interface HeaderObject extends ParameterObject {
// (undocumented)
in: 'header';
// (undocumented)
style: 'simple';
}
// @public (undocumented)
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
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)
type ImmutableContentObject = ImmutableObject<ContentObject>;
// @public (undocumented)
type ImmutableCookieObject = ImmutableObject<CookieObject>;
// @public (undocumented)
type ImmutableHeaderObject = ImmutableObject<HeaderObject>;
// @public (undocumented)
type ImmutableObject<T> = {
readonly [K in keyof T]: Immutable<T[K]>;
};
// @public (undocumented)
type ImmutableOpenAPIObject = ImmutableObject<OpenAPIObject>;
// @public (undocumented)
type ImmutableParameterObject = ImmutableObject<ParameterObject>;
// @public (undocumented)
type ImmutablePathObject = ImmutableObject<PathObject>;
// @public (undocumented)
type ImmutableQueryObject = ImmutableObject<QueryObject>;
// @public (undocumented)
type ImmutableReferenceObject = ImmutableObject<ReferenceObject>;
// @public (undocumented)
type ImmutableRequestBodyObject = ImmutableObject<RequestBodyObject>;
// @public (undocumented)
type ImmutableResponseObject = ImmutableObject<ResponseObject>;
// @public (undocumented)
type ImmutableSchemaObject = ImmutableObject<SchemaObject>;
declare namespace internal {
export {
RequiredDoc,
PathDoc,
ValueOf,
PathTemplate,
DocPath,
DocPathTemplate,
DocPathMethod,
MethodAwareDocPath,
DocOperation,
ComponentTypes,
ComponentRef,
SchemaRef,
ObjectWithContentSchema,
UnionToIntersection,
LastOf,
Push,
TuplifyUnion,
ConvertAll,
UnknownIfNever,
ToTypeSafe,
DiscriminateUnion,
MapDiscriminatedUnion,
PickOptionalKeys,
PickRequiredKeys,
OptionalMap,
RequiredMap,
FullMap,
Filter,
DocRequestHandler,
DocRequestHandlerParams,
DocRequestMatcher,
Immutable,
ImmutableObject,
ImmutableReferenceObject,
ImmutableOpenAPIObject,
ImmutableContentObject,
ImmutableRequestBodyObject,
ImmutableResponseObject,
ImmutableParameterObject,
HeaderObject,
ImmutableHeaderObject,
CookieObject,
ImmutableCookieObject,
QueryObject,
ImmutableQueryObject,
PathObject,
ImmutablePathObject,
ImmutableSchemaObject,
DocParameter,
DocParameters,
ResolveDocParameterSchema,
ParameterSchema,
MapToSchema,
ParametersSchema,
HeaderSchema,
CookieSchema,
PathSchema,
QuerySchema,
RequestBody,
RequestBodySchema,
RequestBodyToJsonSchema,
Response_2 as Response,
ResponseSchemas,
ResponseBodyToJsonSchema,
};
}
export { internal };
// @public (undocumented)
type LastOf<T> = UnionToIntersection<
T extends any ? () => T : never
> extends () => infer R
? R
: never;
// @public (undocumented)
type MapDiscriminatedUnion<T extends Record<K, string>, K extends keyof T> = {
[V in T[K]]: DiscriminateUnion<T, K, V>;
};
// @public (undocumented)
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)
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)
type ObjectWithContentSchema<
Doc extends RequiredDoc,
Object extends {
content?: ImmutableContentObject;
},
> = Object['content'] extends ImmutableContentObject
? SchemaRef<Doc, Object['content']['application/json']['schema']>
: never;
// @public (undocumented)
type OptionalMap<
T extends {
[key: string]: any;
},
> = {
[P in Exclude<PickOptionalKeys<T>, undefined>]?: NonNullable<T[P]>;
};
// @public (undocumented)
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)
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 (undocumented)
type PathDoc = Pick<ImmutableOpenAPIObject, 'paths'>;
// @public (undocumented)
interface PathObject extends ParameterObject {
// (undocumented)
in: 'path';
// (undocumented)
style?: 'simple' | 'label' | 'matrix';
}
// @public (undocumented)
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
type PathTemplate<Path extends string> =
Path extends `${infer Prefix}{${infer PathName}}${infer Suffix}`
? `${Prefix}:${PathName}${PathTemplate<Suffix>}`
: Path;
// @public (undocumented)
type PickOptionalKeys<
T extends {
[key: string]: any;
},
> = {
[K in keyof T]: true extends T[K]['required'] ? never : K;
}[keyof T];
// @public (undocumented)
type PickRequiredKeys<
T extends {
[key: string]: any;
},
> = {
[K in keyof T]: true extends T[K]['required'] ? K : never;
}[keyof T];
// @public (undocumented)
type Push<T extends any[], V> = [...T, V];
// @public (undocumented)
interface QueryObject extends ParameterObject {
// (undocumented)
in: 'query';
// (undocumented)
style?: 'form' | 'deepObject' | 'pipeDelimited' | 'spaceDelimited';
}
// @public (undocumented)
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)
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)
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
type RequestBodyToJsonSchema<
Doc extends RequiredDoc,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
Method extends DocPathMethod<Doc, Path>,
> = ToTypeSafe<RequestBodySchema<Doc, Path, Method>>;
// @public
type RequiredDoc = Pick<ImmutableOpenAPIObject, 'paths' | 'components'>;
// @public (undocumented)
type RequiredMap<
T extends {
[key: string]: any;
},
> = {
[P in Exclude<PickRequiredKeys<T>, undefined>]: NonNullable<T[P]>;
};
// @public (undocumented)
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 DocOperation<Doc, Path, Method>['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];
// @public
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)
type ResponseSchemas<
Doc extends RequiredDoc,
Path extends DocPathTemplate<Doc>,
Method extends DocPathMethod<Doc, Path>,
> = {
[StatusCode in keyof DocOperation<
Doc,
Path,
Method
>['responses']]: Response_2<
Doc,
Path,
Method,
StatusCode
> extends ImmutableResponseObject
? ObjectWithContentSchema<Doc, Response_2<Doc, Path, Method, StatusCode>>
: never;
};
// @public (undocumented)
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)
type ToTypeSafe<T> = UnknownIfNever<ConvertAll<TuplifyUnion<T>>[number]>;
// @public (undocumented)
type TuplifyUnion<
T,
L = LastOf<T>,
N = [T] extends [never] ? true : false,
> = true extends N ? [] : Push<TuplifyUnion<Exclude<T, L>>, L>;
// @public
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I,
) => void
? I
: never;
// @public (undocumented)
type UnknownIfNever<P> = [P] extends [never] ? unknown : P;
// @public
type ValueOf<T> = T[keyof T];
```
@@ -0,0 +1,40 @@
{
"name": "@backstage/backend-openapi-utils",
"description": "OpenAPI typescript support.",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "node-library"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"devDependencies": {
"@backstage/cli": "workspace:^"
},
"files": [
"dist"
],
"dependencies": {
"@types/express": "^4.17.6",
"@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",
"openapi3-ts": "^3.1.2"
}
}
@@ -0,0 +1,25 @@
/*
* 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.
*/
/**
* Common functionalities for the openapi-router plugin.
*
* @packageDocumentation
*/
import * as internal from './types';
export { internal };
export type { ApiRouter } from './router';
@@ -0,0 +1,39 @@
/*
* 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 { Router } from 'express';
import { DocRequestMatcher, RequiredDoc } 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'>;
post: DocRequestMatcher<Doc, this, 'post'>;
all: DocRequestMatcher<Doc, this, 'all'>;
put: DocRequestMatcher<Doc, this, 'put'>;
delete: DocRequestMatcher<Doc, this, 'delete'>;
patch: DocRequestMatcher<Doc, this, 'patch'>;
options: DocRequestMatcher<Doc, this, 'options'>;
head: DocRequestMatcher<Doc, this, 'head'>;
}
@@ -0,0 +1,16 @@
/*
* 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 {};
@@ -0,0 +1,286 @@
/*
* 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 { FromSchema, JSONSchema7 } from 'json-schema-to-ts';
import {
ImmutableContentObject,
ImmutableOpenAPIObject,
ImmutableReferenceObject,
} from './immutable';
/**
* 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
*/
export type ValueOf<T> = T[keyof T];
/**
* Validate a string against OpenAPI path template, {@link https://spec.openapis.org/oas/v3.1.0#path-templating-matching}.
*
* @example
* ```ts
* 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}`
? `${Prefix}:${PathName}${PathTemplate<Suffix>}`
: Path;
/**
* Extract path as specified in OpenAPI `Doc` based on request path
* @example
* ```ts
* const spec = {
* paths: {
* "/posts/{postId}/comments/{commentId}": {},
* "/posts/comments": {},
* }
* };
* 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,
Path extends PathTemplate<Extract<keyof Doc['paths'], string>>,
> = ValueOf<{
[Template in Extract<
keyof Doc['paths'],
string
>]: 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>>,
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
*/
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>,
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
*/
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
*/
export type ObjectWithContentSchema<
Doc extends RequiredDoc,
Object extends { content?: ImmutableContentObject },
> = Object['content'] extends ImmutableContentObject
? SchemaRef<Doc, Object['content']['application/json']['schema']>
: never;
/**
* 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
) extends (k: infer I) => void
? 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,
]
? 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,
> = {
[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;
@@ -0,0 +1,79 @@
/*
* 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 type core from 'express-serve-static-core';
import { DocPathTemplate, MethodAwareDocPath, RequiredDoc } from './common';
import { PathSchema, QuerySchema } from './params';
import { RequestBodyToJsonSchema } from './requests';
import { ResponseBodyToJsonSchema } from './responses';
/**
* Typed express request handler.
* @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>
>;
/**
* Typed express error handler / request handler union type.
* @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>
>;
/**
* Superset of the express router path matcher that enforces typed request and response bodies.
* @public
*/
export interface DocRequestMatcher<
Doc extends RequiredDoc,
T,
Method extends
| 'all'
| 'get'
| 'post'
| 'put'
| 'delete'
| 'patch'
| 'options'
| 'head',
> {
<Path extends MethodAwareDocPath<Doc, DocPathTemplate<Doc>, Method>>(
path: Path,
...handlers: Array<DocRequestHandler<Doc, Path, Method>>
): T;
<Path extends MethodAwareDocPath<Doc, DocPathTemplate<Doc>, Method>>(
path: Path,
...handlers: Array<DocRequestHandlerParams<Doc, Path, Method>>
): T;
}
@@ -0,0 +1,140 @@
/*
* 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 type {
ContentObject,
OpenAPIObject,
ReferenceObject,
RequestBodyObject,
ParameterObject,
SchemaObject,
ResponseObject,
} from 'openapi3-ts';
/**
* This file is meant to hold Immutable overwrites of the values provided by the `openapi3-ts`
* package due to issues with `as const` supporting only readonly values.
*/
/**
* 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
| 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
*/
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 {
in: 'header';
style: 'simple';
}
/**
* @public
*/
export type ImmutableHeaderObject = ImmutableObject<HeaderObject>;
/**
* @public
*/
export interface CookieObject extends ParameterObject {
in: 'cookie';
style?: 'form';
}
/**
* @public
*/
export type ImmutableCookieObject = ImmutableObject<CookieObject>;
/**
* @public
*/
export interface QueryObject extends ParameterObject {
in: 'query';
style?: 'form' | 'deepObject' | 'pipeDelimited' | 'spaceDelimited';
}
/**
* @public
*/
export type ImmutableQueryObject = ImmutableObject<QueryObject>;
/**
* @public
*/
export interface PathObject extends ParameterObject {
in: 'path';
style?: 'simple' | 'label' | 'matrix';
}
/**
* @public
*/
export type ImmutablePathObject = ImmutableObject<PathObject>;
/**
* @public
*/
export type ImmutableSchemaObject = ImmutableObject<SchemaObject>;
@@ -0,0 +1,22 @@
/*
* 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 './express';
export * from './immutable';
export * from './params';
export * from './requests';
export * from './responses';
@@ -0,0 +1,171 @@
/*
* 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 {
ImmutableCookieObject,
ImmutableHeaderObject,
ImmutableParameterObject,
ImmutablePathObject,
ImmutableQueryObject,
ImmutableReferenceObject,
ImmutableSchemaObject,
} from './immutable';
import {
ComponentRef,
ComponentTypes,
DocOperation,
DocPath,
DocPathMethod,
Filter,
FullMap,
MapDiscriminatedUnion,
PathTemplate,
RequiredDoc,
} from './common';
import { FromSchema, JSONSchema7 } from 'json-schema-to-ts';
/**
* @public
*/
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
*/
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 ResolveDocParameterSchema<
Doc extends RequiredDoc,
Schema extends ImmutableParameterObject['schema'],
> = Schema extends ImmutableReferenceObject
? 'parameters' extends ComponentTypes<Doc>
? ComponentRef<Doc, 'parameters', Schema>
: never
: Schema;
/**
* @public
*/
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
*/
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
*/
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 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>>,
Method extends DocPathMethod<Doc, Path>,
> = ParametersSchema<Doc, DocPath<Doc, Path>, Method, ImmutableQueryObject>;
@@ -0,0 +1,82 @@
/*
* 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 {
ComponentRef,
ComponentTypes,
ObjectWithContentSchema,
RequiredDoc,
DocOperation,
DocPath,
DocPathMethod,
DocPathTemplate,
PathTemplate,
ToTypeSafe,
} from './common';
import {
ImmutableReferenceObject,
ImmutableRequestBodyObject,
} from './immutable';
/**
* @public
*/
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
*/
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;
/**
* Transform the OpenAPI request body schema to a typesafe JSON schema.
* @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>>;
@@ -0,0 +1,83 @@
/*
* 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 {
ComponentRef,
ComponentTypes,
ObjectWithContentSchema,
RequiredDoc,
DocOperation,
DocPathMethod,
DocPathTemplate,
PathTemplate,
ToTypeSafe,
ValueOf,
} from './common';
import { ImmutableReferenceObject, ImmutableResponseObject } from './immutable';
/**
* @public
*/
export type Response<
Doc extends RequiredDoc,
Path extends keyof Doc['paths'],
Method extends keyof Doc['paths'][Path],
StatusCode extends keyof DocOperation<Doc, Path, Method>['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];
/**
* @public
*/
export type ResponseSchemas<
Doc extends RequiredDoc,
Path extends DocPathTemplate<Doc>,
Method extends DocPathMethod<Doc, Path>,
> = {
[StatusCode in keyof DocOperation<Doc, Path, Method>['responses']]: Response<
Doc,
Path,
Method,
StatusCode
> extends ImmutableResponseObject
? ObjectWithContentSchema<Doc, Response<Doc, Path, Method, StatusCode>>
: never;
};
/**
* Transform the OpenAPI request body schema to a typesafe JSON schema.
* @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>>>;
+20
View File
@@ -14,6 +14,8 @@ Options:
Commands:
api-reports [options] [paths...]
type-deps
schema:openapi:verify [paths...]
schema:openapi:generate [paths...]
help [command]
```
@@ -33,6 +35,24 @@ Options:
-h, --help
```
### `backstage-repo-tools schema:openapi:generate`
```
Usage: backstage-repo-tools schema:openapi:generate [options] [paths...]
Options:
-h, --help
```
### `backstage-repo-tools schema:openapi:verify`
```
Usage: backstage-repo-tools schema:openapi:verify [options] [paths...]
Options:
-h, --help
```
### `backstage-repo-tools type-deps`
```
+4
View File
@@ -30,6 +30,7 @@
"backstage-repo-tools": "bin/backstage-repo-tools"
},
"dependencies": {
"@apidevtools/swagger-parser": "^10.1.0",
"@backstage/cli-common": "workspace:^",
"@backstage/errors": "workspace:^",
"@manypkg/get-packages": "^1.1.3",
@@ -40,7 +41,10 @@
"fs-extra": "10.1.0",
"glob": "^8.0.3",
"is-glob": "^4.0.3",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"minimatch": "^5.1.1",
"p-limit": "^3.0.2",
"ts-node": "^10.0.0"
},
"devDependencies": {
@@ -15,7 +15,6 @@
*/
import { OptionValues } from 'commander';
import fs from 'fs-extra';
import {
createTemporaryTsConfig,
categorizePackageDirs,
@@ -23,7 +22,7 @@ import {
runCliExtraction,
buildDocs,
} from './api-extractor';
import { findPackageDirs, paths as cliPaths } from '../../lib/paths';
import { getMatchingWorkspacePaths, paths as cliPaths } from '../../lib/paths';
import { generateTypeDeclarations } from './generateTypeDeclarations';
type Options = {
@@ -49,10 +48,7 @@ export const buildApiReports = async (paths: string[] = [], opts: Options) => {
const omitMessages = parseArrayOption(opts.omitMessages);
const isAllPackages = !paths?.length;
const selectedPaths = isAllPackages
? await getWorkspacePackagePathPatterns()
: paths;
const selectedPackageDirs = await findPackageDirs(selectedPaths);
const selectedPackageDirs = await getMatchingWorkspacePaths(paths);
if (isAllPackages && !isCiBuild && !isDocsBuild) {
console.log('');
@@ -111,26 +107,6 @@ export const buildApiReports = async (paths: string[] = [], opts: Options) => {
}
};
/**
* Retrieves the list of package names in the "workspaces" field of the `package.json` file in the current workspace root.
*
* If the file does not exist, or the "workspaces" field is not present, returns `undefined`.
*
* @returns {Promise<string[] | undefined>} The list of package names, or `undefined` if not found.
*/
async function getWorkspacePackagePathPatterns() {
const pkgJson = await fs
.readJson(cliPaths.resolveTargetRoot('package.json'))
.catch(error => {
if (error.code === 'ENOENT') {
return undefined;
}
throw error;
});
const workspaces = pkgJson?.workspaces?.packages;
return workspaces;
}
/**
* Splits the input string on comma, and returns an array of the resulting substrings.
* for `undefined` or an empty string, returns an empty array.
+14
View File
@@ -52,6 +52,20 @@ export function registerCommands(program: Command) {
.command('type-deps')
.description('Find inconsistencies in types of all packages and plugins')
.action(lazy(() => import('./type-deps/type-deps').then(m => m.default)));
program
.command('schema:openapi:verify [paths...]')
.description(
'Verify that all OpenAPI schemas are valid and have a matching `schemas/openapi.ts` file.',
)
.action(lazy(() => import('./openapi/verify').then(m => m.bulkCommand)));
program
.command('schema:openapi:generate [paths...]')
.description(
'Generates a Typescript file from an OpenAPI yaml spec. For use with the `@backstage/backend-openapi-utils` ApiRouter type.',
)
.action(lazy(() => import('./openapi/generate').then(m => m.bulkCommand)));
}
// Wraps an action function so that it always exits and handles errors
@@ -0,0 +1,18 @@
/*
* 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 const YAML_SCHEMA_PATH = 'src/schema/openapi.yaml';
export const TS_MODULE = 'src/schema/openapi';
export const TS_SCHEMA_PATH = `${TS_MODULE}.ts`;
@@ -0,0 +1,77 @@
/*
* 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 fs from 'fs-extra';
import YAML from 'js-yaml';
import chalk from 'chalk';
import { resolve } from 'path';
import { runner } from './runner';
import { TS_SCHEMA_PATH, YAML_SCHEMA_PATH } from './constants';
import { promisify } from 'util';
import { exec as execCb } from 'child_process';
const exec = promisify(execCb);
async function generate(
directoryPath: string,
config?: { skipMissingYamlFile: boolean },
) {
const { skipMissingYamlFile } = config ?? {};
const openapiPath = resolve(directoryPath, YAML_SCHEMA_PATH);
if (!(await fs.pathExists(openapiPath))) {
if (skipMissingYamlFile) {
return;
}
throw new Error(`Could not find ${YAML_SCHEMA_PATH} in root of directory.`);
}
const yaml = YAML.load(await fs.readFile(openapiPath, 'utf8'));
const tsPath = resolve(directoryPath, TS_SCHEMA_PATH);
await fs.writeFile(
tsPath,
`export default ${JSON.stringify(yaml, null, 2)} as const`,
);
await exec(`yarn backstage-cli package lint --fix ${tsPath}`);
}
export async function bulkCommand(paths: string[] = []): Promise<void> {
const resultsList = await runner(paths, (dir: string) =>
generate(dir, { skipMissingYamlFile: true }),
);
let failed = false;
for (const { relativeDir, resultText } of resultsList) {
if (resultText) {
console.log();
console.log(
chalk.red(
`OpenAPI yaml to Typescript generation failed in ${relativeDir}:`,
),
);
console.log(resultText.trimStart());
failed = true;
}
}
if (failed) {
process.exit(1);
} else {
console.log(chalk.green('Generated all files.'));
}
}
@@ -0,0 +1,49 @@
/*
* 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 { getMatchingWorkspacePaths } from '../../lib/paths';
import pLimit from 'p-limit';
import { relative as relativePath } from 'path';
import { paths as cliPaths } from '../../lib/paths';
export async function runner(
paths: string[],
command: (dir: string) => Promise<void>,
) {
const packages = await getMatchingWorkspacePaths(paths);
const limit = pLimit(5);
const resultsList = await Promise.all(
packages.map(pkg =>
limit(async () => {
let resultText = '';
try {
console.log(`## Processing ${pkg}`);
await command(pkg);
} catch (err) {
resultText = err.message;
}
return {
relativeDir: relativePath(cliPaths.targetRoot, pkg),
resultText,
};
}),
),
);
return resultsList;
}
@@ -0,0 +1,74 @@
/*
* 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 fs from 'fs-extra';
import YAML from 'js-yaml';
import { isEqual, cloneDeep } from 'lodash';
import { join } from 'path';
import chalk from 'chalk';
import { relative as relativePath, resolve as resolvePath } from 'path';
import Parser from '@apidevtools/swagger-parser';
import { runner } from './runner';
import { paths as cliPaths } from '../../lib/paths';
import { TS_MODULE, TS_SCHEMA_PATH, YAML_SCHEMA_PATH } from './constants';
async function verify(directoryPath: string) {
const openapiPath = join(directoryPath, YAML_SCHEMA_PATH);
if (!(await fs.pathExists(openapiPath))) {
return;
}
const yaml = YAML.load(await fs.readFile(openapiPath, 'utf8'));
await Parser.validate(cloneDeep(yaml) as any);
const schemaPath = join(directoryPath, TS_SCHEMA_PATH);
if (!(await fs.pathExists(schemaPath))) {
throw new Error(`No \`${TS_SCHEMA_PATH}\` file found.`);
}
const schema = await import(resolvePath(join(directoryPath, TS_MODULE)));
if (!schema.default) {
throw new Error(`\`${TS_SCHEMA_PATH}\` needs to have a default export.`);
}
if (!isEqual(schema.default, yaml)) {
const path = relativePath(cliPaths.targetRoot, directoryPath);
throw new Error(
`\`${YAML_SCHEMA_PATH}\` and \`${TS_SCHEMA_PATH}\` do not match. Please run \`yarn backstage-repo-tools schema:openapi:generate ${path}\` to regenerate \`${TS_SCHEMA_PATH}\`.`,
);
}
}
export async function bulkCommand(paths: string[] = []): Promise<void> {
const resultsList = await runner(paths, dir => verify(dir));
let failed = false;
for (const { relativeDir, resultText } of resultsList) {
if (resultText) {
console.log();
console.log(chalk.red(`OpenAPI validation failed in ${relativeDir}:`));
console.log(resultText.trimStart());
failed = true;
}
}
if (failed) {
process.exit(1);
} else {
console.log(chalk.green('Verified all files.'));
}
}
+36
View File
@@ -66,3 +66,39 @@ export async function findPackageDirs(selectedPaths: string[] = []) {
}
return packageDirs;
}
/**
* Retrieves the list of package names in the "workspaces" field of the `package.json` file in the current workspace root.
*
* If the file does not exist, or the "workspaces" field is not present, returns `undefined`.
*
* @returns The list of package names, or `undefined` if not found.
*/
export async function getWorkspacePackagePathPatterns(): Promise<
string[] | undefined
> {
const pkgJson = await fs
.readJson(paths.resolveTargetRoot('package.json'))
.catch(error => {
if (error.code === 'ENOENT') {
return undefined;
}
throw error;
});
const workspaces = pkgJson?.workspaces?.packages;
return workspaces;
}
/**
* Given a list of paths from the user, returns the listing package directories from the
* workspace. Returns all directories if no paths are given.
* @param cliPaths User given paths from CLI.
* @returns Matching package directories or all if no cli paths passed in.
*/
export async function getMatchingWorkspacePaths(cliPaths: string[]) {
const isAllPackages = !cliPaths?.length;
const selectedPaths = isAllPackages
? await getWorkspacePackagePathPatterns()
: cliPaths;
return await findPackageDirs(selectedPaths);
}
+1
View File
@@ -84,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:^",
File diff suppressed because it is too large Load Diff
@@ -6,6 +6,7 @@ info:
description: The Backstage backend plugin that provides the Backstage catalog
license:
name: Apache-2.0
url: http://www.apache.org/licenses/LICENSE-2.0.html
contact: {}
servers:
@@ -14,21 +15,94 @@ servers:
components:
examples: {}
headers: {}
parameters: {}
parameters:
kind:
name: kind
in: path
required: true
schema:
type: string
namespace:
name: namespace
in: path
required: true
schema:
type: string
name:
name: name
in: path
required: true
schema:
type: string
uid:
name: uid
in: path
required: true
schema:
type: string
cursor:
name: cursor
in: query
description: Cursor to a set page of results.
required: false
schema:
type: string
minLength: 1
after:
name: after
in: query
description: Pointer to the previous page of results.
required: false
schema:
type: string
minLength: 1
fields:
name: fields
in: query
description: Restrict to just these fields in the response.
required: false
schema:
type: string
filter:
name: filter
in: query
description: Filter for just the entities defined by this filter.
required: false
schema:
type: string
offset:
name: offset
in: query
description: Number of records to skip in the query page.
required: false
schema:
type: integer
minimum: 0
limit:
name: limit
in: query
description: Number of records to return in the response.
required: false
schema:
type: integer
minimum: 1
sortField:
name: sortField
in: query
description: The fields to sort returned results by.
required: false
schema:
type: array
items:
type: array
items:
type: string
description: A two-item tuple of [field, order].
explode: true
style: form
requestBodies: {}
responses: {}
schemas:
RefreshOptions:
type: object
properties:
authorizationToken:
type: string
entityRef:
type: string
description: The reference to a single entity that should be refreshed
required:
- entityRef
description: Options for requesting a refresh of entities in the catalog.
JsonObject:
type: object
properties: {}
@@ -57,6 +131,7 @@ components:
required:
- url
description: A link to external information that is related to the entity.
additionalProperties: false
EntityMeta:
allOf:
- $ref: '#/components/schemas/JsonObject'
@@ -133,6 +208,7 @@ components:
required:
- name
description: Metadata fields common to all versions/kinds of entity.
additionalProperties: false
EntityRelation:
type: object
properties:
@@ -146,6 +222,7 @@ components:
- targetRef
- type
description: A relation of a specific type to another entity in the catalog.
additionalProperties: false
Entity:
type: object
properties:
@@ -171,6 +248,7 @@ components:
- kind
- apiVersion
description: The parts of the format that's common to all versions/kinds of entity.
additionalProperties: false
EntityAncestryResponse:
type: object
properties:
@@ -193,35 +271,43 @@ components:
required:
- items
- rootEntityRef
additionalProperties: false
EntitiesBatchResponse:
type: object
properties:
items:
type: array
items:
allOf:
anyOf:
- $ref: '#/components/schemas/Entity'
nullable: true
- type: 'null'
description: |-
The list of entities, in the same order as the refs in the request. Entries
that are null signify that no entity existed with that ref.
required:
- items
EntityFacets:
additionalProperties: false
EntityFacet:
type: object
properties:
value:
type: string
count:
type: number
description: Construct a type with a set of properties K of type T
additionalProperties: false
EntityFacetsResponse:
type: object
properties: {}
additionalProperties:
$ref: '#/components/schemas/EntityFacets'
properties:
facets:
type: object
additionalProperties:
type: array
items:
$ref: '#/components/schemas/EntityFacet'
required:
- facets
additionalProperties: false
Location:
type: object
properties:
@@ -236,6 +322,7 @@ components:
- type
- id
description: Entity location for a specific entity.
additionalProperties: false
LocationSpec:
type: object
properties:
@@ -252,6 +339,7 @@ components:
- target
- type
description: Holds the entity location information.
additionalProperties: false
AnalyzeLocationExistingEntity:
type: object
properties:
@@ -270,7 +358,94 @@ components:
read and emitted like this so that the frontend can inform the user that it
located them and can make sure to register them as well if they weren't
already
RecursivePartial_Entity_:
additionalProperties: false
RecursivePartialEntityRelation:
type: object
properties:
targetRef:
type: string
description: The entity ref of the target of this relation.
type:
type: string
description: The type of the relation.
description: A relation of a specific type to another entity in the catalog.
additionalProperties: false
RecursivePartialEntityMeta:
allOf:
- $ref: '#/components/schemas/JsonObject'
- type: object
properties:
links:
type: array
items:
$ref: '#/components/schemas/EntityLink'
description: A list of external hyperlinks related to the entity.
tags:
type: array
items:
type: string
description: |-
A list of single-valued strings, to for example classify catalog entities in
various ways.
annotations:
$ref: '#/components/schemas/MapStringString'
labels:
$ref: '#/components/schemas/MapStringString'
description:
type: string
description: |-
A short (typically relatively few words, on one line) description of the
entity.
title:
type: string
description: |-
A display name of the entity, to be presented in user interfaces instead
of the `name` property above, when available.
This field is sometimes useful when the `name` is cumbersome or ends up
being perceived as overly technical. The title generally does not have
as stringent format requirements on it, so it may contain special
characters and be more explanatory. Do keep it very short though, and
avoid situations where a title can be confused with the name of another
entity, or where two entities share a title.
Note that this is only for display purposes, and may be ignored by some
parts of the code. Entity references still always make use of the `name`
property, not the title.
namespace:
type: string
description: The namespace that the entity belongs to.
name:
type: string
description: |-
The name of the entity.
Must be unique within the catalog at any given point in time, for any
given namespace + kind pair. This value is part of the technical
identifier of the entity, and as such it will appear in URLs, database
tables, entity references, and similar. It is subject to restrictions
regarding what characters are allowed.
If you want to use a different, more human readable string with fewer
restrictions on it in user interfaces, see the `title` field below.
etag:
type: string
description: |-
An opaque string that changes for each update operation to any part of
the entity, including metadata.
This field can not be set by the user at creation time, and the server
will reject an attempt to do so. The field will be populated in read
operations. The field can (optionally) be specified when performing
update or delete operations, and the server will then reject the
operation if it does not match the current stored value.
uid:
type: string
description: |-
A globally unique ID for the entity.
This field can not be set by the user at creation time, and the server
will reject an attempt to do so. The field will be populated in read
operations. The field can (optionally) be specified when performing
update or delete operations, but the server is free to reject requests
that do so in such a way that it breaks semantics.
description: Metadata fields common to all versions/kinds of entity.
additionalProperties: false
RecursivePartialEntity:
type: object
properties:
apiVersion:
@@ -282,15 +457,16 @@ components:
type: string
description: The high level entity type being described.
metadata:
$ref: '#/components/schemas/EntityMeta'
$ref: '#/components/schemas/RecursivePartialEntityMeta'
spec:
$ref: '#/components/schemas/JsonObject'
relations:
type: array
items:
$ref: '#/components/schemas/EntityRelation'
$ref: '#/components/schemas/RecursivePartialEntityRelation'
description: The relations that this entity has with other entities.
description: Makes all keys of an entire hierarchy optional.
additionalProperties: false
AnalyzeLocationEntityField:
type: object
properties:
@@ -302,8 +478,9 @@ components:
field empty; which would currently make it owned by X" where X is taken from the
codeowners file.
value:
type: string
nullable: true
type:
- string
- 'null'
state:
type: string
enum:
@@ -321,6 +498,7 @@ components:
- value
- state
- field
additionalProperties: false
AnalyzeLocationGenerateEntity:
type: object
properties:
@@ -329,7 +507,7 @@ components:
items:
$ref: '#/components/schemas/AnalyzeLocationEntityField'
entity:
$ref: '#/components/schemas/RecursivePartial_Entity_'
$ref: '#/components/schemas/RecursivePartialEntity'
required:
- fields
- entity
@@ -339,6 +517,7 @@ components:
the frontend. It'll probably contain a (possibly incomplete) entity, plus
enough info for the frontend to know what form data to show to the user
for overriding/completing the info.
additionalProperties: false
AnalyzeLocationResponse:
type: object
properties:
@@ -353,6 +532,7 @@ components:
required:
- generateEntities
- existingEntityFiles
additionalProperties: false
LocationInput:
type: object
properties:
@@ -369,6 +549,7 @@ components:
- type
- target
- presence
additionalProperties: false
SerializedError:
allOf:
- $ref: '#/components/schemas/JsonObject'
@@ -390,12 +571,35 @@ components:
- message
- name
description: The serialized form of an Error.
additionalProperties: false
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: |-
The cursor for the next batch of entities.
prevCursor:
type: string
description: |-
The cursor for the previous batch of entities.
additionalProperties: false
securitySchemes:
JWT:
type: http
scheme: bearer
bearerFormat: JWT
paths:
/refresh:
post:
@@ -404,7 +608,6 @@ paths:
'200':
description: Refreshed
security:
# From https://stackoverflow.com/questions/47659324/how-to-specify-an-endpoints-authorization-is-optional-in-openapi-v3
- {}
- JWT: []
parameters: []
@@ -413,8 +616,17 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/RefreshOptions'
type: object
properties:
authorizationToken:
type: string
entityRef:
type: string
description: The reference to a single entity that should be refreshed
required:
- entityRef
description: Options for requesting a refresh of entities in the catalog.
additionalProperties: false
/entities:
get:
operationId: GetEntities
@@ -431,32 +643,11 @@ paths:
- {}
- JWT: []
parameters:
- in: query
name: filter
required: false
schema:
type: string
- in: query
name: fields
required: false
schema:
type: string
- in: query
name: offset
required: false
schema:
type: string
- in: query
name: limit
required: false
schema:
type: string
- in: query
name: after
required: false
schema:
type: string
- $ref: '#/components/parameters/fields'
- $ref: '#/components/parameters/limit'
- $ref: '#/components/parameters/filter'
- $ref: '#/components/parameters/offset'
- $ref: '#/components/parameters/after'
/entities/by-uid/{uid}:
get:
operationId: GetEntityByUid
@@ -471,11 +662,7 @@ paths:
- {}
- JWT: []
parameters:
- in: path
name: uid
required: true
schema:
type: string
- $ref: '#/components/parameters/uid'
delete:
operationId: DeleteEntityByUid
responses:
@@ -485,12 +672,7 @@ paths:
- {}
- JWT: []
parameters:
- in: path
name: uid
required: true
schema:
type: string
- $ref: '#/components/parameters/uid'
/entities/by-name/{kind}/{namespace}/{name}:
get:
operationId: GetEntityByName
@@ -505,22 +687,9 @@ paths:
- {}
- JWT: []
parameters:
- in: path
name: kind
required: true
schema:
type: string
- in: path
name: namespace
required: true
schema:
type: string
- in: path
name: name
required: true
schema:
type: string
- $ref: '#/components/parameters/kind'
- $ref: '#/components/parameters/namespace'
- $ref: '#/components/parameters/name'
/entities/by-name/{kind}/{namespace}/{name}/ancestry:
get:
operationId: GetEntityAncestryByName
@@ -535,22 +704,9 @@ paths:
- {}
- JWT: []
parameters:
- in: path
name: kind
required: true
schema:
type: string
- in: path
name: namespace
required: true
schema:
type: string
- in: path
name: name
required: true
schema:
type: string
- $ref: '#/components/parameters/kind'
- $ref: '#/components/parameters/namespace'
- $ref: '#/components/parameters/name'
/entities/by-refs:
post:
operationId: GetEntitiesByRefs
@@ -565,20 +721,58 @@ paths:
- {}
- JWT: []
parameters:
- in: query
name: fields
required: false
schema:
type: string
- $ref: '#/components/parameters/fields'
requestBody:
required: true
content:
application/json:
schema:
type: array
items:
type: string
type: object
required:
- entityRefs
properties:
entityRefs:
type: array
items:
type: string
fields:
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:
- $ref: '#/components/parameters/fields'
- $ref: '#/components/parameters/limit'
- $ref: '#/components/parameters/sortField'
- $ref: '#/components/parameters/cursor'
- name: fullTextFilterTerm
in: query
description: Text search term.
required: false
schema:
type: string
- name: fullTextFilterFields
in: query
description: A comma separated list of fields to sort returned results by.
required: false
schema:
type: array
items:
type: string
explode: false
style: form
/entity-facets:
get:
operationId: GetEntityFacets
@@ -598,12 +792,7 @@ paths:
required: true
schema:
type: string
- in: query
name: filter
required: false
schema:
type: string
- $ref: '#/components/parameters/filter'
/locations:
post:
operationId: CreateLocation
@@ -634,7 +823,7 @@ paths:
name: dryRun
required: false
schema:
type: boolean
type: string
requestBody:
required: true
content:
@@ -675,7 +864,6 @@ paths:
- {}
- JWT: []
parameters: []
/locations/{id}:
get:
operationId: GetLocation
@@ -709,7 +897,6 @@ paths:
required: true
schema:
type: string
/analyze-location:
post:
operationId: AnalyzeLocation
@@ -738,7 +925,6 @@ paths:
required:
- catalogFileName
- location
/validate-entity:
post:
operationId: ValidateEntity
@@ -50,6 +50,8 @@ import {
locationInput,
validateRequestBody,
} from './util';
import type { ApiRouter } from '@backstage/backend-openapi-utils';
import spec from '../schema/openapi';
/**
* Options used by {@link createRouter}.
@@ -85,7 +87,7 @@ export async function createRouter(
logger,
permissionIntegrationRouter,
} = options;
const router = Router();
const router = Router() as ApiRouter<typeof spec>;
router.use(express.json());
const readonlyEnabled =
+105 -5
View File
@@ -21,6 +21,17 @@ __metadata:
languageName: node
linkType: hard
"@apidevtools/json-schema-ref-parser@npm:9.0.6":
version: 9.0.6
resolution: "@apidevtools/json-schema-ref-parser@npm:9.0.6"
dependencies:
"@jsdevtools/ono": ^7.1.3
call-me-maybe: ^1.0.1
js-yaml: ^3.13.1
checksum: c7ff53623ab8a9dd221772a5757fa0b9e5167a5ac3a71c23596634bae6efc85d8efcdebbe17f73ee5c027ea5afc48c705e8a720f02c4909f9a357d8027040b7b
languageName: node
linkType: hard
"@apidevtools/json-schema-ref-parser@npm:^9.0.6":
version: 9.1.2
resolution: "@apidevtools/json-schema-ref-parser@npm:9.1.2"
@@ -33,6 +44,37 @@ __metadata:
languageName: node
linkType: hard
"@apidevtools/openapi-schemas@npm:^2.1.0":
version: 2.1.0
resolution: "@apidevtools/openapi-schemas@npm:2.1.0"
checksum: 4a8f64935b9049ef21e41fa4b188f39f6bc3f5291cebd451701db1115451ccb246a739e46cc5ce9ecdec781671431db40db7851acdac84a990a45756e0f32de3
languageName: node
linkType: hard
"@apidevtools/swagger-methods@npm:^3.0.2":
version: 3.0.2
resolution: "@apidevtools/swagger-methods@npm:3.0.2"
checksum: d06b1ac5c1956613c4c6be695612ef860cd4e962b93a509ca551735a328a856cae1e33399cac1dcbf8333ba22b231746f3586074769ef0e172cf549ec9e7eaae
languageName: node
linkType: hard
"@apidevtools/swagger-parser@npm:^10.1.0":
version: 10.1.0
resolution: "@apidevtools/swagger-parser@npm:10.1.0"
dependencies:
"@apidevtools/json-schema-ref-parser": 9.0.6
"@apidevtools/openapi-schemas": ^2.1.0
"@apidevtools/swagger-methods": ^3.0.2
"@jsdevtools/ono": ^7.1.3
ajv: ^8.6.3
ajv-draft-04: ^1.0.0
call-me-maybe: ^1.0.1
peerDependencies:
openapi-types: ">=7"
checksum: c7c923755bd025ee2cae97e1cfd525538523ba74c341a0ac814c023ffe5e63fc2d997539a8ccf9a0fcec41a2d6337d40cc5735acb991ddcbb415853a241908d1
languageName: node
linkType: hard
"@apollo/cache-control-types@npm:^1.0.2":
version: 1.0.2
resolution: "@apollo/cache-control-types@npm:1.0.2"
@@ -3342,12 +3384,12 @@ __metadata:
languageName: node
linkType: hard
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.6, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.1, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2":
version: 7.20.13
resolution: "@babel/runtime@npm:7.20.13"
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.6, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.1, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2":
version: 7.21.0
resolution: "@babel/runtime@npm:7.21.0"
dependencies:
regenerator-runtime: ^0.13.11
checksum: 09b7a97a05c80540db6c9e4ddf8c5d2ebb06cae5caf3a87e33c33f27f8c4d49d9c67a2d72f1570e796045288fad569f98a26ceba0c4f5fad2af84b6ad855c4fb
checksum: 7b33e25bfa9e0e1b9e8828bb61b2d32bdd46b41b07ba7cb43319ad08efc6fda8eb89445193e67d6541814627df0ca59122c0ea795e412b99c5183a0540d338ab
languageName: node
linkType: hard
@@ -3581,6 +3623,20 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/backend-openapi-utils@workspace:^, @backstage/backend-openapi-utils@workspace:packages/backend-openapi-utils":
version: 0.0.0-use.local
resolution: "@backstage/backend-openapi-utils@workspace:packages/backend-openapi-utils"
dependencies:
"@backstage/cli": "workspace:^"
"@types/express": ^4.17.6
"@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
openapi3-ts: ^3.1.2
languageName: unknown
linkType: soft
"@backstage/backend-plugin-api@workspace:^, @backstage/backend-plugin-api@workspace:packages/backend-plugin-api":
version: 0.0.0-use.local
resolution: "@backstage/backend-plugin-api@workspace:packages/backend-plugin-api"
@@ -5343,6 +5399,7 @@ __metadata:
resolution: "@backstage/plugin-catalog-backend@workspace:plugins/catalog-backend"
dependencies:
"@backstage/backend-common": "workspace:^"
"@backstage/backend-openapi-utils": "workspace:^"
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-test-utils": "workspace:^"
"@backstage/catalog-client": "workspace:^"
@@ -9348,6 +9405,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@backstage/repo-tools@workspace:packages/repo-tools"
dependencies:
"@apidevtools/swagger-parser": ^10.1.0
"@backstage/cli": "workspace:^"
"@backstage/cli-common": "workspace:^"
"@backstage/errors": "workspace:^"
@@ -9363,8 +9421,11 @@ __metadata:
fs-extra: 10.1.0
glob: ^8.0.3
is-glob: ^4.0.3
js-yaml: ^4.1.0
lodash: ^4.17.21
minimatch: ^5.1.1
mock-fs: ^5.1.0
p-limit: ^3.0.2
ts-node: ^10.0.0
peerDependencies:
"@microsoft/api-extractor-model": "*"
@@ -17591,6 +17652,18 @@ __metadata:
languageName: node
linkType: hard
"ajv-draft-04@npm:^1.0.0":
version: 1.0.0
resolution: "ajv-draft-04@npm:1.0.0"
peerDependencies:
ajv: ^8.5.0
peerDependenciesMeta:
ajv:
optional: true
checksum: 3f11fa0e7f7359bef6608657f02ab78e9cc62b1fb7bdd860db0d00351b3863a1189c1a23b72466d2d82726cab4eb20725c76f5e7c134a89865e2bfd0e6828137
languageName: node
linkType: hard
"ajv-formats@npm:^2.1.1":
version: 2.1.1
resolution: "ajv-formats@npm:2.1.1"
@@ -17637,7 +17710,7 @@ __metadata:
languageName: node
linkType: hard
"ajv@npm:^8.0.0, ajv@npm:^8.10.0, ajv@npm:^8.11.0, ajv@npm:^8.12.0, ajv@npm:^8.8.0":
"ajv@npm:^8.0.0, ajv@npm:^8.10.0, ajv@npm:^8.11.0, ajv@npm:^8.12.0, ajv@npm:^8.6.3, ajv@npm:^8.8.0":
version: 8.12.0
resolution: "ajv@npm:8.12.0"
dependencies:
@@ -28273,6 +28346,17 @@ __metadata:
languageName: node
linkType: hard
"json-schema-to-ts@npm:^2.6.2":
version: 2.7.2
resolution: "json-schema-to-ts@npm:2.7.2"
dependencies:
"@babel/runtime": ^7.18.3
"@types/json-schema": ^7.0.9
ts-algebra: ^1.2.0
checksum: cc359d3b2aed035b550582e85989a6fdd4d54bbfd5a2933c9a1e242709a04a7babdba53f5c22280e11b4bd9730b08a933d060fc69810814310908cce76f9e152
languageName: node
linkType: hard
"json-schema-traverse@npm:^0.4.1":
version: 0.4.1
resolution: "json-schema-traverse@npm:0.4.1"
@@ -31942,6 +32026,15 @@ __metadata:
languageName: node
linkType: hard
"openapi3-ts@npm:^3.1.2":
version: 3.2.0
resolution: "openapi3-ts@npm:3.2.0"
dependencies:
yaml: ^2.2.1
checksum: 8796a29a1363bc892ba1acb3ddffd9e6b80e8f83cbfad4cd507262e957317139cac2528ab4b14c1b30bf350ebc9cc4c43ad32a89da4d7c4b85f7e815ffba3ebe
languageName: node
linkType: hard
"openid-client@npm:^5.2.1, openid-client@npm:^5.3.0":
version: 5.4.0
resolution: "openid-client@npm:5.4.0"
@@ -38435,6 +38528,13 @@ __metadata:
languageName: node
linkType: hard
"ts-algebra@npm:^1.2.0":
version: 1.2.0
resolution: "ts-algebra@npm:1.2.0"
checksum: 471d3a049dbceadf2355f980e4d0a4cba413b50e66f0c8fb5eece876c238f7d2a270e1c85aa9ebd04349ae70335715f8e6d87338fd8cf755d12e285e884b3d91
languageName: node
linkType: hard
"ts-easing@npm:^0.2.0":
version: 0.2.0
resolution: "ts-easing@npm:0.2.0"