Skip to content

Commit

Permalink
[kbn/server-route-repository] Add createRepositoryClient function (el…
Browse files Browse the repository at this point in the history
…astic#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 <[email protected]>
  • Loading branch information
miltonhultgren and kibanamachine authored Aug 9, 2024
1 parent 1a82dd6 commit 986001c
Show file tree
Hide file tree
Showing 22 changed files with 695 additions and 28 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions packages/kbn-server-route-repository-client/README.md
Original file line number Diff line number Diff line change
@@ -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 changes: 10 additions & 0 deletions packages/kbn-server-route-repository-client/index.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 13 additions & 0 deletions packages/kbn-server-route-repository-client/jest.config.js
Original file line number Diff line number Diff line change
@@ -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 changes: 5 additions & 0 deletions packages/kbn-server-route-repository-client/kibana.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "shared-browser",
"id": "@kbn/server-route-repository-client",
"owner": "@elastic/obs-knowledge-team"
}
6 changes: 6 additions & 0 deletions packages/kbn-server-route-repository-client/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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,
});
});
});
Original file line number Diff line number Diff line change
@@ -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 changes: 20 additions & 0 deletions packages/kbn-server-route-repository-client/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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",
]
}
17 changes: 17 additions & 0 deletions packages/kbn-server-route-repository-utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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>;
}
6 changes: 5 additions & 1 deletion packages/kbn-server-route-repository-utils/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,9 @@
"exclude": [
"target/**/*"
],
"kbn_references": []
"kbn_references": [
"@kbn/core-http-browser",
"@kbn/core-http-server",
"@kbn/core",
]
}
Loading

0 comments on commit 986001c

Please sign in to comment.