feat(openapi-tooling): Add support for request validation and create router stubs.
Signed-off-by: Aramis Sennyey <sennyeyaramis@gmail.com> Signed-off-by: Aramis <sennyeyaramis@gmail.com>
This commit is contained in:
@@ -13,16 +13,56 @@ This package is meant to provide a typed Express router for an OpenAPI spec. Bas
|
||||
2. In your plugin's `src/service/createRouter.ts`,
|
||||
|
||||
```ts
|
||||
import { ApiRouter } from `@backstage/backend-openapi-utils`;
|
||||
import spec from '../schema/openapi.generated';
|
||||
import { createOpenApiRouter } from '../schema/openapi.generated';
|
||||
// ...
|
||||
export function createRouter() {
|
||||
const router = Router() as ApiRouter<typeof spec>;
|
||||
// ...
|
||||
return router;
|
||||
const router = createOpenApiRouter();
|
||||
// add routes to router, it's just an express router.
|
||||
return router;
|
||||
}
|
||||
```
|
||||
|
||||
3. Add `@backstage/backend-openapi-utils` to your `package.json`'s `dependencies`.
|
||||
|
||||
Why do I need to add this to `dependencies`? If you check the `src/schema/openapi.generated.ts` file, we're creating a router stub for you with the `@backstage/backend-openapi-utils` package.
|
||||
|
||||
### Customization
|
||||
|
||||
If the out of the box `router` doesn't work, you can do the following,
|
||||
|
||||
```ts
|
||||
import { createOpenApiRouter } from '../schema/openapi.generated';
|
||||
// ...
|
||||
export function createRouter() {
|
||||
// See https://github.com/cdimascio/express-openapi-validator/wiki/Documentation for available options.
|
||||
const router = createOpenApiRouter(validatorOptions);
|
||||
// add routes to router, it's just an express router.
|
||||
return router;
|
||||
}
|
||||
```
|
||||
|
||||
If you need even more control -- say for example you wanted to update the spec at runtime -- you can do the following,
|
||||
|
||||
```ts
|
||||
import { spec } from '../schema/openapi.generated';
|
||||
import { createValidatedOpenApiRouter } from '@backstage/backend-openapi-utils';
|
||||
// ...
|
||||
export function createRouter() {
|
||||
// Update the spec here.
|
||||
const newSpec = { ...spec, myproperty123: 123 };
|
||||
|
||||
// See https://github.com/cdimascio/express-openapi-validator/wiki/Documentation for available options.
|
||||
const router = createValidatedOpenApiRouter<typeof newSpec>(
|
||||
newSpec,
|
||||
validatorOptions,
|
||||
);
|
||||
// add routes to router, it's just an express router.
|
||||
return router;
|
||||
}
|
||||
```
|
||||
|
||||
## INTERNAL
|
||||
|
||||
### Limitations
|
||||
|
||||
1. `as const` makes all fields `readonly`
|
||||
@@ -40,6 +80,10 @@ Router() as ApiRouter<DeepWriteable<typeof spec>>
|
||||
|
||||
## Future Work
|
||||
|
||||
### Runtime validation
|
||||
### Response 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/e0506af8fc54074a160fb91c83d6cae8172d3bb3/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.
|
||||
This is a murky ground and something that will take a while to gain adoption. For now, keep responses in the spec and at the type level, but will need to work to drive adoption of response validation.
|
||||
|
||||
### Common Error Format
|
||||
|
||||
With the new `createRouter` method, we can start to control error response formats for input and coercion errors.
|
||||
|
||||
@@ -7,10 +7,12 @@ 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 { middleware } from 'express-openapi-validator';
|
||||
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 { RequestHandler } from 'express';
|
||||
import type { ResponseObject } from 'openapi3-ts';
|
||||
import { Router } from 'express';
|
||||
import type { SchemaObject } from 'openapi3-ts';
|
||||
@@ -79,6 +81,15 @@ type CookieSchema<
|
||||
Method extends DocPathMethod<Doc, Path>,
|
||||
> = ParametersSchema<Doc, DocPath<Doc, Path>, Method, ImmutableCookieObject>;
|
||||
|
||||
// @public
|
||||
export function createValidatedOpenApiRouter<T extends RequiredDoc>(
|
||||
spec: T,
|
||||
options?: {
|
||||
validatorOptions?: Partial<Parameters<typeof middleware>['0']>;
|
||||
middleware?: RequestHandler[];
|
||||
},
|
||||
): ApiRouter<T>;
|
||||
|
||||
// @public (undocumented)
|
||||
type DiscriminateUnion<T, K extends keyof T, V extends T[K]> = Extract<
|
||||
T,
|
||||
@@ -117,15 +128,18 @@ 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;
|
||||
> = {
|
||||
[Index in keyof DocOperation<
|
||||
Doc,
|
||||
Path,
|
||||
Method
|
||||
>['parameters'] as FromNumberStringToNumber<Index>]: DocParameter<
|
||||
Doc,
|
||||
Path,
|
||||
Method,
|
||||
Index
|
||||
>;
|
||||
};
|
||||
|
||||
// @public
|
||||
type DocPath<
|
||||
@@ -204,6 +218,10 @@ interface DocRequestMatcher<
|
||||
// @public (undocumented)
|
||||
type Filter<T, U> = T extends U ? T : never;
|
||||
|
||||
// @public
|
||||
type FromNumberStringToNumber<NumberString extends string | number | symbol> =
|
||||
NumberString extends `${infer R extends number}` ? R : never;
|
||||
|
||||
// @public (undocumented)
|
||||
type FullMap<
|
||||
T extends {
|
||||
@@ -332,6 +350,7 @@ declare namespace internal {
|
||||
ImmutablePathObject,
|
||||
ImmutableSchemaObject,
|
||||
DocParameter,
|
||||
FromNumberStringToNumber,
|
||||
DocParameters,
|
||||
ParameterSchema,
|
||||
MapToSchema,
|
||||
@@ -425,17 +444,15 @@ type ParametersSchema<
|
||||
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'
|
||||
>
|
||||
>
|
||||
> = MapToSchema<
|
||||
Doc,
|
||||
FullMap<
|
||||
MapDiscriminatedUnion<
|
||||
Filter<ValueOf<DocParameters<Doc, Path, Method>>, FilterType>,
|
||||
'name'
|
||||
>
|
||||
: never;
|
||||
>
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
type PathDoc = Pick<ImmutableOpenAPIObject, 'paths'>;
|
||||
|
||||
@@ -24,17 +24,21 @@
|
||||
"postpack": "backstage-cli package postpack"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/cli": "workspace:^",
|
||||
"supertest": "^6.1.3"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/express-serve-static-core": "^4.17.5",
|
||||
"express": "^4.17.1",
|
||||
"express-openapi-validator": "^5.0.4",
|
||||
"express-promise-router": "^4.1.0",
|
||||
"json-schema-to-ts": "^2.6.2",
|
||||
"lodash": "^4.17.21",
|
||||
"openapi3-ts": "^3.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export default {
|
||||
openapi: '3.0.2',
|
||||
info: {
|
||||
title: 'Swagger Petstore - OpenAPI 3.0',
|
||||
description:
|
||||
"This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about\nSwagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.\n\nSome useful links:\n- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)\n- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)",
|
||||
termsOfService: 'http://swagger.io/terms/',
|
||||
contact: {
|
||||
email: 'apiteam@swagger.io',
|
||||
},
|
||||
license: {
|
||||
name: 'Apache 2.0',
|
||||
url: 'http://www.apache.org/licenses/LICENSE-2.0.html',
|
||||
},
|
||||
version: '1.0.17',
|
||||
},
|
||||
externalDocs: {
|
||||
description: 'Find out more about Swagger',
|
||||
url: 'http://swagger.io',
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: '/',
|
||||
},
|
||||
],
|
||||
paths: {
|
||||
'/pet/{petId}': {
|
||||
get: {
|
||||
summary: 'Find pet by ID',
|
||||
description: 'Returns a single pet',
|
||||
operationId: 'getPetById',
|
||||
parameters: [
|
||||
{
|
||||
name: 'petId',
|
||||
in: 'path',
|
||||
description: 'ID of pet to return',
|
||||
required: true,
|
||||
schema: {
|
||||
type: 'integer',
|
||||
format: 'int64',
|
||||
},
|
||||
},
|
||||
],
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'successful operation',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/Pet',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'400': {
|
||||
description: 'Invalid ID supplied',
|
||||
},
|
||||
'404': {
|
||||
description: 'Pet not found',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
schemas: {
|
||||
Pet: {
|
||||
required: ['name', 'photoUrls'],
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'integer',
|
||||
format: 'int64',
|
||||
example: 10,
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
example: 'doggie',
|
||||
},
|
||||
photoUrls: {
|
||||
type: 'array',
|
||||
xml: {
|
||||
wrapped: true,
|
||||
},
|
||||
items: {
|
||||
type: 'string',
|
||||
xml: {
|
||||
name: 'photoUrl',
|
||||
},
|
||||
},
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'pet status in the store',
|
||||
enum: ['available', 'pending', 'sold'],
|
||||
},
|
||||
},
|
||||
xml: {
|
||||
name: 'pet',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -23,3 +23,4 @@ import * as internal from './types';
|
||||
|
||||
export { internal };
|
||||
export type { ApiRouter } from './router';
|
||||
export { createValidatedOpenApiRouter } from './stub';
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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 { createValidatedOpenApiRouter } from './stub';
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
import singlePathSpec from './___fixtures__/single-path';
|
||||
|
||||
describe('createRouter', () => {
|
||||
it('does NOT override originalUrl and basePath after execution', async () => {
|
||||
expect.assertions(2);
|
||||
const router = createValidatedOpenApiRouter(singlePathSpec);
|
||||
router.get('/pet/:petId', (req, res) => {
|
||||
expect(req.baseUrl).toBe('/pet-store');
|
||||
expect(req.originalUrl).toBe(`/pet-store/pet/${req.params.petId}`);
|
||||
res.send('');
|
||||
});
|
||||
|
||||
const appRouter = express();
|
||||
appRouter.use('/pet-store', router);
|
||||
|
||||
await request(appRouter).get('/pet-store/pet/1');
|
||||
});
|
||||
|
||||
it('handles nested routes correctly (by treating plugin specs as full paths)', async () => {
|
||||
expect.assertions(1);
|
||||
const router = createValidatedOpenApiRouter(singlePathSpec);
|
||||
const routerGetFn = jest.fn();
|
||||
router.get('/pet/:petId', (_, res) => {
|
||||
routerGetFn();
|
||||
res.send('');
|
||||
});
|
||||
|
||||
const apiRouter = express.Router();
|
||||
apiRouter.use('/pet-store', router);
|
||||
const appRouter = express();
|
||||
appRouter.use('/api', apiRouter);
|
||||
|
||||
await request(appRouter).get('/api/pet-store/pet/1');
|
||||
expect(routerGetFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('handles coercing parameters correctly', async () => {
|
||||
expect.assertions(1);
|
||||
const router = createValidatedOpenApiRouter(singlePathSpec);
|
||||
router.get('/pet/:petId', (req, res) => {
|
||||
expect(typeof req.params.petId).toBe('integer');
|
||||
res.send('');
|
||||
});
|
||||
|
||||
const apiRouter = express.Router();
|
||||
apiRouter.use('/pet-store', router);
|
||||
const appRouter = express();
|
||||
appRouter.use('/api', apiRouter);
|
||||
|
||||
await request(appRouter).get('/api/pet-store/pet/1');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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 PromiseRouter from 'express-promise-router';
|
||||
import { ApiRouter } from './router';
|
||||
import { RequiredDoc } from './types';
|
||||
import {
|
||||
ErrorRequestHandler,
|
||||
RequestHandler,
|
||||
NextFunction,
|
||||
Request,
|
||||
Response,
|
||||
json,
|
||||
} from 'express';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { middleware as OpenApiValidator } from 'express-openapi-validator';
|
||||
|
||||
type PropertyOverrideRequest = Request & {
|
||||
[key: symbol]: string;
|
||||
};
|
||||
|
||||
const baseUrlSymbol = Symbol();
|
||||
const originalUrlSymbol = Symbol();
|
||||
|
||||
function validatorErrorTransformer(): ErrorRequestHandler {
|
||||
return (error: Error, _: Request, _2: Response, next: NextFunction) => {
|
||||
next(new InputError(error.message));
|
||||
};
|
||||
}
|
||||
|
||||
export function getDefaultRouterMiddleware() {
|
||||
return [json()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new OpenAPI router with some default middleware.
|
||||
* @param spec - Your OpenAPI spec imported as a JSON object.
|
||||
* @param validatorOptions - `openapi-express-validator` options to override the defaults.
|
||||
* @returns A new express router with validation middleware.
|
||||
* @public
|
||||
*/
|
||||
export function createValidatedOpenApiRouter<T extends RequiredDoc>(
|
||||
spec: T,
|
||||
options?: {
|
||||
validatorOptions?: Partial<Parameters<typeof OpenApiValidator>['0']>;
|
||||
middleware?: RequestHandler[];
|
||||
},
|
||||
) {
|
||||
const router = PromiseRouter() as ApiRouter<typeof spec>;
|
||||
router.use(options?.middleware || getDefaultRouterMiddleware());
|
||||
|
||||
/**
|
||||
* Middleware to setup the routing for OpenApiValidator. OpenApiValidator expects `req.originalUrl`
|
||||
* and `req.baseUrl` to be the full path. We adjust them here to basically be nothing and then
|
||||
* revive the old values in the last function in this method. We could instead update `req.path`
|
||||
* but that might affect the routing and I'd rather not.
|
||||
*
|
||||
* TODO: I opened https://github.com/cdimascio/express-openapi-validator/issues/843
|
||||
* to track this on the middleware side, but there was a similar ticket, https://github.com/cdimascio/express-openapi-validator/issues/113
|
||||
* that has had minimal activity. If that changes, update this to use a new option on their side.
|
||||
*/
|
||||
router.use((req: Request, _, next) => {
|
||||
/**
|
||||
* Express typings are weird. They don't recognize PropertyOverrideRequest as a valid
|
||||
* Request child and try to overload as PathParams. Just cast it here, since we know
|
||||
* what we're doing.
|
||||
*/
|
||||
const customRequest = req as PropertyOverrideRequest;
|
||||
customRequest[baseUrlSymbol] = customRequest.baseUrl;
|
||||
customRequest.baseUrl = '';
|
||||
customRequest[originalUrlSymbol] = customRequest.originalUrl;
|
||||
customRequest.originalUrl = customRequest.url;
|
||||
next();
|
||||
});
|
||||
|
||||
// TODO: Handle errors by converting from OpenApiValidator errors to known @backstage/errors errors.
|
||||
router.use(
|
||||
OpenApiValidator({
|
||||
validateRequests: {
|
||||
coerceTypes: false,
|
||||
allowUnknownQueryParameters: false,
|
||||
},
|
||||
ignoreUndocumented: true,
|
||||
validateResponses: false,
|
||||
...options?.validatorOptions,
|
||||
apiSpec: spec as any,
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Revert `req.baseUrl` and `req.originalUrl` changes. This ensures that any further usage
|
||||
* of these variables will be unchanged.
|
||||
*/
|
||||
router.use((req: Request, _, next) => {
|
||||
const customRequest = req as PropertyOverrideRequest;
|
||||
customRequest.baseUrl = customRequest[baseUrlSymbol];
|
||||
customRequest.originalUrl = customRequest[originalUrlSymbol];
|
||||
delete customRequest[baseUrlSymbol];
|
||||
delete customRequest[originalUrlSymbol];
|
||||
next();
|
||||
});
|
||||
|
||||
// Any errors from the middleware get through here.
|
||||
router.use(validatorErrorTransformer());
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
PathTemplate,
|
||||
RequiredDoc,
|
||||
SchemaRef,
|
||||
ValueOf,
|
||||
} from './common';
|
||||
import { FromSchema, JSONSchema7 } from 'json-schema-to-ts';
|
||||
|
||||
@@ -60,6 +61,14 @@ export type DocParameter<
|
||||
: never
|
||||
: DocOperation<Doc, Path, Method>['parameters'][Parameter];
|
||||
|
||||
/**
|
||||
* Helper to convert from string to number, used to index arrays and pull out just the indices in the array.
|
||||
* @public
|
||||
*/
|
||||
export type FromNumberStringToNumber<
|
||||
NumberString extends string | number | symbol,
|
||||
> = NumberString extends `${infer R extends number}` ? R : never;
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -67,15 +76,18 @@ 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;
|
||||
> = {
|
||||
[Index in keyof DocOperation<
|
||||
Doc,
|
||||
Path,
|
||||
Method
|
||||
>['parameters'] as FromNumberStringToNumber<Index>]: DocParameter<
|
||||
Doc,
|
||||
Path,
|
||||
Method,
|
||||
Index
|
||||
>;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -111,17 +123,15 @@ export type ParametersSchema<
|
||||
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'
|
||||
>
|
||||
>
|
||||
> = MapToSchema<
|
||||
Doc,
|
||||
FullMap<
|
||||
MapDiscriminatedUnion<
|
||||
Filter<ValueOf<DocParameters<Doc, Path, Method>>, FilterType>,
|
||||
'name'
|
||||
>
|
||||
: never;
|
||||
>
|
||||
>;
|
||||
|
||||
/**
|
||||
* @public
|
||||
|
||||
Reference in New Issue
Block a user