mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Versioned HTTP] Add response runtime and type-level validation (#153011)
## Summary This PR restructures the `validation` object on the `.addVersion` method by: 1. Adding an `in` object for the `body`, `params` and `query` validations 2. Adding `out` so that we can have both runtime and TS type checking our responses To reviewers: easiest way to interpret these changes is to read the `example.ts` file. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
eb60253fd1
commit
3c7bf58405
9 changed files with 103 additions and 87 deletions
|
@ -13,9 +13,9 @@ import type { ResponseHeaders } from './headers';
|
|||
* HTTP response parameters
|
||||
* @public
|
||||
*/
|
||||
export interface HttpResponseOptions {
|
||||
export interface HttpResponseOptions<T extends HttpResponsePayload | ResponseError = any> {
|
||||
/** HTTP message to send to the client */
|
||||
body?: HttpResponsePayload;
|
||||
body?: T;
|
||||
/** HTTP Headers with additional information about response */
|
||||
headers?: ResponseHeaders;
|
||||
/** Bypass the default error formatting */
|
||||
|
|
|
@ -27,14 +27,18 @@ export interface KibanaSuccessResponseFactory {
|
|||
* Status code: `200`.
|
||||
* @param options - {@link HttpResponseOptions} configures HTTP response body & headers.
|
||||
*/
|
||||
ok(options?: HttpResponseOptions): IKibanaResponse;
|
||||
ok<T extends HttpResponsePayload | ResponseError = any>(
|
||||
options?: HttpResponseOptions<T>
|
||||
): IKibanaResponse<T>;
|
||||
|
||||
/**
|
||||
* The request has been accepted for processing.
|
||||
* Status code: `202`.
|
||||
* @param options - {@link HttpResponseOptions} configures HTTP response body & headers.
|
||||
*/
|
||||
accepted(options?: HttpResponseOptions): IKibanaResponse;
|
||||
accepted<T extends HttpResponsePayload | ResponseError = any>(
|
||||
options?: HttpResponseOptions<T>
|
||||
): IKibanaResponse<T>;
|
||||
|
||||
/**
|
||||
* The server has successfully fulfilled the request and that there is no additional content to send in the response payload body.
|
||||
|
|
|
@ -28,13 +28,18 @@ const versionedRoute = versionedRouter
|
|||
{
|
||||
version: '1',
|
||||
validate: {
|
||||
query: schema.object({
|
||||
name: schema.maybe(schema.string({ minLength: 2, maxLength: 50 })),
|
||||
}),
|
||||
params: schema.object({
|
||||
id: schema.maybe(schema.string({ minLength: 10, maxLength: 13 })),
|
||||
}),
|
||||
body: schema.object({ foo: schema.string() }),
|
||||
request: {
|
||||
query: schema.object({
|
||||
name: schema.maybe(schema.string({ minLength: 2, maxLength: 50 })),
|
||||
}),
|
||||
params: schema.object({
|
||||
id: schema.maybe(schema.string({ minLength: 10, maxLength: 13 })),
|
||||
}),
|
||||
body: schema.object({ foo: schema.string() }),
|
||||
},
|
||||
response: {
|
||||
body: schema.object({ foo: schema.string() }),
|
||||
},
|
||||
},
|
||||
},
|
||||
async (ctx, req, res) => {
|
||||
|
@ -47,13 +52,18 @@ const versionedRoute = versionedRouter
|
|||
{
|
||||
version: '2',
|
||||
validate: {
|
||||
query: schema.object({
|
||||
name: schema.maybe(schema.string({ minLength: 2, maxLength: 50 })),
|
||||
}),
|
||||
params: schema.object({
|
||||
id: schema.maybe(schema.string({ minLength: 10, maxLength: 13 })),
|
||||
}),
|
||||
body: schema.object({ fooString: schema.string() }),
|
||||
request: {
|
||||
query: schema.object({
|
||||
name: schema.maybe(schema.string({ minLength: 2, maxLength: 50 })),
|
||||
}),
|
||||
params: schema.object({
|
||||
id: schema.maybe(schema.string({ minLength: 10, maxLength: 13 })),
|
||||
}),
|
||||
body: schema.object({ fooString: schema.string() }),
|
||||
},
|
||||
response: {
|
||||
body: schema.object({ fooName: schema.string() }),
|
||||
},
|
||||
},
|
||||
},
|
||||
async (ctx, req, res) => {
|
||||
|
@ -66,13 +76,18 @@ const versionedRoute = versionedRouter
|
|||
{
|
||||
version: '3',
|
||||
validate: {
|
||||
query: schema.object({
|
||||
name: schema.maybe(schema.string({ minLength: 2, maxLength: 50 })),
|
||||
}),
|
||||
params: schema.object({
|
||||
id: schema.maybe(schema.string({ minLength: 10, maxLength: 13 })),
|
||||
}),
|
||||
body: schema.object({ fooString: schema.string({ minLength: 0, maxLength: 1000 }) }),
|
||||
request: {
|
||||
query: schema.object({
|
||||
name: schema.maybe(schema.string({ minLength: 2, maxLength: 50 })),
|
||||
}),
|
||||
params: schema.object({
|
||||
id: schema.maybe(schema.string({ minLength: 10, maxLength: 13 })),
|
||||
}),
|
||||
body: schema.object({ fooString: schema.string({ minLength: 0, maxLength: 1000 }) }),
|
||||
},
|
||||
response: {
|
||||
body: schema.object({ fooName: schema.string() }),
|
||||
},
|
||||
},
|
||||
},
|
||||
async (ctx, req, res) => {
|
||||
|
|
|
@ -6,14 +6,18 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Type } from '@kbn/config-schema';
|
||||
import type { WithRequiredProperty } from '@kbn/utility-types';
|
||||
import type {
|
||||
IRouter,
|
||||
RouteConfig,
|
||||
RouteMethod,
|
||||
RequestHandler,
|
||||
IKibanaResponse,
|
||||
RouteConfigOptions,
|
||||
RouteValidatorFullConfig,
|
||||
RequestHandlerContextBase,
|
||||
RouteConfigOptions,
|
||||
RouteValidationFunction,
|
||||
} from '@kbn/core-http-server';
|
||||
|
||||
type RqCtx = RequestHandlerContextBase;
|
||||
|
@ -39,53 +43,8 @@ export interface CreateVersionedRouterArgs<Ctx extends RqCtx = RqCtx> {
|
|||
/**
|
||||
* This interface is the starting point for creating versioned routers and routes
|
||||
*
|
||||
* @example
|
||||
* const versionedRouter = vtk.createVersionedRouter({ router });
|
||||
* @example see ./example.ts
|
||||
*
|
||||
* ```ts
|
||||
* const versionedRoute = versionedRouter
|
||||
* .post({
|
||||
* path: '/api/my-app/foo/{id?}',
|
||||
* options: { timeout: { payload: 60000 }, access: 'public' },
|
||||
* })
|
||||
* .addVersion(
|
||||
* {
|
||||
* version: '1',
|
||||
* validate: {
|
||||
* query: schema.object({
|
||||
* name: schema.maybe(schema.string({ minLength: 2, maxLength: 50 })),
|
||||
* }),
|
||||
* params: schema.object({
|
||||
* id: schema.maybe(schema.string({ minLength: 10, maxLength: 13 })),
|
||||
* }),
|
||||
* body: schema.object({ foo: schema.string() }),
|
||||
* },
|
||||
* },
|
||||
* async (ctx, req, res) => {
|
||||
* await ctx.fooService.create(req.body.foo, req.params.id, req.query.name);
|
||||
* return res.ok({ body: { foo: req.body.foo } });
|
||||
* }
|
||||
* )
|
||||
* // BREAKING CHANGE: { foo: string } => { fooString: string } in body
|
||||
* .addVersion(
|
||||
* {
|
||||
* version: '2',
|
||||
* validate: {
|
||||
* query: schema.object({
|
||||
* name: schema.maybe(schema.string({ minLength: 2, maxLength: 50 })),
|
||||
* }),
|
||||
* params: schema.object({
|
||||
* id: schema.maybe(schema.string({ minLength: 10, maxLength: 13 })),
|
||||
* }),
|
||||
* body: schema.object({ fooString: schema.string() }),
|
||||
* },
|
||||
* },
|
||||
* async (ctx, req, res) => {
|
||||
* await ctx.fooService.create(req.body.fooString, req.params.id, req.query.name);
|
||||
* return res.ok({ body: { fooName: req.body.fooString } });
|
||||
* }
|
||||
* )
|
||||
* ```
|
||||
* @experimental
|
||||
*/
|
||||
export interface VersionHTTPToolkit {
|
||||
|
@ -100,13 +59,6 @@ export interface VersionHTTPToolkit {
|
|||
): VersionedRouter<Ctx>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an input property from optional to required. Needed for making RouteConfigOptions['access'] required.
|
||||
*/
|
||||
type WithRequiredProperty<Type, Key extends keyof Type> = Type & {
|
||||
[Property in Key]-?: Type[Property];
|
||||
};
|
||||
|
||||
/**
|
||||
* Versioned route access flag, required
|
||||
* - '/api/foo' is 'public'
|
||||
|
@ -154,12 +106,39 @@ export interface VersionedRouter<Ctx extends RqCtx = RqCtx> {
|
|||
options: VersionedRouteRegistrar<'options', Ctx>;
|
||||
}
|
||||
|
||||
/** @experimental */
|
||||
export type RequestValidation<P, Q, B> = RouteValidatorFullConfig<P, Q, B>;
|
||||
|
||||
/** @experimental */
|
||||
export interface ResponseValidation<R> {
|
||||
body: RouteValidationFunction<R> | Type<R>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Versioned route validation
|
||||
* @experimental
|
||||
*/
|
||||
interface FullValidationConfig<P, Q, B, R> {
|
||||
/**
|
||||
* Validation to run against route inputs: params, query and body
|
||||
* @experimental
|
||||
*/
|
||||
request?: RequestValidation<P, Q, B>;
|
||||
/**
|
||||
* Validation to run against route output
|
||||
* @note This validation is only intended to run in development. Do not use this
|
||||
* for setting default values!
|
||||
* @experimental
|
||||
*/
|
||||
response?: ResponseValidation<R>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for a versioned route. Probably needs a lot more options like sunsetting
|
||||
* of an endpoint etc.
|
||||
* @experimental
|
||||
*/
|
||||
export interface AddVersionOpts<P, Q, B, Method extends RouteMethod = RouteMethod> {
|
||||
export interface AddVersionOpts<P, Q, B, R> {
|
||||
/**
|
||||
* Version to assign to this route
|
||||
* @experimental
|
||||
|
@ -169,7 +148,7 @@ export interface AddVersionOpts<P, Q, B, Method extends RouteMethod = RouteMetho
|
|||
* Validation for this version of a route
|
||||
* @experimental
|
||||
*/
|
||||
validate: false | RouteValidatorFullConfig<P, Q, B>;
|
||||
validate: false | FullValidationConfig<P, Q, B, R>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -187,8 +166,8 @@ export interface VersionedRoute<
|
|||
* @returns A versioned route, allows for fluent chaining of version declarations
|
||||
* @experimental
|
||||
*/
|
||||
addVersion<P, Q, B>(
|
||||
opts: AddVersionOpts<P, Q, B>,
|
||||
handler: RequestHandler<P, Q, B, Ctx>
|
||||
addVersion<P, Q, B, R>(
|
||||
options: AddVersionOpts<P, Q, B, R>,
|
||||
handler: (...params: Parameters<RequestHandler<P, Q, B, Ctx>>) => Promise<IKibanaResponse<R>>
|
||||
): VersionedRoute<Method, Ctx>;
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
"kbn_references": [
|
||||
"@kbn/config-schema",
|
||||
"@kbn/core-http-server",
|
||||
"@kbn/utility-types",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -140,3 +140,14 @@ export type DeepPartialObject<T> = { [P in keyof T]+?: DeepPartial<T[P]> };
|
|||
export type { DotObject, DedotObject } from './src/dot';
|
||||
|
||||
export type ArrayElement<A> = A extends ReadonlyArray<infer T> ? T : never;
|
||||
|
||||
/**
|
||||
* Takes a type and makes selected properties required.
|
||||
*
|
||||
* @example
|
||||
* interface Foo { bar?: string }
|
||||
* const foo: WithRequiredProperty<Foo, 'bar'> = { bar: 'baz' }
|
||||
*/
|
||||
export type WithRequiredProperty<Type, Key extends keyof Type> = Omit<Type, Key> & {
|
||||
[Property in Key]-?: Type[Property];
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { HttpResponseOptions } from '@kbn/core-http-server';
|
||||
import { DataView, DataViewField } from '../../../common';
|
||||
import { SERVICE_KEY_LEGACY, SERVICE_KEY_TYPE, SERVICE_KEY } from '../../constants';
|
||||
|
||||
|
@ -15,7 +16,11 @@ interface ResponseFormatterArgs {
|
|||
dataView: DataView;
|
||||
}
|
||||
|
||||
export const responseFormatter = ({ serviceKey, fields, dataView }: ResponseFormatterArgs) => {
|
||||
export const responseFormatter = ({
|
||||
serviceKey,
|
||||
fields,
|
||||
dataView,
|
||||
}: ResponseFormatterArgs): HttpResponseOptions => {
|
||||
const response = {
|
||||
body: {
|
||||
fields: fields.map((field) => field.toSpec()),
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
"@kbn/config-schema",
|
||||
"@kbn/utility-types-jest",
|
||||
"@kbn/safer-lodash-set",
|
||||
"@kbn/core-http-server",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -75,9 +75,9 @@ export function initRoutes(
|
|||
}, Promise.resolve<ConcreteTaskInstance[] | undefined>(undefined));
|
||||
|
||||
return res.ok({
|
||||
body: await new Promise((resolve) => {
|
||||
body: await new Promise<PerfResult>((resolve) => {
|
||||
setTimeout(() => {
|
||||
performanceApi.endCapture().then((perf: PerfResult) => resolve(perf));
|
||||
performanceApi.endCapture().then((perf) => resolve(perf));
|
||||
}, durationInSeconds * 1000 + 10000 /* wait extra 10s to drain queue */);
|
||||
}),
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue