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

Improve IRouter typings #47047

Closed
mshustov opened this issue Oct 1, 2019 · 4 comments
Closed

Improve IRouter typings #47047

mshustov opened this issue Oct 1, 2019 · 4 comments
Labels
discuss Feature:New Platform Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc

Comments

@mshustov
Copy link
Contributor

mshustov commented Oct 1, 2019

Problem

the current IRouter implementation has following interface:

import { ObjectType } from '@kbn/config-schema';
get: <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(
    route: RouteConfig<P, Q, B>,
    handler: RequestHandler<P, Q, B>
) => void;

We utilize 3 generics here to allow TS to refer types in route.validate and handler. That provides easy-to-start interface when you inline a route handler, at the cost of coupling all interfaces to @kbn/config-schema library

import { ObjectType } from '@kbn/config-schema';
router.get({
  path: '/',
  validate: {
    body: schema.object({
      bar: schema.string(),
    }),
  },
  (context, req, res) => { // body infers types from validate.body
    req.body.bar; // string
  };
});
// or
const validate =  {
  body: schema.object({
    bar: schema.string(),
  }),
};
router.get({
  path: '/',
  validate,
  (context, req, res) => {
    req.body.bar; // string
  };
});
// If you don't inline a route handler you have to specify typings manually:
const validate =  {
  body: schema.object({
    bar: schema.string(),
  }),
};
const handler = (
  context: RequestHandlerContext,
  req: KibanaRequest<unknown, unknown, TypeOf<typeof validate.body>>,
  res: KibanaResponseFactory
) => {
  req.body.bar; // string
};

// or 
const handler: RequestHandler<ObjectType<any>, ObjectType<any>, typeof validate.body> = (context, req, res) => {
  req.body.bar; // string
};

router.get({
  path: '/',
  validate,
  handler,
});

That creates confusion for plugin authors when you want to a wrapper on top of IRouter methods. An example from @jasonrhodes, when route wrapper allows configuring request method. This wrapper has to define all 3 generics for IRouter methods to provide type safety. This requirement is not obvious to plugin authors.

  type MyRouteConfig<
    P extends ObjectType,
    Q extends ObjectType,
    B extends ObjectType
  > = RouteConfig<P, Q, B> & {
    method: RouteMethod;
  };

 function registerRoute<P extends ObjectType, Q extends ObjectType, B extends ObjectType>(
  route: MyRouteConfig<P, Q, B>,
  handler: RequestHandler<P, Q, B>
) {
  const { method, ...options } = route;
  router[method](options, handler);
}

We can minimize necessity in IRouter wrappers if provide an interface where route.validation and handler are part of the same object.

interface NewRouterConfig<P extends ObjectType, Q extends ObjectType, B extends ObjectType> {
  path: string,
  method: 'get' | 'post' | 'put' | 'delete';
  options: Options;
  validate: Schema<P, Q, B>;
  handler: RouteHandler<P, Q, B>;
}
IRouter{
  public add(config: RouterConfig): void
}

But this doesn't solve the problem that plugin authors have to declare generics for custom wrappers

type MyCustomConfig<P extends ObjectType, Q extends ObjectType, B extends ObjectType> = NewRouterConfig<P, Q, B> & { myProperty: string };

We cannot get rid of all generics completely, because we want that TS to infer validation types with route handler types out-of-the-box. Although we can simplify typings if we switch generic interfaces to typescript primitives instead of using @kbn/config-schema.

interface IRouter {
  get: <P extends object = {}, Q extends object = {}, B extends object = {}>(
     route: RouteConfig<P, Q, B>,
     handler: RequestHandler<P, Q, B>
  ) => void;
}

interface RouteConfig<
  P extends object = {},
  Q extends object = {},
  B extends object = {}
>{
  validate(params: unknown, query: unknown, body: unknown)  => {
    params?: P;
    query?: Q;
    body?: B;
  };
}

const validate =  {
  body: schema.object({
    bar: schema.string(),
  }),
};
router.get<{}, {}, TypeOf<typeof validate.body>>({
  path: '/',
  validate: (params: unknown, query: unknown, body: unknown) => {
    return {
      body: validate.body.validate(body, {}, 'request body'),
    };
  },
  handler: (context, req, res) => {
    req.body.bar; // string
  },
});

This change allows a plugin to use any validation library they want or do not implement validation at all. This is the main problem of this approach as well, I'd prefer platform forces good practices, therefore it's a no-go option for us. Want to hear @elastic/kibana-platform opinion. Related discussion: #44170

Possible actions:

  • introduce common purpose interface core.http.route instead of IRouter.get, IRouter.post, etc. as proposed in [Discuss] Add NP core.http.route #44174
  • document how to register routes with examples from this issue
@mshustov mshustov added discuss Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc Feature:New Platform labels Oct 1, 2019
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-platform

@jasonrhodes
Copy link
Member

jasonrhodes commented Oct 1, 2019

I'd prefer platform forces good practices, therefore it's a no-go option for us

My opinion on this is that this stance is likely to cause you and others more pain than it will be worth in the end, as people will still eventually hack around the limitations but others will be penalized for not being able to easily do things a slightly different way (such as using io-ts, as a very big example I can already think of off the top of my head). Because of that, it seems like that last example would be a great approach.

I think the harder and more complicated it is to do simple things like "create a wrapper for a request handler", the worse the migration effort is going to go, overall. My $0.02. Thanks for writing this up!

@rudolf
Copy link
Contributor

rudolf commented Oct 18, 2019

We can minimize necessity in IRouter wrappers if provide an interface where route.validation and handler are part of the same object.

I just noticed we have an existing abstraction in x-pack that creates a router API similar to what's in NP now in favour of the config based routing: #30299

@bmcconaghy Was there a reason a router with methods like router.post was preferred above hapi's server.route which takes a configuration object?

@mshustov
Copy link
Contributor Author

should be done as a part of #50179

afharo added a commit that referenced this issue Dec 20, 2019
…chema (#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 #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>
afharo added a commit to afharo/kibana that referenced this issue Dec 20, 2019
…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>
afharo added a commit that referenced this issue Dec 21, 2019
…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>
jkelastic pushed a commit to jkelastic/kibana that referenced this issue Jan 8, 2020
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discuss Feature:New Platform Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc
Projects
None yet
Development

No branches or pull requests

4 participants