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

Add context type parameter to IRouter and createRouter #57674

Conversation

pgayvallet
Copy link
Contributor

@pgayvallet pgayvallet commented Feb 14, 2020

Summary

Fix #57588

Adds a Context type to the IRouter interface and concrete type to specify the type of the RequestHandlerContext expected to be accessible from the routes created with the router.

Documentation has not been updated yet. This is a POC to validate that this is the approach we want instead of the declare statement thing.

Checklist

For maintainers

@pgayvallet pgayvallet added Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc Feature:New Platform v8.0.0 v7.7.0 labels Feb 14, 2020
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-platform (Team:Platform)

Comment on lines 269 to 275
createRouter: <Context extends RequestHandlerContext = RequestHandlerContext>(
path: string,
plugin?: PluginOpaqueId
) => IRouter<Context>;
getAuthHeaders: GetAuthHeaders;
registerRouteHandlerContext: <T extends keyof RequestHandlerContext>(
pluginOpaqueId: PluginOpaqueId,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we are more or less deprecating the

declare module 'src/core/server' {
  interface RouteHandlerContext {
    myField: MyType;
  }
}

approach in favor of that, I think the

  registerRouteHandlerContext: <T extends keyof RequestHandlerContext>(
    contextName: T,
    provider: RequestHandlerContextProvider<T>
  ) => RequestHandlerContextContainer;

should change to something like

  registerRouteHandlerContext: <T extends string>(
    contextName: T,
    provider: RequestHandlerContextProvider<T>
  ) => RequestHandlerContextContainer;

Else, plugin would still need to use the declare statement to be able to add the key to the RequestHandlerContext before registering. WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only other option I can is making the context type here generic as well, so it'd be:

  registerRouteHandlerContext: <
    TContext extends RequestHandlerContext,
    T extends keyof TContext
  >(
    contextName: T,
    // this would probably need to be changed to accept a type arg as well
    // ...and possibly IContextContainer would need to be changed?
    provider: RequestHandlerContextProvider<T>
  ) => RequestHandlerContextContainer;

Only concern with this is that the generics in IContextContainer are already confusing...adding more generics might make this worse?

Copy link
Contributor Author

@pgayvallet pgayvallet Feb 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have enough generics in this part of the code yes and would like to avoid introducing new confusing ones yes. Also with your proposition, I think the call to registerRouteHandlerContext will always to specify the TContext type as it cannot be guessed from the call:

http.registerRouteHandlerContext<MyCustomRequestHandlerContext>('myKey', provider)

Although, my example is also false:

  registerRouteHandlerContext: <T extends string>(
    contextName: T,
    provider: RequestHandlerContextProvider<T>
  ) => RequestHandlerContextContainer;

Still doesn't work without the declare statement, as the provider is still expected to be a RequestHandlerContextProvider<T>, which is:

export type RequestHandlerContextProvider<
  TContextName extends keyof RequestHandlerContext
> = IContextProvider<RequestHandler<any, any, any>, TContextName>;

which is(...)

export type IContextProvider<
  THandler extends HandlerFunction<any>,
  TContextName extends keyof HandlerContextType<THandler>
> = (
  context: Partial<HandlerContextType<THandler>>,
  ...rest: HandlerParameters<THandler>
) =>
  | Promise<HandlerContextType<THandler>[TContextName]>
  | HandlerContextType<THandler>[TContextName];

Meaning it still expects to resolve it's type from a property of RequestHandlerContext.

Changing the registerRouteHandlerContext signature is going to be way harder than anticipated. Really not sure of the direction to take, the context types in src/core/utils/context.ts are really hard to follow... My guess is that we should cut the relation between the context name and the context provider when registering, as there is no longer a direct link between the name of the context and the provider's result type.

However that's some big changes as it means changing the context base types, and these are used elsewhere. So I'm starting to think your proposition is the only realistic one...

WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My guess is that we should cut the relation between the context name and the context provider when registering, as there is no longer a direct link between the name of the context and the provider's result type.

I personally prefer that we maintain type safety here so that we can be sure that the provider that is registered is actually returning what it is supposed to.

However, I agree the generics in src/core/utils/context.ts are already confusing. One option could be to only add the generics to registerRouteHandlerContext and cast it to any when passing it over to IContextContainer#registerContext. Reason I think this may be acceptable at this time is that we may be able to replace the super generic IContextContainer with a concrete implementation just for route handlers once we remove ApplicationService's context completely (already deprecated).

Copy link
Contributor Author

@pgayvallet pgayvallet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joshdover I implemented what we discussed about yesterday on slack. PTAL, would like a second opinion on whereas this is still worth the change.

Comment on lines -251 to +259
registerRouteHandlerContext: <T extends keyof RequestHandlerContext>(
contextName: T,
provider: RequestHandlerContextProvider<T>
registerRouteHandlerContext: <
TContextType extends RequestHandlerContext = RequestHandlerContext,
TContextName extends keyof TContextType = 'core'
>(
contextName: TContextName,
provider: RequestHandlerContextProvider<TContextName, TContextType>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, some explanations on that:

I introduced the new TContextType parametrized type. this one has a default, but is explicit. TContextName had no default, but was explicit. as defaulting generics must be declared after non-default ones, I could have done

registerRouteHandlerContext: <
    TContextName extends keyof TContextType
    TContextType extends RequestHandlerContext = RequestHandlerContext,
  >

However, the TContextName is inferred from the input parameter, and TContextType is not. That would have forced to declare the name generic on every calls:

http.registerRouteHandlerContext<'search', MyCustomHandlerContext>('search', provider)

The default=core trick allow to reverse the generics order, to have the inferred one last, and allow not to declare it:

http.registerRouteHandlerContext<MyCustomHandlerContext>('search', provider)

Comment on lines -280 to +292
registerRouteHandlerContext: setupDeps.core.http.registerRouteHandlerContext.bind(
null,
this.legacyId
),
createRouter: () => setupDeps.core.http.createRouter('', this.legacyId),
registerRouteHandlerContext: <
TContextType extends RequestHandlerContext = RequestHandlerContext,
TContextName extends keyof TContextType = 'core'
>(
contextName: TContextName,
provider: RequestHandlerContextProvider<TContextName, TContextType>
) => setupDeps.core.http.registerRouteHandlerContext(this.legacyId, contextName, provider),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the generic changes, I did not find a way to make TS happy about the bind call. I was forced to reproduce manually the registerRouteHandlerContext signature. See next comment for root cause.

registerRouteHandlerContext: jest.fn(),
registerRouteHandlerContext: jest.fn() as any,
Copy link
Contributor Author

@pgayvallet pgayvallet Feb 25, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where it become very ugly, and may force us to reconsider the whole PR:

The introduced generic 'breaks' the jest mock, as it makes some parameters non-inferrable:

Error:(104, 5) TS2322: Type 'Mock<any, any>' is not assignable to type 'MockInstance<RequestHandlerContextContainer, never> & (<TContextType extends RequestHandlerContext = RequestHandlerContext, TContextName extends keyof TContextType = "core">(contextName: TContextName, provider: IContextProvider<...>) => RequestHandlerContextContainer)'.
  Type 'Mock<any, any>' is not assignable to type 'MockInstance<RequestHandlerContextContainer, never>'.
    Types of property 'mock' are incompatible.
      Type 'MockContext<any, any>' is not assignable to type 'MockContext<RequestHandlerContextContainer, never>'.
        Type 'any' is not assignable to type 'never'.

This is caused by the (only possible) way jest retrieve the function parameters:

type Mocked<T> = {
        [P in keyof T]: T[P] extends (...args: any[]) => any
            ? MockInstance<ReturnType<T[P]>, ArgsType<T[P]>>
            : T[P] extends Constructable
            ? MockedClass<T[P]>
            : T[P]
    } &
        T;

type ArgsType<T> = T extends (...args: infer A) => any ? A : never;

But the parameters are no longer inferrable, probably due to the fact that the RequestHandlerContext is now generified. The effective type of the mock becomes MockInstance<RequestHandlerContextContainer, never>.

The root cause seems to be the type inferring we are doing for the HandlerContextType, which makes TS no longer able to infer the upper inferring when resolving jest.mock

export type HandlerContextType<T extends HandlerFunction<any>> = T extends HandlerFunction<infer U>
  ? U
  : never;

This forces to cast the jest.fn to any, which is kinda ugly. But I don't think we really have any other easy solution...

@joshdover
Copy link
Contributor

Should we close this PR? I know there are still some cleanup tasks, but this seems more or less stale at this point.

@kibanamachine
Copy link
Contributor

💔 Build Failed


Test Failures

X-Pack Jest Tests.x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__.MonitorList component renders the monitor list

Link to Jenkins

Standard Out

Failed Tests Reporter:
  - Test has not failed recently on tracked branches


Stack Trace

Error: expect(received).toMatchSnapshot()

Snapshot name: `MonitorList component renders the monitor list 1`

- Snapshot
+ Received

@@ -191,11 +191,11 @@
                            class="euiText euiText--extraSmall"
                          >
                            <div
                              class="euiTextColor euiTextColor--subdued"
                            >
-                             1897 Yr ago
+                             1898 Yr ago
                            </div>
                          </div>
                        </span>
                      </span>
                    </div>
@@ -369,11 +369,11 @@
                            class="euiText euiText--extraSmall"
                          >
                            <div
                              class="euiTextColor euiTextColor--subdued"
                            >
-                             1895 Yr ago
+                             1896 Yr ago
                            </div>
                          </div>
                        </span>
                      </span>
                    </div>
    at Object.it (/var/lib/jenkins/workspace/elastic+kibana+pipeline-pull-request/kibana/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list.test.tsx:123:23)
    at Object.asyncJestTest (/var/lib/jenkins/workspace/elastic+kibana+pipeline-pull-request/kibana/node_modules/jest-jasmine2/build/jasmineAsyncInstall.js:102:37)
    at resolve (/var/lib/jenkins/workspace/elastic+kibana+pipeline-pull-request/kibana/node_modules/jest-jasmine2/build/queueRunner.js:43:12)
    at new Promise (<anonymous>)
    at mapper (/var/lib/jenkins/workspace/elastic+kibana+pipeline-pull-request/kibana/node_modules/jest-jasmine2/build/queueRunner.js:26:19)
    at promise.then (/var/lib/jenkins/workspace/elastic+kibana+pipeline-pull-request/kibana/node_modules/jest-jasmine2/build/queueRunner.js:73:41)
    at process._tickCallback (internal/process/next_tick.js:68:7)

X-Pack Jest Tests.x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__.PingHistogram component renders the component without errors

Link to Jenkins

Standard Out

Failed Tests Reporter:
  - Test has not failed recently on tracked branches


Stack Trace

Error: expect(received).toMatchSnapshot()

Snapshot name: `PingHistogram component renders the component without errors 1`

- Snapshot
+ Received

@@ -3,11 +3,11 @@
      class="euiTitle euiTitle--xsmall"
    >
      Pings over time
    </h2>,
    <div
-     aria-label="Bar Chart showing uptime status over time from a year ago to a year ago."
+     aria-label="Bar Chart showing uptime status over time from 2 years ago to 2 years ago."
      style="height:100%;opacity:1;transition:opacity 0.2s"
    >
      <div
        class="echChart"
      >
    at Object.it (/var/lib/jenkins/workspace/elastic+kibana+pipeline-pull-request/kibana/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/ping_histogram.test.tsx:53:23)
    at Object.asyncJestTest (/var/lib/jenkins/workspace/elastic+kibana+pipeline-pull-request/kibana/node_modules/jest-jasmine2/build/jasmineAsyncInstall.js:102:37)
    at resolve (/var/lib/jenkins/workspace/elastic+kibana+pipeline-pull-request/kibana/node_modules/jest-jasmine2/build/queueRunner.js:43:12)
    at new Promise (<anonymous>)
    at mapper (/var/lib/jenkins/workspace/elastic+kibana+pipeline-pull-request/kibana/node_modules/jest-jasmine2/build/queueRunner.js:26:19)
    at promise.then (/var/lib/jenkins/workspace/elastic+kibana+pipeline-pull-request/kibana/node_modules/jest-jasmine2/build/queueRunner.js:73:41)
    at process._tickCallback (internal/process/next_tick.js:68:7)

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

@pgayvallet pgayvallet closed this May 26, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature:New Platform Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc v8.0.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add type argument for RouteHandlerContext
5 participants