diff --git a/packages/core/http/core-http-router-server-mocks/src/router.mock.ts b/packages/core/http/core-http-router-server-mocks/src/router.mock.ts
index a9995ec887a8e..e54272e4c3757 100644
--- a/packages/core/http/core-http-router-server-mocks/src/router.mock.ts
+++ b/packages/core/http/core-http-router-server-mocks/src/router.mock.ts
@@ -63,7 +63,7 @@ export interface RequestFixtureOptions
{
};
}
-function createKibanaRequestMock
({
+function createKibanaRequestMock
({
path = '/path',
headers = { accept: 'something/html' },
params = {},
@@ -84,7 +84,7 @@ function createKibanaRequestMock
({
const queryString = stringify(query, { sort: false });
const url = new URL(`${path}${queryString ? `?${queryString}` : ''}`, 'http://localhost');
- return kibanaRequestFactory
(
+ return kibanaRequestFactory
(
hapiMocks.createRequest({
app: kibanaRequestState,
auth,
diff --git a/src/core/packages/http/router-server-internal/src/router.test.ts b/src/core/packages/http/router-server-internal/src/router.test.ts
index 54e5e11c6252c..ab38f82369964 100644
--- a/src/core/packages/http/router-server-internal/src/router.test.ts
+++ b/src/core/packages/http/router-server-internal/src/router.test.ts
@@ -258,14 +258,24 @@ describe('Router', () => {
expect(
isConfigSchema(
(
- validationSchemas as () => RouteValidatorRequestAndResponses
+ validationSchemas as () => RouteValidatorRequestAndResponses<
+ unknown,
+ unknown,
+ unknown,
+ unknown
+ >
)().response![200].body!()
)
).toBe(true);
expect(
isConfigSchema(
(
- validationSchemas as () => RouteValidatorRequestAndResponses
+ validationSchemas as () => RouteValidatorRequestAndResponses<
+ unknown,
+ unknown,
+ unknown,
+ unknown
+ >
)().response![404].body!()
)
).toBe(true);
diff --git a/src/core/packages/http/router-server-internal/src/util.test.ts b/src/core/packages/http/router-server-internal/src/util.test.ts
index aebf1e3810826..a9092994918ef 100644
--- a/src/core/packages/http/router-server-internal/src/util.test.ts
+++ b/src/core/packages/http/router-server-internal/src/util.test.ts
@@ -14,7 +14,7 @@ import { kibanaResponseFactory } from './response';
describe('prepareResponseValidation', () => {
it('wraps only expected values in "once"', () => {
- const validation: RouteValidator = {
+ const validation: RouteValidator = {
request: {},
response: {
200: {
diff --git a/src/core/packages/http/router-server-internal/src/util.ts b/src/core/packages/http/router-server-internal/src/util.ts
index b4027d9211890..253f68ef6e3e9 100644
--- a/src/core/packages/http/router-server-internal/src/util.ts
+++ b/src/core/packages/http/router-server-internal/src/util.ts
@@ -26,9 +26,9 @@ function isStatusCode(key: string) {
return !isNaN(parseInt(key, 10));
}
-export function prepareResponseValidation(
- validation: RouteValidatorFullConfigResponse
-): RouteValidatorFullConfigResponse {
+export function prepareResponseValidation(
+ validation: RouteValidatorFullConfigResponse
+): RouteValidatorFullConfigResponse {
const responses = Object.entries(validation).map(([key, value]) => {
if (isStatusCode(key)) {
return [key, { ...value, ...(value.body ? { body: once(value.body) } : {}) }];
@@ -39,7 +39,9 @@ export function prepareResponseValidation(
return Object.fromEntries(responses);
}
-function prepareValidation(validator: RouteValidator
) {
+function prepareValidation
(
+ validator: RouteValidator
+) {
if (isFullValidatorContainer(validator) && validator.response) {
return {
...validator,
diff --git a/src/core/packages/http/router-server-internal/src/versioned_router/util.ts b/src/core/packages/http/router-server-internal/src/versioned_router/util.ts
index 475f69899d861..59cb21b5acb0c 100644
--- a/src/core/packages/http/router-server-internal/src/versioned_router/util.ts
+++ b/src/core/packages/http/router-server-internal/src/versioned_router/util.ts
@@ -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(
+ v:
+ | VersionedRouteCustomResponseBodyValidation
+ | VersionedResponseBodyValidation
+): v is VersionedRouteCustomResponseBodyValidation {
return 'custom' in v;
}
@@ -31,8 +33,8 @@ export function isCustomValidation(
* @param validation - versioned response body validation
* @internal
*/
-export function unwrapVersionedResponseBodyValidation(
- validation: VersionedResponseBodyValidation
+export function unwrapVersionedResponseBodyValidation(
+ validation: VersionedResponseBodyValidation
): RouteValidationSpec {
if (isCustomValidation(validation)) {
return validation.custom;
diff --git a/src/core/packages/http/server-utils/src/request.ts b/src/core/packages/http/server-utils/src/request.ts
index 7ac797727a704..c5e684edf45db 100644
--- a/src/core/packages/http/server-utils/src/request.ts
+++ b/src/core/packages/http/server-utils/src/request.ts
@@ -22,9 +22,9 @@ import type {
* @param withoutSecretHeaders Whether we want to exclude secret headers
* @returns A KibanaRequest object
*/
-export function kibanaRequestFactory(
+export function kibanaRequestFactory
(
req: RawRequest,
- routeSchemas?: RouteValidator
| RouteValidatorFullConfigRequest
,
+ routeSchemas?: RouteValidator
| RouteValidatorFullConfigRequest
,
withoutSecretHeaders: boolean = true
): KibanaRequest
{
return CoreKibanaRequest.from
(req, routeSchemas, withoutSecretHeaders);
diff --git a/src/core/packages/http/server/src/router/route.ts b/src/core/packages/http/server/src/router/route.ts
index adce8aae39e39..dd52b30ca9017 100644
--- a/src/core/packages/http/server/src/router/route.ts
+++ b/src/core/packages/http/server/src/router/route.ts
@@ -440,7 +440,7 @@ export interface RouteConfigOptions {
* Route specific configuration.
* @public
*/
-export interface RouteConfig {
+export interface RouteConfig
{
/**
* The endpoint _within_ the router path to register the route.
*
@@ -512,7 +512,10 @@ export interface RouteConfig
{
* });
* ```
*/
- validate: RouteValidator
| (() => RouteValidator
) | false;
+ validate:
+ | RouteValidator
+ | (() => RouteValidator
)
+ | false;
/**
* Defines the security requirements for a route, including authorization and authentication.
diff --git a/src/core/packages/http/server/src/router/route_validator.ts b/src/core/packages/http/server/src/router/route_validator.ts
index e77de1476049e..d82671a0a64dc 100644
--- a/src/core/packages/http/server/src/router/route_validator.ts
+++ b/src/core/packages/http/server/src/router/route_validator.ts
@@ -172,7 +172,7 @@ export type RouteValidatorFullConfigRequest
= RouteValidatorConfig
{
[statusCode: number]: {
/**
* A description of the response. This is required input for complete OAS documentation.
@@ -182,7 +182,7 @@ export interface RouteValidatorFullConfigResponse {
* A string representing the mime type of the response body.
*/
bodyContentType?: string;
- body?: LazyValidator;
+ body?: LazyValidator;
};
unsafe?: {
body?: boolean;
@@ -193,21 +193,21 @@ export interface RouteValidatorFullConfigResponse {
* An alternative form to register both request schema and all response schemas.
* @public
*/
-export interface RouteValidatorRequestAndResponses
{
+export interface RouteValidatorRequestAndResponses
{
request: RouteValidatorFullConfigRequest
;
/**
* Response schemas for your route.
*/
- response?: RouteValidatorFullConfigResponse;
+ response?: RouteValidatorFullConfigResponse;
}
/**
* Type container for schemas used in route related validations
* @public
*/
-export type RouteValidator =
+export type RouteValidator
=
| RouteValidatorFullConfigRequest
- | (RouteValidatorRequestAndResponses
&
+ | (RouteValidatorRequestAndResponses
&
/* Help TS enforce union discrimination */ NotRouteValidatorFullConfigRequest);
interface NotRouteValidatorFullConfigRequest {
@@ -225,4 +225,4 @@ interface NotRouteValidatorFullConfigRequest {
* @return A @kbn/config-schema schema
* @public
*/
-export type LazyValidator = () => Type | ZodEsque;
+export type LazyValidator = () => Type | ZodEsque;
diff --git a/src/core/packages/http/server/src/router/router.ts b/src/core/packages/http/server/src/router/router.ts
index f6a039a4130a9..6fcd22ac53113 100644
--- a/src/core/packages/http/server/src/router/router.ts
+++ b/src/core/packages/http/server/src/router/router.ts
@@ -132,8 +132,8 @@ export interface RouterRoute {
* that the function will only be called once.
*/
validationSchemas?:
- | (() => RouteValidator)
- | RouteValidator
+ | (() => RouteValidator)
+ | RouteValidator
| false;
handler: (
req: Request,
diff --git a/src/core/packages/http/server/src/router/utils.test.ts b/src/core/packages/http/server/src/router/utils.test.ts
index ee5896f3c6a34..195ebda7cf34f 100644
--- a/src/core/packages/http/server/src/router/utils.test.ts
+++ b/src/core/packages/http/server/src/router/utils.test.ts
@@ -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;
+type Validator = RouteValidator;
describe('isFullValidatorContainer', () => {
it('correctly identifies RouteValidatorRequestAndResponses', () => {
diff --git a/src/core/packages/http/server/src/router/utils.ts b/src/core/packages/http/server/src/router/utils.ts
index f82b7200a5ffb..37faf79ebb8d7 100644
--- a/src/core/packages/http/server/src/router/utils.ts
+++ b/src/core/packages/http/server/src/router/utils.ts
@@ -14,7 +14,7 @@ import {
RouteValidatorRequestAndResponses,
} from './route_validator';
-type AnyRouteValidator = RouteValidator;
+type AnyRouteValidator = RouteValidator;
/**
* {@link RouteValidator} is a union type of all possible ways that validation
@@ -24,7 +24,7 @@ type AnyRouteValidator = RouteValidator;
*/
export function isFullValidatorContainer(
value: AnyRouteValidator
-): value is RouteValidatorRequestAndResponses {
+): value is RouteValidatorRequestAndResponses {
return 'request' in value;
}
@@ -34,7 +34,7 @@ export function isFullValidatorContainer(
* @public
*/
export function getRequestValidation(
- value: RouteValidator
| (() => RouteValidator
)
+ value: RouteValidator
| (() => RouteValidator
)
): RouteValidatorFullConfigRequest
{
if (typeof value === 'function') value = value();
return isFullValidatorContainer(value) ? value.request : value;
@@ -47,9 +47,9 @@ export function getRequestValidation
(
*/
export function getResponseValidation(
value:
- | RouteValidator
- | (() => RouteValidator)
-): undefined | RouteValidatorFullConfigResponse {
+ | RouteValidator
+ | (() => RouteValidator)
+): undefined | RouteValidatorFullConfigResponse {
if (typeof value === 'function') value = value();
return isFullValidatorContainer(value) ? value.response : undefined;
}
diff --git a/src/core/packages/http/server/src/versioning/types.ts b/src/core/packages/http/server/src/versioning/types.ts
index 69f4c77d86c90..bedfdfcef312d 100644
--- a/src/core/packages/http/server/src/versioning/types.ts
+++ b/src/core/packages/http/server/src/versioning/types.ts
@@ -238,15 +238,15 @@ export interface VersionedRouter {
export type VersionedRouteRequestValidation = RouteValidatorFullConfigRequest
;
/** @public */
-export interface VersionedRouteCustomResponseBodyValidation {
+export interface VersionedRouteCustomResponseBodyValidation {
/** A custom validation function */
- custom: RouteValidationFunction;
+ custom: RouteValidationFunction;
}
/** @public */
-export type VersionedResponseBodyValidation =
- | LazyValidator
- | VersionedRouteCustomResponseBodyValidation;
+export type VersionedResponseBodyValidation =
+ | LazyValidator
+ | VersionedRouteCustomResponseBodyValidation;
/**
* Map of response status codes to response schemas
@@ -293,7 +293,7 @@ export interface VersionedRouteResponseValidation {
* A string representing the mime type of the response body.
*/
bodyContentType?: string;
- body?: VersionedResponseBodyValidation;
+ body?: VersionedResponseBodyValidation;
};
unsafe?: { body?: boolean };
}
diff --git a/src/platform/packages/shared/kbn-server-route-repository-utils/index.ts b/src/platform/packages/shared/kbn-server-route-repository-utils/index.ts
index a1e3ec45bd6f7..845e6c1d4bdfa 100644
--- a/src/platform/packages/shared/kbn-server-route-repository-utils/index.ts
+++ b/src/platform/packages/shared/kbn-server-route-repository-utils/index.ts
@@ -26,4 +26,6 @@ export type {
DefaultRouteHandlerResources,
IoTsParamsObject,
ZodParamsObject,
+ ServerRouteHandlerReturnType,
+ TRouteResponse,
} from './src/typings';
diff --git a/src/platform/packages/shared/kbn-server-route-repository-utils/src/typings.ts b/src/platform/packages/shared/kbn-server-route-repository-utils/src/typings.ts
index 6cc176113a590..35f7e3b15fa00 100644
--- a/src/platform/packages/shared/kbn-server-route-repository-utils/src/typings.ts
+++ b/src/platform/packages/shared/kbn-server-route-repository-utils/src/typings.ts
@@ -8,7 +8,6 @@
*/
import type { HttpFetchOptions } from '@kbn/core-http-browser';
-import type { IKibanaResponse, RouteAccess, RouteSecurity } from '@kbn/core-http-server';
import type {
KibanaRequest,
KibanaResponseFactory,
@@ -17,6 +16,14 @@ import type {
RouteConfigOptions,
RouteMethod,
} from '@kbn/core/server';
+import type {
+ HttpResponsePayload,
+ IKibanaResponse,
+ ResponseError,
+ RouteAccess,
+ RouteSecurity,
+ VersionedRouteResponseValidation,
+} from '@kbn/core-http-server';
import type { ServerSentEvent } from '@kbn/sse-utils';
import { z } from '@kbn/zod';
import * as t from 'io-ts';
@@ -123,17 +130,36 @@ type ServerRouteHandlerReturnTypeWithoutRecord =
| null
| void;
-type ServerRouteHandlerReturnType = ServerRouteHandlerReturnTypeWithoutRecord | Record;
+export type ServerRouteHandlerReturnType =
+ | ServerRouteHandlerReturnTypeWithoutRecord
+ | Record;
+
+export type TRouteResponse = {
+ [statusCode: number]: {
+ body: z.ZodSchema;
+ } & Omit;
+} & Omit;
+
+export type ExtractResponseStatusBodyTypes = z.infer<
+ T[Extract]['body']
+>;
type ServerRouteHandler<
TRouteHandlerResources extends ServerRouteHandlerResources,
TRouteParamsRT extends RouteParamsRT | undefined,
- TReturnType extends ServerRouteHandlerReturnType
+ TReturnType extends ServerRouteHandlerReturnType,
+ TResponses extends TRouteResponse | undefined = undefined
> = (
options: TRouteHandlerResources &
(TRouteParamsRT extends RouteParamsRT ? DecodedRequestParamsOfType : {})
) => Promise<
- TReturnType extends ServerRouteHandlerReturnTypeWithoutRecord
+ TResponses extends TRouteResponse
+ ? ExtractResponseStatusBodyTypes extends HttpResponsePayload | ResponseError
+ ?
+ | ExtractResponseStatusBodyTypes
+ | IKibanaResponse>
+ : ExtractResponseStatusBodyTypes
+ : TReturnType extends ServerRouteHandlerReturnTypeWithoutRecord
? TReturnType
: GuardAgainstInvalidRecord
>;
@@ -145,12 +171,14 @@ export type CreateServerRouteFactory<
TEndpoint extends string,
TReturnType extends ServerRouteHandlerReturnType,
TRouteParamsRT extends RouteParamsRT | undefined = undefined,
- TRouteAccess extends RouteAccess | undefined = undefined
+ TRouteAccess extends RouteAccess | undefined = undefined,
+ TResponses extends TRouteResponse | undefined = undefined
>(
options: {
endpoint: ValidateEndpoint extends true ? TEndpoint : never;
- handler: ServerRouteHandler;
+ handler: ServerRouteHandler;
params?: TRouteParamsRT;
+ responses?: TResponses;
security?: RouteSecurity;
} & Required<
{
@@ -168,7 +196,8 @@ export type CreateServerRouteFactory<
TRouteParamsRT,
TRouteHandlerResources,
Awaited,
- TRouteCreateOptions
+ TRouteCreateOptions,
+ TResponses
>
>;
@@ -177,17 +206,29 @@ export type ServerRoute<
TRouteParamsRT extends RouteParamsRT | undefined,
TRouteHandlerResources extends ServerRouteHandlerResources,
TReturnType extends ServerRouteHandlerReturnType,
- TRouteCreateOptions extends DefaultRouteCreateOptions | undefined
+ TRouteCreateOptions extends DefaultRouteCreateOptions | undefined,
+ TResponses extends TRouteResponse | undefined = undefined
> = {
endpoint: TEndpoint;
- handler: ServerRouteHandler;
+ handler: ServerRouteHandler;
security?: RouteSecurity;
} & (TRouteParamsRT extends RouteParamsRT ? { params: TRouteParamsRT } : {}) &
- (TRouteCreateOptions extends DefaultRouteCreateOptions ? { options: TRouteCreateOptions } : {});
+ (TRouteCreateOptions extends DefaultRouteCreateOptions
+ ? { options: TRouteCreateOptions }
+ : {}) & {
+ responses?: TRouteResponse;
+ };
export type ServerRouteRepository = Record<
string,
- ServerRoute
+ ServerRoute<
+ string,
+ RouteParamsRT | undefined,
+ any,
+ any,
+ ServerRouteCreateOptions | undefined,
+ TRouteResponse | undefined
+ >
>;
type ClientRequestParamsOfType =
@@ -218,8 +259,17 @@ export type EndpointOf =
export type ReturnOf<
TServerRouteRepository extends ServerRouteRepository,
TEndpoint extends keyof TServerRouteRepository
-> = TServerRouteRepository[TEndpoint] extends ServerRoute
- ? TReturnType extends IKibanaResponse
+> = TServerRouteRepository[TEndpoint] extends ServerRoute<
+ any,
+ any,
+ any,
+ infer TReturnType,
+ any,
+ infer TResponseType
+>
+ ? TResponseType extends TRouteResponse
+ ? ExtractResponseStatusBodyTypes
+ : TReturnType extends IKibanaResponse
? TWrappedResponseType
: TReturnType
: never;
@@ -241,7 +291,8 @@ export type ClientRequestParamsOf<
infer TRouteParamsRT,
any,
any,
- ServerRouteCreateOptions | undefined
+ ServerRouteCreateOptions | undefined,
+ any
>
? TRouteParamsRT extends RouteParamsRT
? ClientRequestParamsOfType
diff --git a/src/platform/packages/shared/kbn-server-route-repository/src/make_zod_validation_object.ts b/src/platform/packages/shared/kbn-server-route-repository/src/make_zod_validation_object.ts
index 23d50e5bdb25c..bb395dc57097a 100644
--- a/src/platform/packages/shared/kbn-server-route-repository/src/make_zod_validation_object.ts
+++ b/src/platform/packages/shared/kbn-server-route-repository/src/make_zod_validation_object.ts
@@ -7,8 +7,10 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
+import { RouteValidatorFullConfigResponse } from '@kbn/core-http-server';
+import { ExtractResponseStatusBodyTypes } from '@kbn/server-route-repository-utils/src/typings';
import { z, ZodObject } from '@kbn/zod';
-import { ZodParamsObject } from '@kbn/server-route-repository-utils';
+import { ZodParamsObject, TRouteResponse } from '@kbn/server-route-repository-utils';
import { noParamsValidationObject } from './validation_objects';
export function makeZodValidationObject(params: ZodParamsObject) {
@@ -19,6 +21,23 @@ export function makeZodValidationObject(params: ZodParamsObject) {
};
}
+export function makeZodResponsesValidationObject<
+ T extends TRouteResponse,
+ TReturnType = ExtractResponseStatusBodyTypes
+>(responseSchema: T): RouteValidatorFullConfigResponse {
+ const { unsafe, ...statusCodes } = responseSchema;
+ const response: RouteValidatorFullConfigResponse = { unsafe };
+
+ for (const [statusCode, validation] of Object.entries(statusCodes)) {
+ response[parseInt(statusCode, 10)] = {
+ ...validation,
+ body: validation.body ? () => validation.body : validation.body,
+ };
+ }
+
+ return response;
+}
+
function asStrict(schema: z.Schema) {
if (schema instanceof ZodObject) {
return schema.strict();
diff --git a/src/platform/packages/shared/kbn-server-route-repository/src/register_routes.test.ts b/src/platform/packages/shared/kbn-server-route-repository/src/register_routes.test.ts
index b13592c57ba59..196143f9e274b 100644
--- a/src/platform/packages/shared/kbn-server-route-repository/src/register_routes.test.ts
+++ b/src/platform/packages/shared/kbn-server-route-repository/src/register_routes.test.ts
@@ -12,10 +12,10 @@ import { loggerMock } from '@kbn/logging-mocks';
import { z } from '@kbn/zod';
import * as t from 'io-ts';
import { NEVER } from 'rxjs';
-import * as makeZodValidationObject from './make_zod_validation_object';
+import { ServerRouteRepository } from '@kbn/server-route-repository-utils';
+import * as makeZodValidations from './make_zod_validation_object';
import { registerRoutes } from './register_routes';
import { passThroughValidationObject, noParamsValidationObject } from './validation_objects';
-import { ServerRouteRepository } from '@kbn/server-route-repository-utils';
describe('registerRoutes', () => {
const post = jest.fn();
@@ -96,7 +96,8 @@ describe('registerRoutes', () => {
expect(internalRoute.options).toEqual({
access: 'internal',
});
- expect(internalRoute.validate).toEqual(noParamsValidationObject);
+ expect(internalRoute.validate.request).toEqual(noParamsValidationObject);
+ expect(internalRoute.validate.response).toBeUndefined();
const [internalRouteWithSecurity] = post.mock.calls[1];
@@ -280,9 +281,11 @@ describe('registerRoutes', () => {
});
describe('when using zod', () => {
- const makeZodValidationObjectSpy = jest.spyOn(
- makeZodValidationObject,
- 'makeZodValidationObject'
+ const makeZodValidationObjectSpy = jest.spyOn(makeZodValidations, 'makeZodValidationObject');
+
+ const makeZodResponsesValidationObjectSpy = jest.spyOn(
+ makeZodValidations,
+ 'makeZodResponsesValidationObject'
);
const zodParamsRt = z.object({
@@ -297,18 +300,34 @@ describe('registerRoutes', () => {
}),
});
+ const zodResponseRt = {
+ ['200']: {
+ body: z.object({
+ data: z.array(z.number()),
+ }),
+ },
+ };
+
it('uses Core validation', () => {
callRegisterRoutes({
'POST /internal/route': {
endpoint: 'POST /internal/route',
params: zodParamsRt,
- handler: jest.fn,
+ responses: zodResponseRt,
+ handler: jest.fn(),
},
});
const [internalRoute] = post.mock.calls[0];
expect(makeZodValidationObjectSpy).toHaveBeenCalledWith(zodParamsRt);
- expect(internalRoute.validate).toEqual(makeZodValidationObjectSpy.mock.results[0].value);
+ expect(internalRoute.validate.request).toEqual(
+ makeZodValidationObjectSpy.mock.results[0].value
+ );
+
+ expect(makeZodResponsesValidationObjectSpy).toHaveBeenCalledWith(zodResponseRt);
+ expect(internalRoute.validate.response).toEqual(
+ makeZodResponsesValidationObjectSpy.mock.results[0].value
+ );
});
it('passes on params', async () => {
@@ -317,6 +336,7 @@ describe('registerRoutes', () => {
'POST /internal/route': {
endpoint: 'POST /internal/route',
params: zodParamsRt,
+ responses: zodResponseRt,
handler,
},
});
@@ -372,15 +392,16 @@ describe('registerRoutes', () => {
it('bypasses Core validation', () => {
callRegisterRoutes({
- 'POST /internal/route': {
+ 'POST /internal/route/core': {
endpoint: 'POST /internal/route',
params: iotsParamsRt,
- handler: jest.fn,
+ handler: jest.fn(),
},
});
const [internalRoute] = post.mock.calls[0];
- expect(internalRoute.validate).toEqual(passThroughValidationObject);
+ expect(internalRoute.validate.request).toEqual(passThroughValidationObject);
+ expect(internalRoute.validate.response).toBeUndefined();
});
it('decodes params', async () => {
@@ -429,7 +450,7 @@ describe('registerRoutes', () => {
});
});
- function callRegisterRoutes(repository: any) {
+ function callRegisterRoutes(repository: Parameters[0]['repository']) {
registerRoutes({
core: coreSetup,
logger: mockLogger,
diff --git a/src/platform/packages/shared/kbn-server-route-repository/src/register_routes.ts b/src/platform/packages/shared/kbn-server-route-repository/src/register_routes.ts
index 90c4f42b9ce44..17c209ea97d7b 100644
--- a/src/platform/packages/shared/kbn-server-route-repository/src/register_routes.ts
+++ b/src/platform/packages/shared/kbn-server-route-repository/src/register_routes.ts
@@ -26,7 +26,10 @@ import { observableIntoEventSourceStream } from '@kbn/sse-utils-server';
import { isZod } from '@kbn/zod';
import { merge, omit } from 'lodash';
import { Observable, isObservable } from 'rxjs';
-import { makeZodValidationObject } from './make_zod_validation_object';
+import {
+ makeZodResponsesValidationObject,
+ makeZodValidationObject,
+} from './make_zod_validation_object';
import { validateAndDecodeParams } from './validate_and_decode_params';
import { noParamsValidationObject, passThroughValidationObject } from './validation_objects';
@@ -161,7 +164,12 @@ export function registerRoutes>({
access,
},
security,
- validate: validationObject,
+ validate: {
+ request: validationObject,
+ response: route.responses
+ ? makeZodResponsesValidationObject(route.responses)
+ : undefined,
+ },
},
wrappedHandler
);
@@ -177,6 +185,9 @@ export function registerRoutes>({
version,
validate: {
request: validationObject,
+ response: route.responses
+ ? makeZodResponsesValidationObject(route.responses)
+ : undefined,
},
},
wrappedHandler
diff --git a/src/platform/packages/shared/kbn-server-route-repository/src/test_types.ts b/src/platform/packages/shared/kbn-server-route-repository/src/test_types.ts
index 6b099c158f07f..754069cf6c832 100644
--- a/src/platform/packages/shared/kbn-server-route-repository/src/test_types.ts
+++ b/src/platform/packages/shared/kbn-server-route-repository/src/test_types.ts
@@ -128,6 +128,48 @@ createServerRouteFactory<{}, { tags: string[] }>()({
handler: async (resources) => {},
});
+// handler return, respects the responses
+createServerRouteFactory<{}, { tags: string[] }>()({
+ endpoint: 'GET /api/endpoint_with_response_validation 2023-10-31',
+ options: {
+ tags: [],
+ },
+ responses: {
+ 200: {
+ body: z.object({
+ success: z.literal(true),
+ data: z.array(z.object({ id: z.number() })),
+ }),
+ },
+ },
+ // @ts-expect-error Property 'data' is missing in type '{ success: true; }' but required in type 'InferType<{ id: NumberC; }>[]'.
+ handler: async (resources) => {
+ return { success: true as const };
+ },
+});
+
+// handler return, respects the responses with IKibanaResponseFactory
+createServerRouteFactory<{}, { tags: string[] }>()({
+ endpoint: 'GET /api/endpoint_with_response_validation 2023-10-31',
+ options: {
+ tags: [],
+ },
+ responses: {
+ 200: {
+ body: z.object({
+ success: z.literal(true),
+ data: z.array(z.object({ id: z.number() })),
+ }),
+ },
+ },
+ // @ts-expect-error Property 'data' is missing in type '{ success: true; }' but required in type 'InferType<{ id: NumberC; }>[]'.
+ handler: async (resources) => {
+ return kibanaResponseFactory.ok({
+ body: { data: [] },
+ });
+ },
+});
+
// cannot return observables that are not in the SSE structure
const route = createServerRouteFactory<{}, {}>()({
endpoint: 'POST /internal/endpoint_returning_observable_without_sse_structure',
@@ -228,6 +270,60 @@ const repository = {
return of({ type: 'foo' as const, streamed_response: true });
},
}),
+ ...createServerRoute({
+ endpoint: 'GET /internal/endpoint_with_response_validation_zod',
+ params: z.object({
+ query: z.object({
+ start: z.string(),
+ }),
+ }),
+ responses: {
+ 200: {
+ body: z.object({
+ success: z.literal(true),
+ data: z.array(z.object({ id: z.number() })),
+ }),
+ },
+ 202: {
+ body: z.object({
+ success: z.literal(true),
+ data: z.array(z.object({ id: z.number() })).length(0),
+ }),
+ },
+ 204: {
+ body: z.object({
+ success: z.literal(false),
+ message: z.string(),
+ }),
+ },
+ },
+ async handler(resources) {
+ const start = resources.params.query.start;
+
+ if (start === 'something-1') {
+ return {
+ success: true as const,
+ data: [{ id: 1 }, { id: 2 }],
+ };
+ }
+
+ if (start === 'something-2') {
+ return kibanaResponseFactory.accepted({
+ body: {
+ success: true as const,
+ data: [{ id: 1 }, { id: 2 }],
+ },
+ });
+ }
+
+ // Test returning an error response
+ if (start === 'something-3') {
+ return kibanaResponseFactory.notFound({ body: { message: 'Not found' } });
+ }
+
+ return { success: false as const, message: 'No change!' };
+ },
+ }),
};
type TestRepository = typeof repository;
@@ -240,6 +336,7 @@ assertType>>([
'GET /internal/endpoint_with_optional_params',
'GET /internal/endpoint_with_params_zod',
'GET /internal/endpoint_with_optional_params_zod',
+ 'GET /internal/endpoint_with_response_validation_zod',
'GET /internal/endpoint_returning_result',
'GET /internal/endpoint_returning_kibana_response',
]);
@@ -372,6 +469,29 @@ client
}>(res);
});
+client
+ .fetch('GET /internal/endpoint_with_response_validation_zod', {
+ params: {
+ query: {
+ start: '',
+ },
+ },
+ timeout: 1,
+ })
+ .then((res) => {
+ if (res.success) {
+ assertType<{
+ success: true;
+ data: Array<{ id: number }>;
+ }>(res);
+ } else {
+ assertType<{
+ success: false;
+ message: string;
+ }>(res);
+ }
+ });
+
client
.fetch('GET /internal/endpoint_returning_result', {
timeout: 1,