diff --git a/packages/core/http/core-http-server/src/router/response.ts b/packages/core/http/core-http-server/src/router/response.ts index 41f7d84f6b5d1..07ec226e8c3a9 100644 --- a/packages/core/http/core-http-server/src/router/response.ts +++ b/packages/core/http/core-http-server/src/router/response.ts @@ -13,9 +13,9 @@ import type { ResponseHeaders } from './headers'; * HTTP response parameters * @public */ -export interface HttpResponseOptions { +export interface HttpResponseOptions { /** 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 */ diff --git a/packages/core/http/core-http-server/src/router/response_factory.ts b/packages/core/http/core-http-server/src/router/response_factory.ts index 9f9be08dc2cb7..34303103732a2 100644 --- a/packages/core/http/core-http-server/src/router/response_factory.ts +++ b/packages/core/http/core-http-server/src/router/response_factory.ts @@ -27,14 +27,18 @@ export interface KibanaSuccessResponseFactory { * Status code: `200`. * @param options - {@link HttpResponseOptions} configures HTTP response body & headers. */ - ok(options?: HttpResponseOptions): IKibanaResponse; + ok( + options?: HttpResponseOptions + ): IKibanaResponse; /** * The request has been accepted for processing. * Status code: `202`. * @param options - {@link HttpResponseOptions} configures HTTP response body & headers. */ - accepted(options?: HttpResponseOptions): IKibanaResponse; + accepted( + options?: HttpResponseOptions + ): IKibanaResponse; /** * The server has successfully fulfilled the request and that there is no additional content to send in the response payload body. diff --git a/packages/core/versioning/core-version-http-server/src/example.ts b/packages/core/versioning/core-version-http-server/src/example.ts index b63c75e86a562..fc0a9b3e39aec 100644 --- a/packages/core/versioning/core-version-http-server/src/example.ts +++ b/packages/core/versioning/core-version-http-server/src/example.ts @@ -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) => { diff --git a/packages/core/versioning/core-version-http-server/src/version_http_toolkit.ts b/packages/core/versioning/core-version-http-server/src/version_http_toolkit.ts index 7d8dd7765e476..1488184aea7e0 100644 --- a/packages/core/versioning/core-version-http-server/src/version_http_toolkit.ts +++ b/packages/core/versioning/core-version-http-server/src/version_http_toolkit.ts @@ -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 { /** * 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; } -/** - * Converts an input property from optional to required. Needed for making RouteConfigOptions['access'] required. - */ -type WithRequiredProperty = Type & { - [Property in Key]-?: Type[Property]; -}; - /** * Versioned route access flag, required * - '/api/foo' is 'public' @@ -154,12 +106,39 @@ export interface VersionedRouter { options: VersionedRouteRegistrar<'options', Ctx>; } +/** @experimental */ +export type RequestValidation = RouteValidatorFullConfig; + +/** @experimental */ +export interface ResponseValidation { + body: RouteValidationFunction | Type; +} + +/** + * Versioned route validation + * @experimental + */ +interface FullValidationConfig { + /** + * Validation to run against route inputs: params, query and body + * @experimental + */ + request?: RequestValidation; + /** + * 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; +} + /** * Options for a versioned route. Probably needs a lot more options like sunsetting * of an endpoint etc. * @experimental */ -export interface AddVersionOpts { +export interface AddVersionOpts { /** * Version to assign to this route * @experimental @@ -169,7 +148,7 @@ export interface AddVersionOpts; + validate: false | FullValidationConfig; } /** @@ -187,8 +166,8 @@ export interface VersionedRoute< * @returns A versioned route, allows for fluent chaining of version declarations * @experimental */ - addVersion( - opts: AddVersionOpts, - handler: RequestHandler + addVersion( + options: AddVersionOpts, + handler: (...params: Parameters>) => Promise> ): VersionedRoute; } diff --git a/packages/core/versioning/core-version-http-server/tsconfig.json b/packages/core/versioning/core-version-http-server/tsconfig.json index fa73dc9c397bf..d0ff9556e176f 100644 --- a/packages/core/versioning/core-version-http-server/tsconfig.json +++ b/packages/core/versioning/core-version-http-server/tsconfig.json @@ -13,6 +13,7 @@ "kbn_references": [ "@kbn/config-schema", "@kbn/core-http-server", + "@kbn/utility-types", ], "exclude": [ "target/**/*", diff --git a/packages/kbn-utility-types/index.ts b/packages/kbn-utility-types/index.ts index 52f707f2bc0d4..3aeaf04083c66 100644 --- a/packages/kbn-utility-types/index.ts +++ b/packages/kbn-utility-types/index.ts @@ -140,3 +140,14 @@ export type DeepPartialObject = { [P in keyof T]+?: DeepPartial }; export type { DotObject, DedotObject } from './src/dot'; export type ArrayElement = A extends ReadonlyArray ? T : never; + +/** + * Takes a type and makes selected properties required. + * + * @example + * interface Foo { bar?: string } + * const foo: WithRequiredProperty = { bar: 'baz' } + */ +export type WithRequiredProperty = Omit & { + [Property in Key]-?: Type[Property]; +}; diff --git a/src/plugins/data_views/server/rest_api_routes/runtime_fields/response_formatter.ts b/src/plugins/data_views/server/rest_api_routes/runtime_fields/response_formatter.ts index 83dc31b4b713d..8bb9f58fb8668 100644 --- a/src/plugins/data_views/server/rest_api_routes/runtime_fields/response_formatter.ts +++ b/src/plugins/data_views/server/rest_api_routes/runtime_fields/response_formatter.ts @@ -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()), diff --git a/src/plugins/data_views/tsconfig.json b/src/plugins/data_views/tsconfig.json index ae13beea68d45..94fe86cb31321 100644 --- a/src/plugins/data_views/tsconfig.json +++ b/src/plugins/data_views/tsconfig.json @@ -27,6 +27,7 @@ "@kbn/config-schema", "@kbn/utility-types-jest", "@kbn/safer-lodash-set", + "@kbn/core-http-server", ], "exclude": [ "target/**/*", diff --git a/x-pack/test/plugin_api_perf/plugins/task_manager_performance/server/init_routes.ts b/x-pack/test/plugin_api_perf/plugins/task_manager_performance/server/init_routes.ts index 845bd4cb210e2..7d5118c5d93d3 100644 --- a/x-pack/test/plugin_api_perf/plugins/task_manager_performance/server/init_routes.ts +++ b/x-pack/test/plugin_api_perf/plugins/task_manager_performance/server/init_routes.ts @@ -75,9 +75,9 @@ export function initRoutes( }, Promise.resolve(undefined)); return res.ok({ - body: await new Promise((resolve) => { + body: await new Promise((resolve) => { setTimeout(() => { - performanceApi.endCapture().then((perf: PerfResult) => resolve(perf)); + performanceApi.endCapture().then((perf) => resolve(perf)); }, durationInSeconds * 1000 + 10000 /* wait extra 10s to drain queue */); }), });