mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[kbn/server-route-repository] Add createRepositoryClient function (#189764)
### Summary This PR adds the `createRepositoryClient` function, which takes the type of a server route repository as a generic argument and creates a wrapper around `core.http` that is type bound by the routes defined in the repository, as well as their request param types and return types. This function was extracted from the code that exists in the AI Assistant plugin. Other changes include: * Adding usage documentation * Creation of a new package `@kbn/server-route-repository-client` to house `createRepositoryClient` so it can be safely imported in browser side code * Moving the types from ``@kbn/server-route-repository` to ``@kbn/server-route-repository-utils` in order to use them in both the server and browser side * Add some default types to the generics for `createServerRouteFactory` (`createRepositoryClient` also has default types) * Allow `registerRoutes` to take a generic to constrain the shape of `dependencies` so that the type used when calling `createServerRouteFactory` can be used in both places --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
1a82dd6f57
commit
986001c9a5
22 changed files with 695 additions and 28 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -776,6 +776,7 @@ packages/kbn-securitysolution-t-grid @elastic/security-detection-engine
|
|||
packages/kbn-securitysolution-utils @elastic/security-detection-engine
|
||||
packages/kbn-server-http-tools @elastic/kibana-core
|
||||
packages/kbn-server-route-repository @elastic/obs-knowledge-team
|
||||
packages/kbn-server-route-repository-client @elastic/obs-knowledge-team
|
||||
packages/kbn-server-route-repository-utils @elastic/obs-knowledge-team
|
||||
x-pack/plugins/serverless @elastic/appex-sharedux
|
||||
packages/serverless/settings/common @elastic/appex-sharedux @elastic/kibana-management
|
||||
|
|
|
@ -791,6 +791,7 @@
|
|||
"@kbn/securitysolution-utils": "link:packages/kbn-securitysolution-utils",
|
||||
"@kbn/server-http-tools": "link:packages/kbn-server-http-tools",
|
||||
"@kbn/server-route-repository": "link:packages/kbn-server-route-repository",
|
||||
"@kbn/server-route-repository-client": "link:packages/kbn-server-route-repository-client",
|
||||
"@kbn/server-route-repository-utils": "link:packages/kbn-server-route-repository-utils",
|
||||
"@kbn/serverless": "link:x-pack/plugins/serverless",
|
||||
"@kbn/serverless-common-settings": "link:packages/serverless/settings/common",
|
||||
|
|
3
packages/kbn-server-route-repository-client/README.md
Normal file
3
packages/kbn-server-route-repository-client/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# @kbn/server-route-repository-client
|
||||
|
||||
Extension of `@kbn/server-route-repository` with the browser side parts of the `@kbn/server-route-repository` package.
|
10
packages/kbn-server-route-repository-client/index.ts
Normal file
10
packages/kbn-server-route-repository-client/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { createRepositoryClient } from './src/create_repository_client';
|
||||
export type { DefaultClientOptions } from '@kbn/server-route-repository-utils';
|
13
packages/kbn-server-route-repository-client/jest.config.js
Normal file
13
packages/kbn-server-route-repository-client/jest.config.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test/jest_node',
|
||||
rootDir: '../..',
|
||||
roots: ['<rootDir>/packages/kbn-server-route-repository-client'],
|
||||
};
|
5
packages/kbn-server-route-repository-client/kibana.jsonc
Normal file
5
packages/kbn-server-route-repository-client/kibana.jsonc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"type": "shared-browser",
|
||||
"id": "@kbn/server-route-repository-client",
|
||||
"owner": "@elastic/obs-knowledge-team"
|
||||
}
|
6
packages/kbn-server-route-repository-client/package.json
Normal file
6
packages/kbn-server-route-repository-client/package.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/server-route-repository-client",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { CoreSetup } from '@kbn/core-lifecycle-browser';
|
||||
import { createRepositoryClient } from './create_repository_client';
|
||||
|
||||
describe('createRepositoryClient', () => {
|
||||
const getMock = jest.fn();
|
||||
const coreSetupMock = {
|
||||
http: {
|
||||
get: getMock,
|
||||
},
|
||||
} as unknown as CoreSetup;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('provides a default value for options when they are not required', () => {
|
||||
const repository = {
|
||||
'GET /internal/handler': {
|
||||
endpoint: 'GET /internal/handler',
|
||||
handler: jest.fn().mockResolvedValue('OK'),
|
||||
},
|
||||
};
|
||||
const { fetch } = createRepositoryClient<typeof repository>(coreSetupMock);
|
||||
|
||||
fetch('GET /internal/handler');
|
||||
|
||||
expect(getMock).toHaveBeenCalledTimes(1);
|
||||
expect(getMock).toHaveBeenNthCalledWith(1, '/internal/handler', {
|
||||
body: undefined,
|
||||
query: undefined,
|
||||
version: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('extract the version from the endpoint', () => {
|
||||
const repository = {
|
||||
'GET /api/handler 2024-08-05': {
|
||||
endpoint: 'GET /api/handler 2024-08-05',
|
||||
handler: jest.fn().mockResolvedValue('OK'),
|
||||
},
|
||||
};
|
||||
const { fetch } = createRepositoryClient<typeof repository>(coreSetupMock);
|
||||
|
||||
fetch('GET /api/handler 2024-08-05');
|
||||
|
||||
expect(getMock).toHaveBeenCalledTimes(1);
|
||||
expect(getMock).toHaveBeenNthCalledWith(1, '/api/handler', {
|
||||
body: undefined,
|
||||
query: undefined,
|
||||
version: '2024-08-05',
|
||||
});
|
||||
});
|
||||
|
||||
it('passes on the provided client parameters', () => {
|
||||
const repository = {
|
||||
'GET /internal/handler': {
|
||||
endpoint: 'GET /internal/handler',
|
||||
handler: jest.fn().mockResolvedValue('OK'),
|
||||
},
|
||||
};
|
||||
const { fetch } = createRepositoryClient<typeof repository>(coreSetupMock);
|
||||
|
||||
fetch('GET /internal/handler', {
|
||||
headers: {
|
||||
some_header: 'header_value',
|
||||
},
|
||||
});
|
||||
|
||||
expect(getMock).toHaveBeenCalledTimes(1);
|
||||
expect(getMock).toHaveBeenNthCalledWith(1, '/internal/handler', {
|
||||
headers: {
|
||||
some_header: 'header_value',
|
||||
},
|
||||
body: undefined,
|
||||
query: undefined,
|
||||
version: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('replaces path params before making the call', () => {
|
||||
const repository = {
|
||||
'GET /internal/handler/{param}': {
|
||||
endpoint: 'GET /internal/handler/{param}',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
param: t.string,
|
||||
}),
|
||||
}),
|
||||
handler: jest.fn().mockResolvedValue('OK'),
|
||||
},
|
||||
};
|
||||
const { fetch } = createRepositoryClient<typeof repository>(coreSetupMock);
|
||||
|
||||
fetch('GET /internal/handler/{param}', {
|
||||
params: {
|
||||
path: {
|
||||
param: 'param_value',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getMock).toHaveBeenCalledTimes(1);
|
||||
expect(getMock).toHaveBeenNthCalledWith(1, '/internal/handler/param_value', {
|
||||
body: undefined,
|
||||
query: undefined,
|
||||
version: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('passes on the stringified body content when provided', () => {
|
||||
const repository = {
|
||||
'GET /internal/handler': {
|
||||
endpoint: 'GET /internal/handler',
|
||||
params: t.type({
|
||||
body: t.type({
|
||||
payload: t.string,
|
||||
}),
|
||||
}),
|
||||
handler: jest.fn().mockResolvedValue('OK'),
|
||||
},
|
||||
};
|
||||
const { fetch } = createRepositoryClient<typeof repository>(coreSetupMock);
|
||||
|
||||
fetch('GET /internal/handler', {
|
||||
params: {
|
||||
body: {
|
||||
payload: 'body_value',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getMock).toHaveBeenCalledTimes(1);
|
||||
expect(getMock).toHaveBeenNthCalledWith(1, '/internal/handler', {
|
||||
body: JSON.stringify({
|
||||
payload: 'body_value',
|
||||
}),
|
||||
query: undefined,
|
||||
version: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('passes on the query parameters when provided', () => {
|
||||
const repository = {
|
||||
'GET /internal/handler': {
|
||||
endpoint: 'GET /internal/handler',
|
||||
params: t.type({
|
||||
query: t.type({
|
||||
parameter: t.string,
|
||||
}),
|
||||
}),
|
||||
handler: jest.fn().mockResolvedValue('OK'),
|
||||
},
|
||||
};
|
||||
const { fetch } = createRepositoryClient<typeof repository>(coreSetupMock);
|
||||
|
||||
fetch('GET /internal/handler', {
|
||||
params: {
|
||||
query: {
|
||||
parameter: 'query_value',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getMock).toHaveBeenCalledTimes(1);
|
||||
expect(getMock).toHaveBeenNthCalledWith(1, '/internal/handler', {
|
||||
body: undefined,
|
||||
query: {
|
||||
parameter: 'query_value',
|
||||
},
|
||||
version: undefined,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { CoreSetup, CoreStart } from '@kbn/core-lifecycle-browser';
|
||||
import {
|
||||
RouteRepositoryClient,
|
||||
ServerRouteRepository,
|
||||
DefaultClientOptions,
|
||||
formatRequest,
|
||||
} from '@kbn/server-route-repository-utils';
|
||||
|
||||
export function createRepositoryClient<
|
||||
TRepository extends ServerRouteRepository,
|
||||
TClientOptions extends Record<string, any> = DefaultClientOptions
|
||||
>(core: CoreStart | CoreSetup) {
|
||||
return {
|
||||
fetch: (endpoint, optionsWithParams) => {
|
||||
const { params, ...options } = (optionsWithParams ?? { params: {} }) as unknown as {
|
||||
params?: Partial<Record<string, any>>;
|
||||
};
|
||||
|
||||
const { method, pathname, version } = formatRequest(endpoint, params?.path);
|
||||
|
||||
return core.http[method](pathname, {
|
||||
...options,
|
||||
body: params && params.body ? JSON.stringify(params.body) : undefined,
|
||||
query: params?.query,
|
||||
version,
|
||||
});
|
||||
},
|
||||
} as { fetch: RouteRepositoryClient<TRepository, TClientOptions> };
|
||||
}
|
20
packages/kbn-server-route-repository-client/tsconfig.json
Normal file
20
packages/kbn-server-route-repository-client/tsconfig.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/server-route-repository-utils",
|
||||
"@kbn/core-lifecycle-browser",
|
||||
]
|
||||
}
|
|
@ -8,3 +8,20 @@
|
|||
|
||||
export { formatRequest } from './src/format_request';
|
||||
export { parseEndpoint } from './src/parse_endpoint';
|
||||
|
||||
export type {
|
||||
ServerRouteCreateOptions,
|
||||
ServerRouteHandlerResources,
|
||||
RouteParamsRT,
|
||||
ServerRoute,
|
||||
EndpointOf,
|
||||
ReturnOf,
|
||||
RouteRepositoryClient,
|
||||
RouteState,
|
||||
ClientRequestParamsOf,
|
||||
DecodedRequestParamsOf,
|
||||
ServerRouteRepository,
|
||||
DefaultClientOptions,
|
||||
DefaultRouteCreateOptions,
|
||||
DefaultRouteHandlerResources,
|
||||
} from './src/typings';
|
||||
|
|
|
@ -6,7 +6,16 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { IKibanaResponse } from '@kbn/core-http-server';
|
||||
import type { HttpFetchOptions } from '@kbn/core-http-browser';
|
||||
import type { IKibanaResponse } from '@kbn/core-http-server';
|
||||
import type {
|
||||
RequestHandlerContext,
|
||||
Logger,
|
||||
RouteConfigOptions,
|
||||
RouteMethod,
|
||||
KibanaRequest,
|
||||
KibanaResponseFactory,
|
||||
} from '@kbn/core/server';
|
||||
import * as t from 'io-ts';
|
||||
import { RequiredKeys } from 'utility-types';
|
||||
|
||||
|
@ -137,9 +146,25 @@ type MaybeOptionalArgs<T extends Record<string, any>> = RequiredKeys<T> extends
|
|||
export type RouteRepositoryClient<
|
||||
TServerRouteRepository extends ServerRouteRepository,
|
||||
TAdditionalClientOptions extends Record<string, any>
|
||||
> = <TEndpoint extends keyof TServerRouteRepository>(
|
||||
> = <TEndpoint extends Extract<keyof TServerRouteRepository, string>>(
|
||||
endpoint: TEndpoint,
|
||||
...args: MaybeOptionalArgs<
|
||||
ClientRequestParamsOf<TServerRouteRepository, TEndpoint> & TAdditionalClientOptions
|
||||
>
|
||||
) => Promise<ReturnOf<TServerRouteRepository, TEndpoint>>;
|
||||
|
||||
export type DefaultClientOptions = HttpFetchOptions;
|
||||
|
||||
interface CoreRouteHandlerResources {
|
||||
request: KibanaRequest;
|
||||
response: KibanaResponseFactory;
|
||||
context: RequestHandlerContext;
|
||||
}
|
||||
|
||||
export interface DefaultRouteHandlerResources extends CoreRouteHandlerResources {
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export interface DefaultRouteCreateOptions {
|
||||
options?: RouteConfigOptions<RouteMethod>;
|
||||
}
|
|
@ -15,5 +15,9 @@
|
|||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": []
|
||||
"kbn_references": [
|
||||
"@kbn/core-http-browser",
|
||||
"@kbn/core-http-server",
|
||||
"@kbn/core",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -2,12 +2,346 @@
|
|||
|
||||
Utility functions for creating a typed server route repository, and a typed client, generating runtime validation and type validation from the same route definition.
|
||||
|
||||
## Usage
|
||||
## Overview
|
||||
|
||||
TBD
|
||||
There are three main functions that make up this package:
|
||||
1. `createServerRouteFactory`
|
||||
2. `registerRoutes`
|
||||
3. `createRepositoryClient`
|
||||
|
||||
## Server vs. Browser entry points
|
||||
`createServerRouteFactory` and `registerRoutes` are used in the server and `createRepositoryClient` in the browser (thus it is imported from `@kbn/server-route-repository-client`).
|
||||
|
||||
This package can only be used on the server. The browser utilities can be found in `@kbn/server-route-repository-utils`.
|
||||
`createServerRouteFactory` returns a function that can be used to create routes for the repository, when calling it you can specify the resources that will be available to your route handler as well as which other options should be specified on your routes.
|
||||
|
||||
When adding utilities to this package, please make sure to update the entry points accordingly and the [BUILD.bazel](./BUILD.bazel)'s `target_web` target build to include all the necessary files.
|
||||
Once the routes have been created and put into a plain object (the "repository"), this repository can then be passed to `registerRoutes` which also accepts the dependencies to be injected into each route handler. `registerRoutes` handles the creation of the Core HTTP router, as well as the final registration of the routes with versioning and request validation.
|
||||
|
||||
By exporting the type of the repository from the server to the browser (make sure you use a `type` import), we can pass that as a generic argument to `createRepositoryClient` and get back a thin but strongly typed wrapper around the Core HTTP service, with auto completion for the available routes, type checking for the request parameters required by each specific route and response type inference. You can also add a generic type for which additional options the client should pass with each request.
|
||||
|
||||
## Basic example
|
||||
|
||||
In the server side, we'll start by creating the route factory, to make things easier it is recommended to keep this in its own file and export it.
|
||||
|
||||
> server/create_my_plugin_server_route.ts
|
||||
```javascript
|
||||
import { createServerRouteFactory } from '@kbn/server-route-repository';
|
||||
import {
|
||||
DefaultRouteHandlerResources,
|
||||
DefaultRouteCreateOptions,
|
||||
} from '@kbn/server-route-repository-utils';
|
||||
|
||||
export const createMyPluginServerRoute = createServerRouteFactory<
|
||||
DefaultRouteHandlerResources,
|
||||
DefaultRouteCreateOptions
|
||||
>();
|
||||
```
|
||||
|
||||
The two generic arguments are optional, this example shows a "default" setup which exposes what Core HTTP would normally provide (`request`, `context`, `response`) plus a logger.
|
||||
|
||||
Next, let's create a minimal route.
|
||||
|
||||
> server/my_route.ts
|
||||
```javascript
|
||||
import { createMyPluginServerRoute } from './create_my_plugin_server_route';
|
||||
|
||||
export const myRoute = createMyPluginServerRoute({
|
||||
endpoint: 'GET /internal/my_plugin/route',
|
||||
handler: async (resources) => {
|
||||
const { request, context, response, logger } = resources;
|
||||
return response.ok({
|
||||
body: 'Hello, my route!',
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
After this we can add the route to a "repository", which is just a plain object, and call `registerRoutes`.
|
||||
|
||||
> server/plugin.ts
|
||||
|
||||
```javascript
|
||||
import { registerRoutes } from '@kbn/server-route-repository';
|
||||
|
||||
import { myRoute } from './my_route';
|
||||
|
||||
const repository = {
|
||||
...myRoute,
|
||||
};
|
||||
|
||||
export type MyPluginRouteRepository = typeof repository;
|
||||
|
||||
class MyPlugin implements Plugin {
|
||||
public setup(core: CoreSetup) {
|
||||
registerRoutes({
|
||||
core,
|
||||
logger,
|
||||
repository,
|
||||
dependencies: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Since this example doesn't use any dependencies, the generic argument for `registerRoutes` is optional and we pass an empty object.
|
||||
We also export the type of the repository, we'll need this for the client which is next!
|
||||
|
||||
The client can be created either in `setup` or `start`.
|
||||
|
||||
> browser/plugin.ts
|
||||
```javascript
|
||||
import { isHttpFetchError } from '@kbn/core-http-browser';
|
||||
import { DefaultClientOptions } from '@kbn/server-route-repository-utils';
|
||||
import { createRepositoryClient } from '@kbn/server-route-repository-client';
|
||||
import type { MyPluginRouteRepository } from '../server/plugin';
|
||||
|
||||
export type MyPluginRepositoryClient =
|
||||
ReturnType<typeof createRepositoryClient<MyPluginRouteRepository, DefaultClientOptions>>;
|
||||
|
||||
class MyPlugin implements Plugin {
|
||||
public setup(core: CoreSetup) {
|
||||
const myPluginRepositoryClient =
|
||||
createRepositoryClient<MyPluginRouteRepository, DefaultClientOptions>(core);
|
||||
|
||||
myPluginRepositoryClient
|
||||
.fetch('GET /internal/my_plugin/route')
|
||||
.then((response) => console.log(response))
|
||||
.catch((error) => {
|
||||
if (isHttpFetchError(error)) {
|
||||
console.log(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This example prints 'Hello, my route!' and the type of the response is **inferred** to this.
|
||||
|
||||
We pass in the type of the repository that we (_type_) imported from the server. The second generic parameter for `createRepositoryClient` is optional.
|
||||
We also export the type of the client itself so we can use it to type the client as we pass it around.
|
||||
|
||||
When using the client's `fetch` function, the first argument is the route to call and this is auto completed to only the available routes.
|
||||
The second argument is optional in this case but allows you to send in any extra options.
|
||||
|
||||
The client translates the endpoint and the options (including request parameters) to the right Core HTTP request.
|
||||
|
||||
## Request parameter validation
|
||||
|
||||
When creating your routes, you can also provide an `io-ts` codec to be used when validating incoming requests.
|
||||
|
||||
```javascript
|
||||
import * as t from 'io-ts';
|
||||
|
||||
const myRoute = createMyPluginServerRoute({
|
||||
endpoint: 'GET /internal/my_plugin/route/{my_path_param}',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
my_path_param: t.string,
|
||||
}),
|
||||
query: t.type({
|
||||
my_query_param: t.string,
|
||||
}),
|
||||
body: t.type({
|
||||
my_body_param: t.string,
|
||||
}),
|
||||
}),
|
||||
handler: async (resources) => {
|
||||
const { request, context, response, logger, params } = resources;
|
||||
|
||||
const { path, query, body } = params;
|
||||
|
||||
return response.ok({
|
||||
body: 'Hello, my route!',
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The `params` object is added to the route resources.
|
||||
`path`, `query` and `body` are validated before your handler is called and the types are **inferred** inside of the handler.
|
||||
|
||||
When calling this endpoint, it will look like this:
|
||||
```javascript
|
||||
client('GET /internal/my_plugin/route/{my_path_param}', {
|
||||
params: {
|
||||
path: {
|
||||
my_path_param: 'some_path_value',
|
||||
},
|
||||
query: {
|
||||
my_query_param: 'some_query_value',
|
||||
},
|
||||
body: {
|
||||
my_body_param: 'some_body_value',
|
||||
},
|
||||
},
|
||||
}).then(console.log);
|
||||
```
|
||||
|
||||
Where the shape of `params` is typed to match the expected shape, meaning you don't need to manually use the codec when calling the route.
|
||||
|
||||
## Public routes
|
||||
|
||||
To define a public route, you need to change the endpoint path and add a version.
|
||||
|
||||
```javascript
|
||||
const myRoute = createMyPluginServerRoute({
|
||||
endpoint: 'GET /api/my_plugin/route 2024-08-02',
|
||||
handler: async (resources) => {
|
||||
const { request, context, response, logger } = resources;
|
||||
return response.ok({
|
||||
body: 'Hello, my route!',
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
`registerRoutes` takes care of setting the `access` option correctly for you and using the right versioned router.
|
||||
|
||||
## Convenient return and throw
|
||||
|
||||
`registerRoutes` translate any returned or thrown non-Kibana response into a Kibana response (including `Boom`).
|
||||
It also handles common concerns like abort signals.
|
||||
|
||||
```javascript
|
||||
import { teapot } from '@hapi/boom';
|
||||
|
||||
const myRoute = createMyPluginServerRoute({
|
||||
endpoint: 'GET /internal/my_plugin/route',
|
||||
handler: async (resources) => {
|
||||
const { request, context, response, logger } = resources;
|
||||
|
||||
const result = coinFlip();
|
||||
if (result === 'heads') {
|
||||
throw teapot();
|
||||
} else {
|
||||
return 'Hello, my route!';
|
||||
}
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Both the teapot error and the plain string will be translated into a Kibana response.
|
||||
|
||||
## Route dependencies
|
||||
|
||||
If you want to provide additional dependencies to your route, you need to change the generic argument to `createServerRouteFactory` and `registerRoutes`.
|
||||
|
||||
```javascript
|
||||
import { createServerRouteFactory } from '@kbn/server-route-repository';
|
||||
import { DefaultRouteHandlerResources } from '@kbn/server-route-repository-utils';
|
||||
|
||||
export interface MyPluginRouteDependencies {
|
||||
myDependency: MyDependency;
|
||||
}
|
||||
|
||||
export const createMyPluginServerRoute =
|
||||
createServerRouteFactory<DefaultRouteHandlerResources & MyPluginRouteDependencies>();
|
||||
```
|
||||
|
||||
If you don't want your route to have access to the default resources, you could pass in only `MyPluginRouteDependencies`.
|
||||
|
||||
Then we use the same type when calling `registerRoutes`
|
||||
|
||||
```javascript
|
||||
registerRoutes<MyPluginRouteDependencies>({
|
||||
core,
|
||||
logger,
|
||||
repository,
|
||||
dependencies: {
|
||||
myDependency: new MyDependency(),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
This way, when creating a route, you will have `myDependency` available in the route resources.
|
||||
|
||||
```javascript
|
||||
import { createMyPluginServerRoute } from './create_my_plugin_server_route';
|
||||
|
||||
export const myRoute = createMyPluginServerRoute({
|
||||
endpoint: 'GET /internal/my_plugin/route',
|
||||
handler: async (resources) => {
|
||||
const { request, context, response, logger, myDependency } = resources;
|
||||
return response.ok({
|
||||
body: myDependency.sayHello(),
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Route creation options
|
||||
|
||||
Core HTTP allows certain options to be passed to the route when it's being created, and you may want to include your own options as well.
|
||||
To do this, override the second generic argument when calling `createServerRouteFactory`.
|
||||
|
||||
```javascript
|
||||
import { createServerRouteFactory } from '@kbn/server-route-repository';
|
||||
import {
|
||||
DefaultRouteHandlerResources,
|
||||
DefaultRouteCreateOptions,
|
||||
} from '@kbn/server-route-repository-utils';
|
||||
|
||||
interface MyPluginRouteCreateOptions {
|
||||
isDangerous: boolean;
|
||||
}
|
||||
|
||||
export const createMyPluginServerRoute = createServerRouteFactory<
|
||||
DefaultRouteHandlerResources,
|
||||
DefaultRouteCreateOptions & MyPluginRouteCreateOptions
|
||||
>();
|
||||
```
|
||||
|
||||
If you don't want your route to have access to the options provided by Core HTTP, you could pass in only `MyPluginRouteCreateOptions`.
|
||||
|
||||
You can then specify this option when creating the route.
|
||||
```javascript
|
||||
import { createMyPluginServerRoute } from './create_my_plugin_server_route';
|
||||
|
||||
export const myRoute = createMyPluginServerRoute({
|
||||
options: {
|
||||
access: 'internal',
|
||||
},
|
||||
isDangerous: true,
|
||||
endpoint: 'GET /internal/my_plugin/route',
|
||||
handler: async (resources) => {
|
||||
const { request, context, response, logger } = resources;
|
||||
return response.ok({
|
||||
body: 'Hello, my route!',
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Client calling options
|
||||
|
||||
Core HTTP allows certain options to be passed with the request, and you may want to include your own options as well.
|
||||
To do this, override the second generic argument when calling `createRepositoryClient`.
|
||||
|
||||
```javascript
|
||||
import { DefaultClientOptions } from '@kbn/server-route-repository-utils';
|
||||
import { createRepositoryClient } from '@kbn/server-route-repository-client';
|
||||
import type { MyPluginRouteRepository } from '../server/plugin';
|
||||
|
||||
interface MyPluginClientOptions {
|
||||
makeSafe: boolean;
|
||||
}
|
||||
|
||||
export type MyPluginRepositoryClient =
|
||||
ReturnType<typeof createRepositoryClient<MyPluginRouteRepository, DefaultClientOptions & MyPluginClientOptions>>;
|
||||
|
||||
class MyPlugin implements Plugin {
|
||||
public setup(core: CoreSetup) {
|
||||
const myPluginRepositoryClient =
|
||||
createRepositoryClient<MyPluginRouteRepository, DefaultClientOptions & MyPluginClientOptions>(core);
|
||||
|
||||
myPluginRepositoryClient.fetch('GET /internal/my_plugin/route', {
|
||||
makeSafe: true,
|
||||
headers: {
|
||||
my_plugin_header: 'I am a header',
|
||||
},
|
||||
}).then(console.log);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you don't want your route to have access to the options provided by Core HTTP, you could pass in only `MyPluginClientOptions`.
|
||||
|
|
|
@ -22,4 +22,6 @@ export type {
|
|||
ServerRoute,
|
||||
RouteParamsRT,
|
||||
RouteState,
|
||||
} from './src/typings';
|
||||
DefaultRouteCreateOptions,
|
||||
DefaultRouteHandlerResources,
|
||||
} from '@kbn/server-route-repository-utils';
|
||||
|
|
|
@ -5,16 +5,19 @@
|
|||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import {
|
||||
ServerRouteCreateOptions,
|
||||
ServerRouteHandlerResources,
|
||||
RouteParamsRT,
|
||||
ServerRoute,
|
||||
} from './typings';
|
||||
ServerRouteCreateOptions,
|
||||
ServerRouteHandlerResources,
|
||||
DefaultRouteHandlerResources,
|
||||
DefaultRouteCreateOptions,
|
||||
} from '@kbn/server-route-repository-utils';
|
||||
|
||||
export function createServerRouteFactory<
|
||||
TRouteHandlerResources extends ServerRouteHandlerResources,
|
||||
TRouteCreateOptions extends ServerRouteCreateOptions
|
||||
TRouteHandlerResources extends ServerRouteHandlerResources = DefaultRouteHandlerResources,
|
||||
TRouteCreateOptions extends ServerRouteCreateOptions = DefaultRouteCreateOptions
|
||||
>(): <
|
||||
TEndpoint extends string,
|
||||
TReturnType,
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
*/
|
||||
import Boom from '@hapi/boom';
|
||||
import { formatErrors, strictKeysRt } from '@kbn/io-ts-utils';
|
||||
import { RouteParamsRT } from '@kbn/server-route-repository-utils';
|
||||
import { isLeft } from 'fp-ts/lib/Either';
|
||||
import * as t from 'io-ts';
|
||||
import { isEmpty, isPlainObject, omitBy } from 'lodash';
|
||||
import { RouteParamsRT } from './typings';
|
||||
|
||||
interface KibanaRequestParams {
|
||||
body: unknown;
|
||||
|
|
|
@ -88,19 +88,19 @@ describe('registerRoutes', () => {
|
|||
registerRoutes({
|
||||
core: coreSetup,
|
||||
repository: {
|
||||
internal: {
|
||||
'GET /internal/app/feature': {
|
||||
endpoint: 'GET /internal/app/feature',
|
||||
handler: internalHandler,
|
||||
params: paramsRt,
|
||||
options: internalOptions,
|
||||
},
|
||||
public: {
|
||||
'GET /api/app/feature version': {
|
||||
endpoint: 'GET /api/app/feature version',
|
||||
handler: publicHandler,
|
||||
params: paramsRt,
|
||||
options: publicOptions,
|
||||
},
|
||||
error: {
|
||||
'GET /internal/app/feature/error': {
|
||||
endpoint: 'GET /internal/app/feature/error',
|
||||
handler: errorHandler,
|
||||
params: paramsRt,
|
||||
|
@ -124,11 +124,6 @@ describe('registerRoutes', () => {
|
|||
expect(internalRoute.options).toEqual(internalOptions);
|
||||
expect(internalRoute.validate).toEqual(routeValidationObject);
|
||||
|
||||
const [errorRoute] = get.mock.calls[1];
|
||||
expect(errorRoute.path).toEqual('/internal/app/feature/error');
|
||||
expect(errorRoute.options).toEqual(internalOptions);
|
||||
expect(errorRoute.validate).toEqual(routeValidationObject);
|
||||
|
||||
expect(getWithVersion).toHaveBeenCalledTimes(1);
|
||||
const [publicRoute] = getWithVersion.mock.calls[0];
|
||||
expect(publicRoute.path).toEqual('/api/app/feature');
|
||||
|
|
|
@ -14,10 +14,13 @@ import type { CoreSetup } from '@kbn/core-lifecycle-server';
|
|||
import type { Logger } from '@kbn/logging';
|
||||
import * as t from 'io-ts';
|
||||
import { merge, pick } from 'lodash';
|
||||
import { parseEndpoint } from '@kbn/server-route-repository-utils';
|
||||
import {
|
||||
ServerRoute,
|
||||
ServerRouteCreateOptions,
|
||||
parseEndpoint,
|
||||
} from '@kbn/server-route-repository-utils';
|
||||
import { decodeRequestParams } from './decode_request_params';
|
||||
import { routeValidationObject } from './route_validation_object';
|
||||
import type { ServerRoute, ServerRouteCreateOptions } from './typings';
|
||||
|
||||
const CLIENT_CLOSED_REQUEST = {
|
||||
statusCode: 499,
|
||||
|
@ -26,7 +29,7 @@ const CLIENT_CLOSED_REQUEST = {
|
|||
},
|
||||
};
|
||||
|
||||
export function registerRoutes({
|
||||
export function registerRoutes<TDependencies extends Record<string, any>>({
|
||||
core,
|
||||
repository,
|
||||
logger,
|
||||
|
@ -35,7 +38,7 @@ export function registerRoutes({
|
|||
core: CoreSetup;
|
||||
repository: Record<string, ServerRoute<string, any, any, any, ServerRouteCreateOptions>>;
|
||||
logger: Logger;
|
||||
dependencies: Record<string, any>;
|
||||
dependencies: TDependencies;
|
||||
}) {
|
||||
const routes = Object.values(repository);
|
||||
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
*/
|
||||
import * as t from 'io-ts';
|
||||
import { kibanaResponseFactory } from '@kbn/core/server';
|
||||
import { EndpointOf, ReturnOf, RouteRepositoryClient } from '@kbn/server-route-repository-utils';
|
||||
import { createServerRouteFactory } from './create_server_route_factory';
|
||||
import { decodeRequestParams } from './decode_request_params';
|
||||
import { EndpointOf, ReturnOf, RouteRepositoryClient } from './typings';
|
||||
|
||||
function assertType<TShape = never>(value: TShape) {
|
||||
return value;
|
||||
|
|
|
@ -1546,6 +1546,8 @@
|
|||
"@kbn/server-http-tools/*": ["packages/kbn-server-http-tools/*"],
|
||||
"@kbn/server-route-repository": ["packages/kbn-server-route-repository"],
|
||||
"@kbn/server-route-repository/*": ["packages/kbn-server-route-repository/*"],
|
||||
"@kbn/server-route-repository-client": ["packages/kbn-server-route-repository-client"],
|
||||
"@kbn/server-route-repository-client/*": ["packages/kbn-server-route-repository-client/*"],
|
||||
"@kbn/server-route-repository-utils": ["packages/kbn-server-route-repository-utils"],
|
||||
"@kbn/server-route-repository-utils/*": ["packages/kbn-server-route-repository-utils/*"],
|
||||
"@kbn/serverless": ["x-pack/plugins/serverless"],
|
||||
|
|
|
@ -6368,6 +6368,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/server-route-repository-client@link:packages/kbn-server-route-repository-client":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/server-route-repository-utils@link:packages/kbn-server-route-repository-utils":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue