Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[kbn/server-route-repository] Add support for defining response validations via Zod #205486

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export interface RequestFixtureOptions<P = any, Q = any, B = any> {
};
}

function createKibanaRequestMock<P = any, Q = any, B = any>({
function createKibanaRequestMock<P = any, Q = any, B = any, RequestBody = any>({
path = '/path',
headers = { accept: 'something/html' },
params = {},
Expand All @@ -84,7 +84,7 @@ function createKibanaRequestMock<P = any, Q = any, B = any>({
const queryString = stringify(query, { sort: false });
const url = new URL(`${path}${queryString ? `?${queryString}` : ''}`, 'http://localhost');

return kibanaRequestFactory<P, Q, B>(
return kibanaRequestFactory<P, Q, B, RequestBody>(
hapiMocks.createRequest({
app: kibanaRequestState,
auth,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,14 +254,24 @@ describe('Router', () => {
expect(
isConfigSchema(
(
validationSchemas as () => RouteValidatorRequestAndResponses<unknown, unknown, unknown>
validationSchemas as () => RouteValidatorRequestAndResponses<
unknown,
unknown,
unknown,
unknown
>
)().response![200].body!()
)
).toBe(true);
expect(
isConfigSchema(
(
validationSchemas as () => RouteValidatorRequestAndResponses<unknown, unknown, unknown>
validationSchemas as () => RouteValidatorRequestAndResponses<
unknown,
unknown,
unknown,
unknown
>
)().response![404].body!()
)
).toBe(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { kibanaResponseFactory } from './response';

describe('prepareResponseValidation', () => {
it('wraps only expected values in "once"', () => {
const validation: RouteValidator<unknown, unknown, unknown> = {
const validation: RouteValidator<unknown, unknown, unknown, unknown> = {
request: {},
response: {
200: {
Expand Down
10 changes: 6 additions & 4 deletions src/core/packages/http/router-server-internal/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ function isStatusCode(key: string) {
return !isNaN(parseInt(key, 10));
}

export function prepareResponseValidation(
validation: RouteValidatorFullConfigResponse
): RouteValidatorFullConfigResponse {
export function prepareResponseValidation<ResponseBody = unknown>(
validation: RouteValidatorFullConfigResponse<ResponseBody>
): RouteValidatorFullConfigResponse<ResponseBody> {
const responses = Object.entries(validation).map(([key, value]) => {
if (isStatusCode(key)) {
return [key, { ...value, ...(value.body ? { body: once(value.body) } : {}) }];
Expand All @@ -39,7 +39,9 @@ export function prepareResponseValidation(
return Object.fromEntries(responses);
}

function prepareValidation<P, Q, B>(validator: RouteValidator<P, Q, B>) {
function prepareValidation<P, Q, B, ResponseBody>(
validator: RouteValidator<P, Q, B, ResponseBody>
) {
if (isFullValidatorContainer(validator) && validator.response) {
return {
...validator,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ import type {
} from '@kbn/core-http-server';
import { validRouteSecurity } from '../security_route_config_validator';

export function isCustomValidation(
v: VersionedRouteCustomResponseBodyValidation | VersionedResponseBodyValidation
): v is VersionedRouteCustomResponseBodyValidation {
export function isCustomValidation<ResponseBody = unknown>(
v:
| VersionedRouteCustomResponseBodyValidation<ResponseBody>
| VersionedResponseBodyValidation<ResponseBody>
): v is VersionedRouteCustomResponseBodyValidation<ResponseBody> {
return 'custom' in v;
}

Expand All @@ -31,8 +33,8 @@ export function isCustomValidation(
* @param validation - versioned response body validation
* @internal
*/
export function unwrapVersionedResponseBodyValidation(
validation: VersionedResponseBodyValidation
export function unwrapVersionedResponseBodyValidation<ResponseBody = unknown>(
validation: VersionedResponseBodyValidation<ResponseBody>
): RouteValidationSpec<unknown> {
if (isCustomValidation(validation)) {
return validation.custom;
Expand Down
4 changes: 2 additions & 2 deletions src/core/packages/http/server-utils/src/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ import type {
* @param withoutSecretHeaders Whether we want to exclude secret headers
* @returns A KibanaRequest object
*/
export function kibanaRequestFactory<P, Q, B>(
export function kibanaRequestFactory<P, Q, B, RequestBody>(
req: RawRequest,
routeSchemas?: RouteValidator<P, Q, B> | RouteValidatorFullConfigRequest<P, Q, B>,
routeSchemas?: RouteValidator<P, Q, B, RequestBody> | RouteValidatorFullConfigRequest<P, Q, B>,
withoutSecretHeaders: boolean = true
): KibanaRequest<P, Q, B> {
return CoreKibanaRequest.from<P, Q, B>(req, routeSchemas, withoutSecretHeaders);
Expand Down
7 changes: 5 additions & 2 deletions src/core/packages/http/server/src/router/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ export interface RouteConfigOptions<Method extends RouteMethod> {
* Route specific configuration.
* @public
*/
export interface RouteConfig<P, Q, B, Method extends RouteMethod> {
export interface RouteConfig<P, Q, B, Method extends RouteMethod, ResponseBody = unknown> {
/**
* The endpoint _within_ the router path to register the route.
*
Expand Down Expand Up @@ -510,7 +510,10 @@ export interface RouteConfig<P, Q, B, Method extends RouteMethod> {
* });
* ```
*/
validate: RouteValidator<P, Q, B> | (() => RouteValidator<P, Q, B>) | false;
validate:
| RouteValidator<P, Q, B, ResponseBody>
| (() => RouteValidator<P, Q, B, ResponseBody>)
| false;

/**
* Defines the security requirements for a route, including authorization and authentication.
Expand Down
14 changes: 7 additions & 7 deletions src/core/packages/http/server/src/router/route_validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export type RouteValidatorFullConfigRequest<P, Q, B> = RouteValidatorConfig<P, Q
*
* @public
*/
export interface RouteValidatorFullConfigResponse {
export interface RouteValidatorFullConfigResponse<B> {
[statusCode: number]: {
/**
* A description of the response. This is required input for complete OAS documentation.
Expand All @@ -182,7 +182,7 @@ export interface RouteValidatorFullConfigResponse {
* A string representing the mime type of the response body.
*/
bodyContentType?: string;
body?: LazyValidator;
body?: LazyValidator<B>;
};
unsafe?: {
body?: boolean;
Expand All @@ -193,21 +193,21 @@ export interface RouteValidatorFullConfigResponse {
* An alternative form to register both request schema and all response schemas.
* @public
*/
export interface RouteValidatorRequestAndResponses<P, Q, B> {
export interface RouteValidatorRequestAndResponses<P, Q, B, ResponseBody> {
request: RouteValidatorFullConfigRequest<P, Q, B>;
/**
* Response schemas for your route.
*/
response?: RouteValidatorFullConfigResponse;
response?: RouteValidatorFullConfigResponse<ResponseBody>;
}

/**
* Type container for schemas used in route related validations
* @public
*/
export type RouteValidator<P, Q, B> =
export type RouteValidator<P, Q, B, ResponseBody> =
| RouteValidatorFullConfigRequest<P, Q, B>
| (RouteValidatorRequestAndResponses<P, Q, B> &
| (RouteValidatorRequestAndResponses<P, Q, B, ResponseBody> &
/* Help TS enforce union discrimination */ NotRouteValidatorFullConfigRequest);

interface NotRouteValidatorFullConfigRequest {
Expand All @@ -225,4 +225,4 @@ interface NotRouteValidatorFullConfigRequest {
* @return A @kbn/config-schema schema
* @public
*/
export type LazyValidator = () => Type<unknown> | ZodEsque<unknown>;
export type LazyValidator<B> = () => Type<B> | ZodEsque<B>;
4 changes: 2 additions & 2 deletions src/core/packages/http/server/src/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,8 @@ export interface RouterRoute {
* that the function will only be called once.
*/
validationSchemas?:
| (() => RouteValidator<unknown, unknown, unknown>)
| RouteValidator<unknown, unknown, unknown>
| (() => RouteValidator<unknown, unknown, unknown, unknown>)
| RouteValidator<unknown, unknown, unknown, unknown>
| false;
handler: (
req: Request,
Expand Down
2 changes: 1 addition & 1 deletion src/core/packages/http/server/src/router/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { ObjectType } from '@kbn/config-schema';
import type { RouteValidator } from './route_validator';
import { getRequestValidation, getResponseValidation, isFullValidatorContainer } from './utils';

type Validator = RouteValidator<unknown, unknown, unknown>;
type Validator = RouteValidator<unknown, unknown, unknown, unknown>;

describe('isFullValidatorContainer', () => {
it('correctly identifies RouteValidatorRequestAndResponses', () => {
Expand Down
12 changes: 6 additions & 6 deletions src/core/packages/http/server/src/router/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
RouteValidatorRequestAndResponses,
} from './route_validator';

type AnyRouteValidator = RouteValidator<unknown, unknown, unknown>;
type AnyRouteValidator = RouteValidator<unknown, unknown, unknown, unknown>;

/**
* {@link RouteValidator} is a union type of all possible ways that validation
Expand All @@ -24,7 +24,7 @@ type AnyRouteValidator = RouteValidator<unknown, unknown, unknown>;
*/
export function isFullValidatorContainer(
value: AnyRouteValidator
): value is RouteValidatorRequestAndResponses<unknown, unknown, unknown> {
): value is RouteValidatorRequestAndResponses<unknown, unknown, unknown, unknown> {
return 'request' in value;
}

Expand All @@ -34,7 +34,7 @@ export function isFullValidatorContainer(
* @public
*/
export function getRequestValidation<P, Q, B>(
value: RouteValidator<P, Q, B> | (() => RouteValidator<P, Q, B>)
value: RouteValidator<P, Q, B, unknown> | (() => RouteValidator<P, Q, B, unknown>)
): RouteValidatorFullConfigRequest<P, Q, B> {
if (typeof value === 'function') value = value();
return isFullValidatorContainer(value) ? value.request : value;
Expand All @@ -47,9 +47,9 @@ export function getRequestValidation<P, Q, B>(
*/
export function getResponseValidation(
value:
| RouteValidator<unknown, unknown, unknown>
| (() => RouteValidator<unknown, unknown, unknown>)
): undefined | RouteValidatorFullConfigResponse {
| RouteValidator<unknown, unknown, unknown, unknown>
| (() => RouteValidator<unknown, unknown, unknown, unknown>)
): undefined | RouteValidatorFullConfigResponse<unknown> {
if (typeof value === 'function') value = value();
return isFullValidatorContainer(value) ? value.response : undefined;
}
12 changes: 6 additions & 6 deletions src/core/packages/http/server/src/versioning/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,15 +238,15 @@ export interface VersionedRouter<Ctx extends RqCtx = RqCtx> {
export type VersionedRouteRequestValidation<P, Q, B> = RouteValidatorFullConfigRequest<P, Q, B>;

/** @public */
export interface VersionedRouteCustomResponseBodyValidation {
export interface VersionedRouteCustomResponseBodyValidation<B> {
/** A custom validation function */
custom: RouteValidationFunction<unknown>;
custom: RouteValidationFunction<B>;
}

/** @public */
export type VersionedResponseBodyValidation =
| LazyValidator
| VersionedRouteCustomResponseBodyValidation;
export type VersionedResponseBodyValidation<B> =
| LazyValidator<B>
| VersionedRouteCustomResponseBodyValidation<B>;

/**
* Map of response status codes to response schemas
Expand Down Expand Up @@ -293,7 +293,7 @@ export interface VersionedRouteResponseValidation {
* A string representing the mime type of the response body.
*/
bodyContentType?: string;
body?: VersionedResponseBodyValidation;
body?: VersionedResponseBodyValidation<unknown>;
};
unsafe?: { body?: boolean };
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,6 @@ export type {
DefaultRouteHandlerResources,
IoTsParamsObject,
ZodParamsObject,
ServerRouteHandlerReturnType,
TRouteResponse,
} from './src/typings';
Loading