Skip to content

Commit

Permalink
[HTTP] Versioned router implementation (#153543)
Browse files Browse the repository at this point in the history
## Summary

Implements the designs from
#151596

* Move `packages/versioning/*` into `packages/core/http` to follow
existing structure more closely
* Implements the first iteration of the versioned router as a
wrapper/layer around the existing router
* Adds some integration tests
* Future work needed! Once we have a the versioned spec we should
implement it in this wrapper layer
* Validation is a little bit tricky because of when the
`CoreKibanaResponse` object is instantiated, the approach taken here is
to replace body, params, query on the route-level's request object

Closes #149286

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
jloleysens and kibanamachine authored Mar 28, 2023
1 parent d6a6745 commit e8055e8
Show file tree
Hide file tree
Showing 24 changed files with 789 additions and 287 deletions.
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ packages/core/http/core-http-router-server-mocks @elastic/kibana-core
packages/core/http/core-http-server @elastic/kibana-core
packages/core/http/core-http-server-internal @elastic/kibana-core
packages/core/http/core-http-server-mocks @elastic/kibana-core
packages/core/http/core-http-versioned-router-server-internal @elastic/kibana-core
packages/core/i18n/core-i18n-browser @elastic/kibana-core
packages/core/i18n/core-i18n-browser-internal @elastic/kibana-core
packages/core/i18n/core-i18n-browser-mocks @elastic/kibana-core
Expand Down Expand Up @@ -284,7 +285,6 @@ packages/core/usage-data/core-usage-data-base-server-internal @elastic/kibana-co
packages/core/usage-data/core-usage-data-server @elastic/kibana-core
packages/core/usage-data/core-usage-data-server-internal @elastic/kibana-core
packages/core/usage-data/core-usage-data-server-mocks @elastic/kibana-core
packages/core/versioning/core-version-http-server @elastic/kibana-core
x-pack/plugins/cross_cluster_replication @elastic/platform-deployment-management
packages/kbn-crypto @elastic/kibana-security
packages/kbn-crypto-browser @elastic/kibana-core
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@
"@kbn/core-http-router-server-internal": "link:packages/core/http/core-http-router-server-internal",
"@kbn/core-http-server": "link:packages/core/http/core-http-server",
"@kbn/core-http-server-internal": "link:packages/core/http/core-http-server-internal",
"@kbn/core-http-versioned-router-server-internal": "link:packages/core/http/core-http-versioned-router-server-internal",
"@kbn/core-i18n-browser": "link:packages/core/i18n/core-i18n-browser",
"@kbn/core-i18n-browser-internal": "link:packages/core/i18n/core-i18n-browser-internal",
"@kbn/core-i18n-server": "link:packages/core/i18n/core-i18n-server",
Expand Down Expand Up @@ -329,7 +330,6 @@
"@kbn/core-usage-data-base-server-internal": "link:packages/core/usage-data/core-usage-data-base-server-internal",
"@kbn/core-usage-data-server": "link:packages/core/usage-data/core-usage-data-server",
"@kbn/core-usage-data-server-internal": "link:packages/core/usage-data/core-usage-data-server-internal",
"@kbn/core-version-http-server": "link:packages/core/versioning/core-version-http-server",
"@kbn/cross-cluster-replication-plugin": "link:x-pack/plugins/cross_cluster_replication",
"@kbn/crypto": "link:packages/kbn-crypto",
"@kbn/crypto-browser": "link:packages/kbn-crypto-browser",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export {
isKibanaResponse,
KibanaResponse,
} from './src/response';
export { RouteValidator } from './src/validator';
11 changes: 11 additions & 0 deletions packages/core/http/core-http-server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,14 @@ export type {
HttpServiceSetup,
HttpServiceStart,
} from './src/http_contract';

export type {
AddVersionOpts,
VersionedRouteRequestValidation,
VersionedRouteResponseValidation,
ApiVersion,
VersionedRoute,
VersionedRouteConfig,
VersionedRouteRegistrar,
VersionedRouter,
} from './src/versioning';
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
# @kbn/core-version-http-server

This package contains types for sever-side HTTP versioning.
This folder contains types for sever-side HTTP versioning.

## Experimental

Expand Down
18 changes: 18 additions & 0 deletions packages/core/http/core-http-server/src/versioning/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* 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 type {
ApiVersion,
AddVersionOpts,
VersionedRouteRequestValidation,
VersionedRouteResponseValidation,
VersionedRoute,
VersionedRouteConfig,
VersionedRouteRegistrar,
VersionedRouter,
} from './types';
225 changes: 225 additions & 0 deletions packages/core/http/core-http-server/src/versioning/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
/*
* 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 { Type } from '@kbn/config-schema';
import type { MaybePromise } from '@kbn/utility-types';
import type {
RouteConfig,
RouteMethod,
RequestHandler,
IKibanaResponse,
RouteConfigOptions,
RouteValidatorFullConfig,
RequestHandlerContextBase,
RouteValidationFunction,
} from '../..';

type RqCtx = RequestHandlerContextBase;

/**
* Assuming that version will be a monotonically increasing number where: version > 0.
* @experimental
*/
export type ApiVersion = `${number}`;

/**
* Configuration for a versioned route
* @experimental
*/
export type VersionedRouteConfig<Method extends RouteMethod> = Omit<
RouteConfig<unknown, unknown, unknown, Method>,
'validate' | 'options'
> & {
options?: Omit<RouteConfigOptions<Method>, 'access'>;
/** See {@link RouteConfigOptions<RouteMethod>['access']} */
access: RouteConfigOptions<Method>['access'];
};

/**
* Create an {@link VersionedRoute | versioned route}.
*
* @param config - The route configuration
* @returns A versioned route
* @experimental
*/
export type VersionedRouteRegistrar<Method extends RouteMethod, Ctx extends RqCtx = RqCtx> = (
config: VersionedRouteConfig<Method>
) => VersionedRoute<Method, Ctx>;

/**
* A router, very similar to {@link IRouter} that will return an {@link VersionedRoute}
* instead.
*
* @example
* const versionedRoute = versionedRouter
* .post({
* access: 'internal',
* path: '/api/my-app/foo/{id?}',
* options: { timeout: { payload: 60000 } },
* })
* .addVersion(
* {
* version: '1',
* validate: {
* 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: {
* 200: {
* 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: {
* 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: {
* 200: {
* body: schema.object({ fooName: 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 } });
* }
* )
* // BREAKING CHANGES: Enforce min/max length on fooString
* .addVersion(
* {
* version: '3',
* validate: {
* 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: {
* 200: {
* body: schema.object({ fooName: 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 VersionedRouter<Ctx extends RqCtx = RqCtx> {
/** @experimental */
get: VersionedRouteRegistrar<'get', Ctx>;
/** @experimental */
put: VersionedRouteRegistrar<'put', Ctx>;
/** @experimental */
post: VersionedRouteRegistrar<'post', Ctx>;
/** @experimental */
patch: VersionedRouteRegistrar<'patch', Ctx>;
/** @experimental */
delete: VersionedRouteRegistrar<'delete', Ctx>;
}

