From 17285223e8ac89af41133d4fbe37b3affc53bba9 Mon Sep 17 00:00:00 2001 From: restrry Date: Tue, 30 Jul 2019 14:09:11 +0200 Subject: [PATCH] add TSDoc --- src/core/MIGRATION.md | 158 ++++++++++++++++++ .../server/elasticsearch/cluster_client.ts | 5 +- src/core/server/elasticsearch/index.ts | 2 +- src/core/server/http/auth_headers_storage.ts | 12 +- src/core/server/http/auth_state_storage.ts | 41 ++++- src/core/server/http/base_path_service.ts | 9 +- .../server/http/cookie_session_storage.ts | 16 ++ src/core/server/http/http_server.ts | 44 +++-- src/core/server/http/index.ts | 13 ++ src/core/server/http/router/headers.ts | 43 ++++- src/core/server/http/router/index.ts | 25 ++- src/core/server/http/router/request.ts | 20 ++- src/core/server/http/router/response.ts | 45 ++--- src/core/server/http/router/route.ts | 43 ++++- src/core/server/http/router/router.ts | 72 +++++++- src/core/server/index.ts | 18 +- 16 files changed, 477 insertions(+), 89 deletions(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 295602b6e14866..400890da3fe764 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -1115,6 +1115,164 @@ class MyPlugin { } ``` +### Handle HTTP request with New Platform HTTP Service +Kibana HTTP Service provides own abstraction for work with HTTP stack. +Plugins don't have direct access to `hapi` server and its primitives anymore. Moreover, +you shouldn't rely on the fact that HTTP Service uses one or another library under the hood, there are high chances that we can switch to another stack at all. If HTTP Service +lacks needed functionality we happy to discuss and support your needs. + +To handle an incoming request in your plugin you should: +- create Router instance. Use `plugin id` as a prefix path segment for your routes. +```ts +import { Router } from 'src/core/server'; +const router = new Router('my-app'); +``` + +- use `@kbn/config-schema` package to create a schema to validate `request params`, `query`, `body` if an incoming request used them to pass additional details. Every incoming request will be validated against the created schema. If validation failed, the request is rejected with `400` status and `Bad request` error. +To opt out of validating the request, specify `false`. +```ts +import { schema, TypeOf } from '@kbn/config-schema'; +const validate = { + params: schema.object({ + id: schema.string(), + }), +}; +``` + +- declare a function to respond to incoming request. +The function will receive `request` object containing request details: url, headers, matched route, as well as validated `params`, `query`, `body`. +And `createResponse` object instructing HTTP server to create HTTP response with information sent back to the client as the response body, headers, and HTTP status. +Unlike, `hapi` route handler in the Legacy platform, any exception raised during the handler call will generate `500 Server error` response and log error details for further investigation. +```ts + const handler = async (request: KibanaRequest, createResponse: ResponseFactory) => { + const data = await findObject(request.params.id); + // creates a command to respond with 'not found' error + if (!data) return createResponse.notFound(); + // creates a command to send found data to the client and set response headers + return createResponse.ok(data, { + headers: { + 'content-type': 'application/json' + } + }); + } +``` + +- register route handler for GET request to 'my-app/path/{id}' path +```ts +import { schema, TypeOf } from '@kbn/config-schema'; +import { Router } from 'src/core/server'; +const router = new Router('my-app'); + +const validate = { + params: schema.object({ + id: schema.string(), + }), +}; + +router.get( + { + path: 'path/{id}', + validate + }, + async (request, createResponse) => { + const data = await findObject(request.params.id); + if (!data) return createResponse.notFound(); + return createResponse.ok(data, { + headers: { + 'content-type': 'application/json' + } + }); + } +); +``` +#### What data I can send back with a response? +You have several options to create a response utilizing `createResponse`: +1. create a successful response. Supported types of response body are: +- `undefined`, no content to send. +- `string`, send text +- `JSON`, send JSON object, HTTP server will throw if given object is not valid (has circular references, for example) +- `Stream` send data stream +- `Buffer` send binary stream +```js +return response.ok(undefined); +return response.ok('ack'); +return response.ok({ id: '1' }); +return response.ok(Buffer.from(...);); + +const stream = new Stream.PassThrough(); +fs.createReadStream('./file').pipe(stream); +return res.ok(stream); +``` +HTTP headers are configurable via response factory parameter `options`. + +```js +return response.ok({ id: '1' }, { + headers: { + 'content-type': 'application/json' + } +}); +``` +2. create redirect response. Redirection URL is configures via 'Location' header. +```js +return response.redirected('The document has moved', { + headers: { + location: '/new-url', + }, +}); +``` +3. create error response. You may pass an error message to the client, where error message can be: +- `string` send message text +- `Error` send the message text of given Error object. +- `{ message: string | Error, meta: {data: Record, ...} }` - send message text and attach additional error metadata. +```js +return response.unauthorized('User has no access to the requested resource.', { + headers: { + 'WWW-Authenticate': 'challenge', + } +}) +return response.badRequest(); +return response.badRequest('validation error'); + +try { + // ... +} catch(error){ + return response.badRequest(error); +} + +return response.badRequest({ + message: 'validation error', + meta: { + data: { + requestBody: request.body, + failedFields: validationResult + }, + } +}); + +try { + // ... +} catch(error){ + return response.badRequest({ + message: error, + meta: { + data: { + requestBody: request.body, + }, + } +}); +} + +``` +4. create a custom response. It might happen that `response factory` doesn't cover your use case and you want to specify HTTP response status code as well. +```js +return response.custom('ok', { + statusCode: 201, + headers: { + location: '/created-url' + } +}) +``` + ### Mock core services in tests Core services already provide mocks to simplify testing and make sure plugins always rely on valid public contracts. ```typescript diff --git a/src/core/server/elasticsearch/cluster_client.ts b/src/core/server/elasticsearch/cluster_client.ts index c8500499d97029..aa40010bf3b1ac 100644 --- a/src/core/server/elasticsearch/cluster_client.ts +++ b/src/core/server/elasticsearch/cluster_client.ts @@ -18,10 +18,9 @@ */ import { Client } from 'elasticsearch'; import { get } from 'lodash'; -import { Request } from 'hapi'; import { ElasticsearchErrorHelpers } from './errors'; -import { GetAuthHeaders, isRealRequest } from '../http'; +import { GetAuthHeaders, isRealRequest, LegacyRequest } from '../http'; import { filterHeaders, Headers, KibanaRequest, ensureRawRequest } from '../http/router'; import { Logger } from '../logging'; import { @@ -36,8 +35,6 @@ import { ScopedClusterClient } from './scoped_cluster_client'; * @public */ -export type LegacyRequest = Request; - const noop = () => undefined; /** * The set of options that defines how API call should be made and result be diff --git a/src/core/server/elasticsearch/index.ts b/src/core/server/elasticsearch/index.ts index 1d439dfba49e9e..f732f9e39b9e30 100644 --- a/src/core/server/elasticsearch/index.ts +++ b/src/core/server/elasticsearch/index.ts @@ -18,7 +18,7 @@ */ export { ElasticsearchServiceSetup, ElasticsearchService } from './elasticsearch_service'; -export { CallAPIOptions, ClusterClient, FakeRequest, LegacyRequest } from './cluster_client'; +export { CallAPIOptions, ClusterClient, FakeRequest } from './cluster_client'; export { ScopedClusterClient, Headers, APICaller } from './scoped_cluster_client'; export { ElasticsearchClientConfig } from './elasticsearch_client_config'; export { config } from './elasticsearch_config'; diff --git a/src/core/server/http/auth_headers_storage.ts b/src/core/server/http/auth_headers_storage.ts index bc3b55b3718c01..469e194a61fed2 100644 --- a/src/core/server/http/auth_headers_storage.ts +++ b/src/core/server/http/auth_headers_storage.ts @@ -16,19 +16,21 @@ * specific language governing permissions and limitations * under the License. */ -import { Request } from 'hapi'; -import { KibanaRequest, ensureRawRequest } from './router'; +import { KibanaRequest, ensureRawRequest, LegacyRequest } from './router'; import { AuthHeaders } from './lifecycle/auth'; /** * Get headers to authenticate a user against Elasticsearch. + * @param request {@link KibanaRequest} - an incoming request. + * @return authentication headers {@link AuthHeaders} for - an incoming request. * @public * */ -export type GetAuthHeaders = (request: KibanaRequest | Request) => AuthHeaders | undefined; +export type GetAuthHeaders = (request: KibanaRequest | LegacyRequest) => AuthHeaders | undefined; +/** @internal */ export class AuthHeadersStorage { - private authHeadersCache = new WeakMap(); - public set = (request: KibanaRequest | Request, headers: AuthHeaders) => { + private authHeadersCache = new WeakMap(); + public set = (request: KibanaRequest | LegacyRequest, headers: AuthHeaders) => { this.authHeadersCache.set(ensureRawRequest(request), headers); }; public get: GetAuthHeaders = request => { diff --git a/src/core/server/http/auth_state_storage.ts b/src/core/server/http/auth_state_storage.ts index 79fd9ed64f3b54..059dc7f3803514 100644 --- a/src/core/server/http/auth_state_storage.ts +++ b/src/core/server/http/auth_state_storage.ts @@ -16,22 +16,51 @@ * specific language governing permissions and limitations * under the License. */ -import { Request } from 'hapi'; -import { KibanaRequest, ensureRawRequest } from './router'; +import { ensureRawRequest, KibanaRequest, LegacyRequest } from './router'; +/** + * Status indicating an outcome of the authentication. + * @public + */ export enum AuthStatus { + /** + * `auth` interceptor successfully authenticated a user + */ authenticated = 'authenticated', + /** + * `auth` interceptor failed user authentication + */ unauthenticated = 'unauthenticated', + /** + * `auth` interceptor has not been registered + */ unknown = 'unknown', } +/** + * Get authentication state for a request. Returned by `auth` interceptor. + * @param request {@link KibanaRequest} - an incoming request. + * @public + */ +export type GetAuthState = ( + request: KibanaRequest | LegacyRequest +) => { status: AuthStatus; state: unknown }; + +/** + * Return authentication status for a request. + * @param request {@link KibanaRequest} - an incoming request. + * @public + */ +export type IsAuthenticated = (request: KibanaRequest | LegacyRequest) => boolean; + +/** @internal */ export class AuthStateStorage { - private readonly storage = new WeakMap(); + private readonly storage = new WeakMap(); constructor(private readonly canBeAuthenticated: () => boolean) {} - public set = (request: KibanaRequest | Request, state: unknown) => { + public set = (request: KibanaRequest | LegacyRequest, state: unknown) => { this.storage.set(ensureRawRequest(request), state); }; - public get = (request: KibanaRequest | Request) => { + public get: GetAuthState = request => { const key = ensureRawRequest(request); const state = this.storage.get(key); const status: AuthStatus = this.storage.has(key) @@ -42,7 +71,7 @@ export class AuthStateStorage { return { status, state }; }; - public isAuthenticated = (request: KibanaRequest | Request) => { + public isAuthenticated: IsAuthenticated = request => { return this.get(request).status === AuthStatus.authenticated; }; } diff --git a/src/core/server/http/base_path_service.ts b/src/core/server/http/base_path_service.ts index df189d29f2f594..951463a2c9919f 100644 --- a/src/core/server/http/base_path_service.ts +++ b/src/core/server/http/base_path_service.ts @@ -16,24 +16,23 @@ * specific language governing permissions and limitations * under the License. */ -import { Request } from 'hapi'; -import { KibanaRequest, ensureRawRequest } from './router'; +import { ensureRawRequest, KibanaRequest, LegacyRequest } from './router'; import { modifyUrl } from '../../utils'; export class BasePath { - private readonly basePathCache = new WeakMap(); + private readonly basePathCache = new WeakMap(); constructor(private readonly serverBasePath?: string) {} - public get = (request: KibanaRequest | Request) => { + public get = (request: KibanaRequest | LegacyRequest) => { const requestScopePath = this.basePathCache.get(ensureRawRequest(request)) || ''; const serverBasePath = this.serverBasePath || ''; return `${serverBasePath}${requestScopePath}`; }; // should work only for KibanaRequest as soon as spaces migrate to NP - public set = (request: KibanaRequest | Request, requestSpecificBasePath: string) => { + public set = (request: KibanaRequest | LegacyRequest, requestSpecificBasePath: string) => { const rawRequest = ensureRawRequest(request); if (this.basePathCache.has(rawRequest)) { diff --git a/src/core/server/http/cookie_session_storage.ts b/src/core/server/http/cookie_session_storage.ts index 7b2569a1c6dd33..8a1b56d87fb4c9 100644 --- a/src/core/server/http/cookie_session_storage.ts +++ b/src/core/server/http/cookie_session_storage.ts @@ -24,10 +24,26 @@ import { KibanaRequest, ensureRawRequest } from './router'; import { SessionStorageFactory, SessionStorage } from './session_storage'; import { Logger } from '..'; +/** + * Configuration used to create HTTP session storage based on top of cookie mechanism. + * @public + */ export interface SessionStorageCookieOptions { + /** + * Name of the session cookie. + */ name: string; + /** + * A key used to encrypt a cookie value. Should be at least 32 characters long. + */ encryptionKey: string; + /** + * Function called to validate a cookie content. + */ validate: (sessionValue: T) => boolean | Promise; + /** + * Flag indicating whether the cookie should be sent only via a secure connection. + */ isSecure: boolean; } diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index c8b4c5c17f05e1..45cadb7d3811e4 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -25,21 +25,27 @@ import { createServer, getListenerOptions, getServerOptions } from './http_tools import { adoptToHapiAuthFormat, AuthenticationHandler } from './lifecycle/auth'; import { adoptToHapiOnPostAuthFormat, OnPostAuthHandler } from './lifecycle/on_post_auth'; import { adoptToHapiOnPreAuthFormat, OnPreAuthHandler } from './lifecycle/on_pre_auth'; -import { Router, KibanaRequest, ResponseHeaders } from './router'; +import { KibanaRequest, LegacyRequest, ResponseHeaders, Router } from './router'; import { SessionStorageCookieOptions, createCookieSessionStorageFactory, } from './cookie_session_storage'; import { SessionStorageFactory } from './session_storage'; -import { AuthStateStorage } from './auth_state_storage'; -import { AuthHeadersStorage } from './auth_headers_storage'; +import { AuthStateStorage, GetAuthState, IsAuthenticated } from './auth_state_storage'; +import { AuthHeadersStorage, GetAuthHeaders } from './auth_headers_storage'; import { BasePath } from './base_path_service'; +/** @public */ export interface HttpServerSetup { server: Server; + /** + * Add all the routes registered with `router` to HTTP server request listeners. + * @param router {@link Router} - a router with registered route handlers. + */ registerRouter: (router: Router) => void; /** * Creates cookie based session storage factory {@link SessionStorageFactory} + * @param cookieOptions {@link SessionStorageCookieOptions} - options to configure created cookie session storage. */ createCookieSessionStorageFactory: ( cookieOptions: SessionStorageCookieOptions @@ -49,35 +55,53 @@ export interface HttpServerSetup { * A handler should return a state to associate with the incoming request. * The state can be retrieved later via http.auth.get(..) * Only one AuthenticationHandler can be registered. + * @param handler {@link AuthenticationHandler} - function to perform authentication. */ registerAuth: (handler: AuthenticationHandler) => void; /** * To define custom logic to perform for incoming requests. Runs the handler before Auth - * hook performs a check that user has access to requested resources, so it's the only + * interceptor performs a check that user has access to requested resources, so it's the only * place when you can forward a request to another URL right on the server. * Can register any number of registerOnPostAuth, which are called in sequence * (from the first registered to the last). + * @param handler {@link OnPreAuthHandler} - function to call. */ registerOnPreAuth: (handler: OnPreAuthHandler) => void; /** - * To define custom logic to perform for incoming requests. Runs the handler after Auth hook + * To define custom logic to perform for incoming requests. Runs the handler after Auth interceptor * did make sure a user has access to the requested resource. * The auth state is available at stage via http.auth.get(..) * Can register any number of registerOnPreAuth, which are called in sequence * (from the first registered to the last). + * @param handler {@link OnPostAuthHandler} - function to call. */ registerOnPostAuth: (handler: OnPostAuthHandler) => void; basePath: { - get: (request: KibanaRequest | Request) => string; - set: (request: KibanaRequest | Request, basePath: string) => void; + /** + * returns `basePath` value, specific for an incoming request. + */ + get: (request: KibanaRequest | LegacyRequest) => string; + /** + * sets `basePath` value, specific for an incoming request. + */ + set: (request: KibanaRequest | LegacyRequest, basePath: string) => void; + /** + * returns a new `basePath` value, prefixed with passed `url`. + */ prepend: (url: string) => string; + /** + * returns a new `basePath` value, cleaned up from passed `url`. + */ remove: (url: string) => string; }; auth: { - get: AuthStateStorage['get']; - isAuthenticated: AuthStateStorage['isAuthenticated']; - getAuthHeaders: AuthHeadersStorage['get']; + get: GetAuthState; + isAuthenticated: IsAuthenticated; + getAuthHeaders: GetAuthHeaders; }; + /** + * Flag showing whether a server was configured to use TLS connection. + */ isTlsEnabled: boolean; } diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index eff7de0979c119..9730a25610fe0a 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -19,13 +19,25 @@ export { config, HttpConfig, HttpConfigType } from './http_config'; export { HttpService, HttpServiceSetup, HttpServiceStart } from './http_service'; +export { HttpServerSetup } from './http_server'; export { GetAuthHeaders } from './auth_headers_storage'; +export { AuthStatus, GetAuthState, IsAuthenticated } from './auth_state_storage'; export { + CustomHttpResponseOptions, isRealRequest, + HttpResponseOptions, + HttpResponsePayload, KibanaRequest, KibanaRequestRoute, + KnownHeaders, + LegacyRequest, + RedirectResponseOptions, + RequestHandler, ResponseError, ResponseErrorMeta, + responseFactory, + ResponseFactory, + RouteConfig, Router, RouteMethod, RouteConfigOptions, @@ -40,3 +52,4 @@ export { } from './lifecycle/auth'; export { OnPostAuthHandler, OnPostAuthToolkit } from './lifecycle/on_post_auth'; export { SessionStorageFactory, SessionStorage } from './session_storage'; +export { SessionStorageCookieOptions } from './cookie_session_storage'; diff --git a/src/core/server/http/router/headers.ts b/src/core/server/http/router/headers.ts index 3d5fe213f92afd..19eaee50819966 100644 --- a/src/core/server/http/router/headers.ts +++ b/src/core/server/http/router/headers.ts @@ -16,12 +16,49 @@ * specific language governing permissions and limitations * under the License. */ +import { IncomingHttpHeaders } from 'http'; import { pick } from '../../../utils'; -/** @public */ -export type Headers = Record; -export type ResponseHeaders = Record; +/** + * Creates a Union type of all known keys of a given interface. + * @example + * ```ts + * interface Person { + * name: string; + * age: number; + * [attributes: string]: string | number; + * } + * type PersonKnownKeys = KnownKeys; // "age" | "name" + * ``` + */ +type KnownKeys = { + [K in keyof T]: string extends K ? never : number extends K ? never : K; +} extends { [_ in keyof T]: infer U } + ? U + : never; + +/** + * Set of well-known HTTP headers. + * @public + */ +export type KnownHeaders = KnownKeys; + +/** + * Http request headers to read. + * @public + */ +export type Headers = { [header in KnownHeaders]?: string | string[] | undefined } & { + [header: string]: string | string[] | undefined; +}; + +/** + * Http response headers to set. + * @public + */ +export type ResponseHeaders = { [header in KnownHeaders]?: string | string[] } & { + [header: string]: string | string[]; +}; const normalizeHeaderField = (field: string) => field.trim().toLowerCase(); diff --git a/src/core/server/http/router/index.ts b/src/core/server/http/router/index.ts index eefa74cee0802e..29fdfa9c03e1cc 100644 --- a/src/core/server/http/router/index.ts +++ b/src/core/server/http/router/index.ts @@ -17,8 +17,23 @@ * under the License. */ -export { Headers, filterHeaders, ResponseHeaders } from './headers'; -export { Router } from './router'; -export { KibanaRequest, KibanaRequestRoute, ensureRawRequest, isRealRequest } from './request'; -export { RouteMethod, RouteConfigOptions } from './route'; -export { ResponseError, ResponseErrorMeta } from './response'; +export { Headers, filterHeaders, ResponseHeaders, KnownHeaders } from './headers'; +export { Router, RequestHandler } from './router'; +export { + KibanaRequest, + KibanaRequestRoute, + isRealRequest, + LegacyRequest, + ensureRawRequest, +} from './request'; +export { RouteMethod, RouteConfig, RouteConfigOptions } from './route'; +export { + CustomHttpResponseOptions, + HttpResponseOptions, + HttpResponsePayload, + RedirectResponseOptions, + ResponseError, + ResponseErrorMeta, + responseFactory, + ResponseFactory, +} from './response'; diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index da10a6500ccc86..4eac2e98317fcc 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -38,16 +38,22 @@ export interface KibanaRequestRoute { options: Required; } +/** + * @deprecated + * `hapi` request object, supported during migration process only for backward compatibility. + * @public + */ +export interface LegacyRequest extends Request {} // eslint-disable-line @typescript-eslint/no-empty-interface + /** * Kibana specific abstraction for an incoming request. * @public - * */ + */ export class KibanaRequest { /** * Factory for creating requests. Validates the request before creating an * instance of a KibanaRequest. * @internal - * */ public static from

( req: Request, @@ -68,6 +74,7 @@ export class KibanaRequest { * Validates the different parts of a request based on the schemas defined for * the route. Builds up the actual params, query and body object that will be * received in the route handler. + * @internal */ private static validate

( req: Request, @@ -102,8 +109,9 @@ export class KibanaRequest { return { query, params, body }; } - + /** a WHATWG URL standard object. */ public readonly url: Url; + /** matched route details */ public readonly route: RecursiveReadonly; /** * Readonly copy of incoming request headers. @@ -153,14 +161,14 @@ export class KibanaRequest { * Returns underlying Hapi Request * @internal */ -export const ensureRawRequest = (request: KibanaRequest | Request) => +export const ensureRawRequest = (request: KibanaRequest | LegacyRequest) => isKibanaRequest(request) ? request[requestSymbol] : request; function isKibanaRequest(request: unknown): request is KibanaRequest { return request instanceof KibanaRequest; } -function isRequest(request: any): request is Request { +function isRequest(request: any): request is LegacyRequest { try { return request.raw.req && typeof request.raw.req === 'object'; } catch { @@ -172,6 +180,6 @@ function isRequest(request: any): request is Request { * Checks if an incoming request either KibanaRequest or Legacy.Request * @internal */ -export function isRealRequest(request: unknown): request is KibanaRequest | Request { +export function isRealRequest(request: unknown): request is KibanaRequest | LegacyRequest { return isKibanaRequest(request) || isRequest(request); } diff --git a/src/core/server/http/router/response.ts b/src/core/server/http/router/response.ts index 65db87f8ae8f8e..98c8fd38e2892f 100644 --- a/src/core/server/http/router/response.ts +++ b/src/core/server/http/router/response.ts @@ -16,8 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { IncomingHttpHeaders } from 'http'; import { Stream } from 'stream'; +import { ResponseHeaders } from './headers'; /** * Additional metadata to enhance error output or provide error details. @@ -40,6 +40,10 @@ export type ResponseError = meta?: ResponseErrorMeta; }; +/** + * A response data object, expected to returned as a result of {@link RequestHandler} execution + * @internal + */ export class KibanaResponse { constructor( readonly status: number, @@ -48,51 +52,31 @@ export class KibanaResponse { ) {} } -/** - * Creates a Union type of all known keys of a given interface. - * @example - * ```ts - * interface Person { - * name: string; - * age: number; - * [attributes: string]: string | number; - * } - * type PersonKnownKeys = KnownKeys; // "age" | "name" - * ``` - */ -type KnownKeys = { - [K in keyof T]: string extends K ? never : number extends K ? never : K; -} extends { [_ in keyof T]: infer U } - ? U - : never; - -type KnownHeaders = KnownKeys; /** * HTTP response parameters * @public */ export interface HttpResponseOptions { /** HTTP Headers with additional information about response */ - headers?: { [header in KnownHeaders]?: string | string[] } & { - [header: string]: string | string[]; - }; + headers?: ResponseHeaders; } /** + * Data send to the client as a response payload. * @public */ export type HttpResponsePayload = undefined | string | Record | Buffer | Stream; /** - * HTTP response parameters + * HTTP response parameters for a response with adjustable status code. * @public */ -export interface CustomResponseOptions extends HttpResponseOptions { +export interface CustomHttpResponseOptions extends HttpResponseOptions { statusCode: number; } /** - * HTTP response parameters + * HTTP response parameters for redirection response * @public */ export type RedirectResponseOptions = HttpResponseOptions & { @@ -101,6 +85,11 @@ export type RedirectResponseOptions = HttpResponseOptions & { }; }; +/** + * Set of helpers used to create `KibanaResponse` to form HTTP response on an incoming request. + * Should be returned as a result of {@link RequestHandler} execution. + * @public + */ export const responseFactory = { // Success /** @@ -131,9 +120,9 @@ export const responseFactory = { /** * Creates a response with defined status code and payload. * @param payload - {@link HttpResponsePayload} payload to send to the client - * @param options - {@link CustomResponseOptions} configures HTTP response parameters. + * @param options - {@link CustomHttpResponseOptions} configures HTTP response parameters. */ - custom: (payload: HttpResponsePayload | ResponseError, options: CustomResponseOptions) => { + custom: (payload: HttpResponsePayload | ResponseError, options: CustomHttpResponseOptions) => { if (!options || !options.statusCode) { throw new Error(`options.statusCode is expected to be set. given options: ${options}`); } diff --git a/src/core/server/http/router/route.ts b/src/core/server/http/router/route.ts index caf13343ec96ca..e8053560148293 100644 --- a/src/core/server/http/router/route.ts +++ b/src/core/server/http/router/route.ts @@ -21,18 +21,18 @@ import { ObjectType } from '@kbn/config-schema'; /** * The set of common HTTP methods supported by Kibana routing. * @public - * */ + */ export type RouteMethod = 'get' | 'post' | 'put' | 'delete'; /** - * Route specific configuration. + * Additional route options. * @public - * */ + */ export interface RouteConfigOptions { /** * A flag shows that authentication for a route: - * enabled when true - * disabled when false + * `enabled` when true + * `disabled` when false * * Enabled by default. */ @@ -44,27 +44,58 @@ export interface RouteConfigOptions { tags?: readonly string[]; } +/** + * Route specific configuration. + * @public + */ export interface RouteConfig

{ /** * The endpoint _within_ the router path to register the route. E.g. if the * router is registered at `/elasticsearch` and the route path is `/search`, * the full path for the route is `/elasticsearch/search`. + * Supports: + * - named path segments `path/{name}`. + * - optional path segments `path/{position?}`. + * - multi-segments `path/{coordinates*2}`. + * Segments are accessible within a handler function as `params` property of {@link KibanaRequest} object. + * To have read access to `params` you *must* specify validation schema with {@link RouteConfig.validate}. */ path: string; /** * A schema created with `@kbn/config-schema` that every request will be validated against. - * + * You *must* specify a validation schema to be able to read: + * - url path segments + * - request query + * - request body * To opt out of validating the request, specify `false`. + * @example + * ```ts + * import { schema } from '@kbn/config-schema'; + * router.get({ + * path: 'path/{id}' + * validate: { + * params: schema.object({ + * id: schema.string(), + * }), + * query: schema.object({...}), + * body: schema.object({...}), + * }, + * }) + * ``` */ validate: RouteSchemas | false; + /** + * Additional route options {@link RouteConfigOptions}. + */ options?: RouteConfigOptions; } /** * RouteSchemas contains the schemas for validating the different parts of a * request. + * @public */ export interface RouteSchemas

{ params?: P; diff --git a/src/core/server/http/router/router.ts b/src/core/server/http/router/router.ts index b801aa41754faa..408426a1220d09 100644 --- a/src/core/server/http/router/router.ts +++ b/src/core/server/http/router/router.ts @@ -26,21 +26,37 @@ import { KibanaResponse, ResponseFactory, responseFactory } from './response'; import { RouteConfig, RouteConfigOptions, RouteMethod, RouteSchemas } from './route'; import { HapiResponseAdapter } from './response_adapter'; -export interface RouterRoute { +interface RouterRoute { method: RouteMethod; path: string; options: RouteConfigOptions; handler: (req: Request, responseToolkit: ResponseToolkit, log: Logger) => Promise; } -/** @public */ +/** + * Provides ability to declare a handler function for a particular path and HTTP request method. + * Each route can have only one handler functions, which is executed when the route is matched. + * + * @example + * ```ts + * const router = new Router('my-app'); + * // handler is called when 'my-app/path' resource is requested with `GET` method + * router.get({ path: '/path', validate: false }, (req, res) => res.ok({ content: 'ok' })); + * ``` + * + * @public + * */ export class Router { public routes: Array> = []; - + /** + * @param path - a router path, set as the very first path segment for all registered routes. + */ constructor(readonly path: string) {} /** - * Register a `GET` request with the router + * Register a route handler for `GET` request. + * @param route {@link RouteConfig} - a route configuration. + * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ public get

( route: RouteConfig, @@ -58,7 +74,9 @@ export class Router { } /** - * Register a `POST` request with the router + * Register a route handler for `POST` request. + * @param route {@link RouteConfig} - a route configuration. + * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ public post

( route: RouteConfig, @@ -76,7 +94,9 @@ export class Router { } /** - * Register a `PUT` request with the router + * Register a route handler for `PUT` request. + * @param route {@link RouteConfig} - a route configuration. + * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ public put

( route: RouteConfig, @@ -94,7 +114,9 @@ export class Router { } /** - * Register a `DELETE` request with the router + * Register a route handler for `DELETE` request. + * @param route {@link RouteConfig} - a route configuration. + * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ public delete

( route: RouteConfig, @@ -114,13 +136,14 @@ export class Router { /** * Returns all routes registered with the this router. * @returns List of registered routes. + * @internal */ public getRoutes() { return [...this.routes]; } /** - * Create the schemas for a route + * Create the validation schemas for a route * * @returns Route schemas if `validate` is specified on the route, otherwise * undefined. @@ -176,6 +199,39 @@ export class Router { } } +/** + * A function executed when route path matched requested resource path. + * Request handler is expected to return a result of one of {@link ResponseFactory} functions. + * @param request {@link KibanaRequest} - object containing information about requested resource, + * such as path, method, headers, parameters, query, body, etc. + * @param createResponse {@link ResponseFactory} - a set of helper functions used to respond to a request. + * + * @example + * ```ts + * const router = new Router('my-app'); + * // creates a route handler for GET request on 'my-app/path/{id}' path + * router.get( + * { + * path: 'path/{id}', + * // defines a validation schema for a named segment of the route path + * validate: { + * params: schema.object({ + * id: schema.string(), + * }), + * }, + * }, + * // function to execute to create a responses + * async (request, createResponse) => { + * const data = await findObject(request.params.id); + * // creates a command to respond with 'not found' error + * if (!data) return createResponse.notFound(); + * // creates a command to send found data to the client + * return createResponse.ok(data); + * } + * ); + * ``` + * @public + */ export type RequestHandler

= ( request: KibanaRequest, TypeOf, TypeOf>, createResponse: ResponseFactory diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 2bbcaa58f6af52..14dd92c6ca02df 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -56,27 +56,41 @@ export { ElasticsearchErrorHelpers, APICaller, FakeRequest, - LegacyRequest, } from './elasticsearch'; export { AuthenticationHandler, AuthHeaders, AuthResultParams, + AuthStatus, AuthToolkit, + CustomHttpResponseOptions, GetAuthHeaders, + GetAuthState, + HttpResponseOptions, + HttpResponsePayload, + HttpServerSetup, + IsAuthenticated, KibanaRequest, KibanaRequestRoute, + KnownHeaders, + LegacyRequest, OnPreAuthHandler, OnPreAuthToolkit, OnPostAuthHandler, OnPostAuthToolkit, + RedirectResponseOptions, + RequestHandler, ResponseError, ResponseErrorMeta, + responseFactory, + ResponseFactory, + RouteConfig, Router, RouteMethod, RouteConfigOptions, - SessionStorageFactory, SessionStorage, + SessionStorageCookieOptions, + SessionStorageFactory, } from './http'; export { Logger, LoggerFactory, LogMeta, LogRecord, LogLevel } from './logging';