Merge pull request #15667 from sennyeya/express-openapi-test
[PRFC] OpenAPI Typings for Routers
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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)`.
|
||||
@@ -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);
|
||||
@@ -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>>>;
|
||||
@@ -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`
|
||||
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.'));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
+303
-117
@@ -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 =
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user