/** @experimental */
export type VersionedRouteRequestValidation<P, Q, B> = RouteValidatorFullConfig<P, Q, B>;

/** @experimental */
export interface VersionedRouteResponseValidation<R> {
[statusCode: number]: { body: RouteValidationFunction<R> | Type<R> };
unsafe?: { body?: boolean };
}

/**
* Versioned route validation
* @experimental
*/
interface FullValidationConfig<P, Q, B, R> {
/**
* Validation to run against route inputs: params, query and body
* @experimental
*/
request?: VersionedRouteRequestValidation<P, Q, B>;
/**
* Validation to run against route output
* @note This validation is only intended to run in development. Do not use this
* for setting default values!
* @experimental
*/
response?: VersionedRouteResponseValidation<R>;
}

/**
* Options for a versioned route. Probably needs a lot more options like sunsetting
* of an endpoint etc.
* @experimental
*/
export interface AddVersionOpts<P, Q, B, R> {
/**
* Version to assign to this route
* @experimental
*/
version: ApiVersion;
/**
* Validation for this version of a route
* @experimental
*/
validate: false | FullValidationConfig<P, Q, B, R>;
}

/**
* A versioned route
* @experimental
*/
export interface VersionedRoute<
Method extends RouteMethod = RouteMethod,
Ctx extends RqCtx = RqCtx
> {
/**
* Add a new version of this route
* @param opts {@link AddVersionOpts | Options} for this version of a route
* @param handler The request handler for this version of a route
* @returns A versioned route, allows for fluent chaining of version declarations
* @experimental
*/
addVersion<P = unknown, Q = unknown, B = unknown, R = any>(
options: AddVersionOpts<P, Q, B, R>,
handler: (
...params: Parameters<RequestHandler<P, Q, B, Ctx>>
) => MaybePromise<IKibanaResponse<R>>
): VersionedRoute<Method, Ctx>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# @kbn/core-http-versioned-router-server-internal

This package contains the implementation for sever-side HTTP versioning.

## Experimental

See notes in `@kbn/core-http-server/src/versioning`
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,4 @@
* Side Public License, v 1.
*/

// TODO: export once types are ready
export {};
export { CoreVersionedRouter } from './src';
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
module.exports = {
preset: '@kbn/test/jest_node',
rootDir: '../../../..',
roots: ['<rootDir>/packages/core/versioning/core-version-http-server'],
roots: ['<rootDir>/packages/core/http/core-http-versioned-router-server-internal'],
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/core-version-http-server",
"id": "@kbn/core-http-versioned-router-server-internal",
"owner": "@elastic/kibana-core"
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@kbn/core-version-http-server",
"name": "@kbn/core-http-versioned-router-server-internal",
"private": true,
"version": "1.0.0",
"author": "Kibana Core",
Expand Down
Loading

0 comments on commit e8055e8

Please sign in to comment.