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,