[NP] Allow custom validations in HTTP Routes apart from @kbn/config-schema#51919
[NP] Allow custom validations in HTTP Routes apart from @kbn/config-schema#51919afharo merged 44 commits intoelastic:masterfrom
Conversation
|
Pinging @elastic/kibana-platform (Team:Platform) |
💔 Build Failed |
pgayvallet
left a comment
There was a problem hiding this comment.
To answer the main question, I think this is a valid approach and follows what we discussed at EAH.
I have to admit, I'm a little concerned about the growing types complexity in http route types, even if I don't have suggestion on how to reduce this.
| P extends ObjectType | ValidateFunction<unknown>, | ||
| Q extends ObjectType | ValidateFunction<unknown>, | ||
| B extends ObjectType | Type<Buffer> | Type<Stream> | ValidateFunction<unknown>, |
There was a problem hiding this comment.
Question: (I know this is from a previous PR but,) why do we want to allow user to 'manually' handles body as raw buffer or stream again?
There was a problem hiding this comment.
It's a valid question. In the body options:
- When
parse: false, the body is returned as aBuffer - When
output: 'stream', the body is returned as aStream
So in the config-schema validation, the user needs to specify a valid type validation:
- If they do
schema.object({}, {allowUnknown: true}), it will not pass validation. - If they don't provide any validation, the body won't be passed through to them
| export type TypeOfFunctionReturn<T extends ValidateFunction<unknown>> = NonNullable< | ||
| ReturnType<T>['value'] | ||
| >; | ||
|
|
||
| export type ValidateSpecs<T> = ObjectType | Type<T> | ValidateFunction<T>; | ||
| export type ValidatedType<T extends ValidateSpecs<unknown>> = T extends Type<unknown> | ||
| ? TypeOf<T> | ||
| : T extends ValidateFunction<unknown> | ||
| ? TypeOfFunctionReturn<T> | ||
| : never; |
There was a problem hiding this comment.
This is some serious typescript we got there!
There was a problem hiding this comment.
Yeah... annoying as hell... To me, lines 49 and 51 should not exist, but TS is not that smart yet and in the right side of the conditional, it doesn't understand the exclusion of T not being Type<unknown> anymore :/
| function routeSchemasFromRouteConfig< | ||
| P extends ObjectType, | ||
| Q extends ObjectType, | ||
| B extends ObjectType | Type<Buffer> | Type<Stream> | ||
| P extends ObjectType | ValidateFunction<unknown>, | ||
| Q extends ObjectType | ValidateFunction<unknown>, | ||
| B extends ObjectType | Type<Buffer> | Type<Stream> | ValidateFunction<unknown> |
There was a problem hiding this comment.
I think you forgot to change this a few lines after?
kibana/src/core/server/http/router/router.ts
Lines 156 to 164 in 066613e
There was a problem hiding this comment.
Oh! I thought I did! Maybe it got lost when "fixing" the conflicts... Thank you for spotting it!
| Q extends ObjectType | ValidateFunction<unknown>, | ||
| B extends ObjectType | Type<Buffer> | Type<Stream> | ValidateFunction<unknown> |
There was a problem hiding this comment.
given the number of usages in our code, I think type aliases for
ObjectType | ValidateFunction<unknown>ObjectType | Type<Buffer> | Type<Stream> | ValidateFunction<unknown>
should be a good idea. Will also avoid heavy diffs in further validation type updates.
src/core/server/index.ts
Outdated
| ValidateFunctionReturn, | ||
| ValidateFunction, |
There was a problem hiding this comment.
We usually prefix exports from a given core submodule to avoid collision, I think we want to do this here? RouteXXX probably? Also I think RouteValidationFunction maybe is a better name than RouteValidateFunction?
| * Custom validate function (only if @kbn/config-schema is not a valid option in your plugin) | ||
| * @public | ||
| */ | ||
| export type ValidateFunction<T> = (data: any) => ValidateFunctionReturn<T>; |
There was a problem hiding this comment.
I wonder if we can be a little more restrictive than any here? I think it's a pain as
kibana/src/core/server/http/router/request.ts
Lines 114 to 127 in 066613e
req.params, req.query, req.payload have different types, but if possible I think at least something like data: reqParamsType | reqQueryType | reqBodyType is a little better (or is it just worse?)
Mega bonus point if we can context-type it depending on if its a query, params or body validation method, but not sure if possible.
There was a problem hiding this comment.
I guess the best we can assume here is req.params and req.query to always be Record<string, string>. req.payload is a bit harder to assume any type before validating it (although we've assumed {} up until now and it looks like it didn't harm, especially after Hapi's parsing). I'll see if it can be done without overcomplicating (even more) the typings
|
@elasticmachine merge upstream |
💔 Build Failed |
| } | ||
| | { | ||
| value?: undefined; | ||
| error: SchemaTypeError; |
There was a problem hiding this comment.
This task requires to decouple request validation from @kbm/config-schema. Here it still depends on the package. If '@kbn/config-schema' makes changes in the SchemaTypeError, all routes will have to adopt their validation. We should use an internal error type instead, that '@kbn/config-schema' should satisfy.
There was a problem hiding this comment.
Makes sense! I'll export a new type of Error
| if (typeof validationSpec === 'function') { | ||
| return validateFunction(validationSpec, data, namespace); | ||
| } else { | ||
| return validationSpec.validate(data, {}, namespace); |
There was a problem hiding this comment.
so any object with validate method is also allowed?
There was a problem hiding this comment.
You are right, TS does a validation but we should add run-time validation.
| P extends ObjectType, | ||
| Q extends ObjectType, | ||
| B extends ObjectType | Type<Buffer> | Type<Stream>, | ||
| P extends ObjectType | ValidateFunction<unknown>, |
There was a problem hiding this comment.
would be good to add at least one usage example. in integration tests, e.g.
interface Foo {
foo?: number;
}
test('invalid params', async () => {
const router = new Router('/foo', logger, enhanceWithContext);
const validate = {
params: (params: Foo) => {
// TODO add an example with an error
const value = typeof params.foo === 'number' ? params.foo || 0;
return { value };
},
};
const handler: RequestHandler<typeof validate.params> = (context, req, res) => {
console.log(req.body);
return res.ok({ body: String(req.params.test) });
};
router.get(
{
path: '/path-1',
validate,
},
handler
);
const validateKbnSchema = {
params: schema.object({
test: schema.string(),
}),
};
const handlerKbnSchema: RequestHandler<typeof validateKbnSchema.params> = (context, req, res) => {
console.log(req.body);
return res.ok({ body: String(req.params.test) });
};
router.get(
{
path: '/path-1',
validateKbnSchema,
},
handlerKbnSchema
);There was a problem hiding this comment.
There was a problem hiding this comment.
ah... I overlooked it and wrote my test instead. stupid me 🤣
| Q extends ObjectType, | ||
| B extends ObjectType | Type<Buffer> | Type<Stream>, | ||
| P extends ObjectType | ValidateFunction<unknown>, | ||
| Q extends ObjectType | ValidateFunction<unknown>, |
There was a problem hiding this comment.
shouldn't we fallback to = ValidateFunction<unknown> to make all types optional? it would simplify manual declaration:
const handler: RequestHandler<typeof validate.params> = (context, req, res) => { ... };| try { | ||
| result = validationSpec(data); | ||
| } catch (err) { | ||
| result = { error: new SchemaTypeError(err, []) }; |
There was a problem hiding this comment.
I don't follow what a desirable way to fail validation is: to throw an exception or return an exception?
There was a problem hiding this comment.
The desirable way is to return { value: validatedValue } or { error: RouteValidationError }. But since we don't have control over the validation method and it can throw an error, we don't want to break the process and throw a 500 error (or do we?).
💚 Build Succeeded |
afharo
left a comment
There was a problem hiding this comment.
Thanks for the comments. I'll fix the run-time validation and the fallback in the RequestHandler in a bit and will wait for your thoughts regarding the rest of the discussions.
💔 Build Failed |
|
@elasticmachine merge upstream |
💚 Build Succeeded |
💚 Build Succeeded |
|
Before doing an in depth review, I'd like some more rationale on why we're choosing this interface over the existing interface that For example, we could type this as such: type Validator<T> {
validate(value: unknown): T;
}The only way to expose a validation error would be by throwing an exception. There's lots we could debate here, but one huge advantage is this type should be 100% compatible with What tradeoffs are we making here and why are they worth the extra effort? |
|
In the end, there are no breaking changes, so I stroke-through the documentation checkbox |
|
@elastic/kibana-platform This PR should now be ready for a final review. |
|
ack: will review today |
joshdover
left a comment
There was a problem hiding this comment.
This design is looking great! Let us know if you need us to take over wrapping this up since you moved teams 😄
Some additional testing I'd like to see:
- Validation tests added to
src/core/server/http/integration_tests/router.test.tsthat verifies the 400 response code and error message. - Functional tests added to
test/plugin_functional/test_suites/core_plugins/server_plugins.tsthat verifies validation behavior for both config-schema validation and custom validator functions.
| * | ||
| * @public | ||
| */ | ||
| export type RouteValidateFunction<T> = (data: any) => RouteValidateFunctionReturn<T>; |
There was a problem hiding this comment.
It'd be nice if this function was supplied a factory method for creating RouteValidatorError instances so plugins don't have to import that class manually. I think that'd make this a bit more ergonomic and consistent with the response factories provided to route handlers.
There was a problem hiding this comment.
Yeah! Makes perfect sense! That even gave me the idea of providing a RouteValidationResolver that provides the ok and fail methods so the returned contract is maintained. An example of usage: https://github.com/afharo/kibana/blob/np-decouple-configSchema/docs/development/core/server/kibana-plugin-server.routevalidationfunction.md
| export type RouteValidateFunctionReturn<T> = | ||
| | { | ||
| value: T; | ||
| error?: never; | ||
| } | ||
| | { | ||
| value?: never; | ||
| error: RouteValidationError; | ||
| }; |
There was a problem hiding this comment.
I think the intellisense would be improved if this was inlined in the RouteValidateFunction type. It's only referenced in one other place which can be easily replaced with a ReturnType generic.
|
|
||
| /** | ||
| * Allowed property validation options: either @kbn/config-schema validations or custom validation functions | ||
| * |
There was a problem hiding this comment.
| * | |
| * See {@link RouteValidateFunction} for custom validation. |
| export type RouteValidationSpec<T> = ObjectType | Type<T> | RouteValidateFunction<T>; | ||
|
|
||
| // Ugly as hell but we need this conditional typing to have proper type inference | ||
| type RouteValidatedValue<T extends RouteValidationSpec<any> | undefined> = NonNullable< |
There was a problem hiding this comment.
How about renaming to RouteValidationResultType?
| /** | ||
| * Route validator class to define the validation logic for each new route. | ||
| * | ||
| * @private |
There was a problem hiding this comment.
I'm not sure about the behavior differences, but we've been using @internal
| return new RouteValidator({ params, query, body }, options); | ||
| } | ||
|
|
||
| constructor( |
There was a problem hiding this comment.
with the public static from above, maybe this should be private?
| } | ||
|
|
||
| private customValidation<T>( | ||
| validationRule?: RouteValidationSpec<T>, |
There was a problem hiding this comment.
I think you can make this non-optional and remove the if undefined below
There was a problem hiding this comment.
The thing is the validationRule might be not set. Aka: some routes check body but not params (and the other way around), so you get query and params as undefined and the current logic simply returns those 2 properties as {}.
I might prefer not to return anything at all. But our contract is to always return those params, either the validated value or {} if no validation is provided. The current master implementation does it in a very repetitive way:
kibana/src/core/server/http/router/request.ts
Lines 106 to 129 in 2b6ef5c
There was a problem hiding this comment.
That makes sense, but I think the defined-ness is already checked in the function that calls this one. It doesn't appear that this method is ever called with an undefined validationRule.
| private static validate<P, Q, B>( | ||
| req: Request, | ||
| routeSchemas: RouteSchemas<P, Q, B> | undefined | ||
| routeSchemas: RouteValidator<P, Q, B> | undefined |
There was a problem hiding this comment.
nit: rename routeSchemas to routeValidator and maybe make optional with ? instead of | undefined?
There was a problem hiding this comment.
It makes a difference, ? allows you not to send the parameter. | undefined enforces you to send the parameter, but it accepts the parameter to be undefined. This way you don't forget to send it by mistake ;)
Nevermind, I found a better approach so we always can send the RouteValidator and we can remove the if undefined in it.
joshdover
left a comment
There was a problem hiding this comment.
LGTM after these comments, thanks for the expanded test coverage!
| validationResolver: RouteValidationResolver, | ||
| data: any |
There was a problem hiding this comment.
I would expect these arguments to be in the reverse order to more closely match route handlers (where the input argument (request) comes before the response toolkit).
There was a problem hiding this comment.
I did it in that order to somehow enforce the user gets the param :)
But, in order to maintain our convention, I can switch them 👍
| /** | ||
| * The custom validation function if @kbn/config-schema is not a valid solution for your specific plugin requirements. | ||
| * | ||
| * The validation should look something like: |
There was a problem hiding this comment.
nit: add the @example tag above this line.
| * | ||
| * @public | ||
| */ | ||
| export interface RouteValidationResolver { |
There was a problem hiding this comment.
nit: can we call this RouteValidationResultFactory to closer match the KibanaResponseFactory name?
There was a problem hiding this comment.
I've also changed the interface so it returns the methods ok and badRequest (instead of fail) to be an even closer match to the response factory :)
…nterface to look more like the KibanaResponseFactory
💚 Build SucceededHistory
To update your PR or re-run it, just comment with: |
|
@joshdover thank you for your comments. I applied some final changes. If you could give it a final look for a sanity check, I'd appreciate it |
…chema (elastic#51919) * [NP] Allow custom validations in HTTP Routes apart from @kbn/config-schema * API docs * Allow validate function in the route handler (run-code validation) * Prefix RouteXXX + Params and Body Validation Aliases * Fix test broken by lodash * Update API docs * Add default types for simpler manual declaration * Add run-time validation of the RouteValidateSpec * Expose RouteValidationError instead of SchemaTypeError * RouteValidator as a class to match config-schema interface * Test for not-inline handler (need to check IRouter for elastic#47047) * Add preValidation of the input for a safer custom validation * Better types for RouteHandlers * [NP] Move route validation to RouteValidator wrapper * Use the class only internally but maintain the same API * Fix types * Ensure RouteValidator instance in KibanaRequest.from * Fix validator.tests (Buffer.from instead of new Buffer) * Default precheck should allow null values * Also allow undefined in preChecks * MR feedback fixes * Provide RouteValidationResolver to the validation function * Add functional tests * Fix new functional tests * Fix validator additional test * Fix test with new resolver * Remove unused import * Rename ValidationResolver to ValidationResultFactory and change the interface to look more like the KibanaResponseFactory Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
…chema (#51919) (#53714) * [NP] Allow custom validations in HTTP Routes apart from @kbn/config-schema * API docs * Allow validate function in the route handler (run-code validation) * Prefix RouteXXX + Params and Body Validation Aliases * Fix test broken by lodash * Update API docs * Add default types for simpler manual declaration * Add run-time validation of the RouteValidateSpec * Expose RouteValidationError instead of SchemaTypeError * RouteValidator as a class to match config-schema interface * Test for not-inline handler (need to check IRouter for #47047) * Add preValidation of the input for a safer custom validation * Better types for RouteHandlers * [NP] Move route validation to RouteValidator wrapper * Use the class only internally but maintain the same API * Fix types * Ensure RouteValidator instance in KibanaRequest.from * Fix validator.tests (Buffer.from instead of new Buffer) * Default precheck should allow null values * Also allow undefined in preChecks * MR feedback fixes * Provide RouteValidationResolver to the validation function * Add functional tests * Fix new functional tests * Fix validator additional test * Fix test with new resolver * Remove unused import * Rename ValidationResolver to ValidationResultFactory and change the interface to look more like the KibanaResponseFactory Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
…chema (elastic#51919) * [NP] Allow custom validations in HTTP Routes apart from @kbn/config-schema * API docs * Allow validate function in the route handler (run-code validation) * Prefix RouteXXX + Params and Body Validation Aliases * Fix test broken by lodash * Update API docs * Add default types for simpler manual declaration * Add run-time validation of the RouteValidateSpec * Expose RouteValidationError instead of SchemaTypeError * RouteValidator as a class to match config-schema interface * Test for not-inline handler (need to check IRouter for elastic#47047) * Add preValidation of the input for a safer custom validation * Better types for RouteHandlers * [NP] Move route validation to RouteValidator wrapper * Use the class only internally but maintain the same API * Fix types * Ensure RouteValidator instance in KibanaRequest.from * Fix validator.tests (Buffer.from instead of new Buffer) * Default precheck should allow null values * Also allow undefined in preChecks * MR feedback fixes * Provide RouteValidationResolver to the validation function * Add functional tests * Fix new functional tests * Fix validator additional test * Fix test with new resolver * Remove unused import * Rename ValidationResolver to ValidationResultFactory and change the interface to look more like the KibanaResponseFactory Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Summary
Allow the use of custom validation functions when defining the HTTP routes for those use cases where the use of
@kbn/config-schemais not available (like in the browser).Fixes #50179
Checklist
Use
strikethroughsto remove checklist items you don't feel are applicable to this PR.[ ] This was checked for cross-browser compatibility, including a check against IE11[ ] Any text added follows EUI's writing guidelines, uses sentence case text and includes i18n support[ ] Documentation was added for features that require explanation or tutorials[ ] This was checked for keyboard-only and screenreader accessibilityFor maintainers