-
Notifications
You must be signed in to change notification settings - Fork 2k
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
apollo-engine-reporting: update to newer plugin interface #2441
Conversation
Hmm, it builds for me locally. Advice on what build files need to be tweaked would be helpful. |
|
||
// `operationName` is set based on the operation AST, so it is defined | ||
// even if no `request.operationName` was passed in. | ||
// It will be set to `null` for an anonymous operation. | ||
readonly operationName?: string | null; | ||
readonly operation?: OperationDefinitionNode; | ||
|
||
readonly persistedQueryHit?: boolean; | ||
readonly persistedQueryRegister?: boolean; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking we may want to put persistedQueryHit
and persistedQueryRegister
on a metrics
property (of type GraphQLRequestMetrics
), to group these together with information about cache hits.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
@@ -21,7 +21,7 @@ export class Dispatcher<T> { | |||
this.targets.map(target => { | |||
const method = target[methodName]; | |||
if (method && typeof method === 'function') { | |||
return method(...args); | |||
return method.apply(target, args); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch! Sorry I missed this.
@@ -50,13 +52,17 @@ export interface GraphQLRequestContext<TContext = Record<string, any>> { | |||
readonly queryHash?: string; | |||
|
|||
readonly document?: DocumentNode; | |||
readonly originalDocumentString?: string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm still not really sure this is what we want, because the cache key shouldn't depend on the whole document if it contains more than one operation. So ideally we'd have a well-defined uniform notion of an operationID
. But even if we do want to depend on the whole document for now, couldn't we use the queryHash
instead? (We should probably rename that to documentHash
.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This also only gets used for parse failures. But it gets sent to Engine and used to display the (failing) query in the UI. So it needs to be text, not a hash.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But I'm going to call it originalDocumentText for now.
Re caching, which is a separate PR for now but the idea here, I think we're being strict enough about "this is a full request cache" and ignoring other semantically equivalent changes that "you have to be consistent about what the whole document looks like" seems like a fair place to start for v1 at least.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or maybe just documentText, since I think that captures the "original" concept to some degree.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, I like documentText
!
@@ -20,6 +21,15 @@ export interface ApolloServerPlugin { | |||
} | |||
|
|||
export interface GraphQLRequestListener<TContext = Record<string, any>> { | |||
didResolveDocument?( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I see why this hook is needed, because it seems we could just as well rely on didResolveOperation
? If we do need it, I think the name didResolveDocument
may be a bit confusing if we don't have document
(the parsed document) yet at this point. Especially because we have a document cache, it seems fine to invoke it after we're guaranteed to have a document
though. (I may be missing something, but I couldn't find where this is invoked from.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This gets the text in even if parsing fails.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, that makes sense. I like the name didResolveDocument
, but since we already use document
in the sense of parsed document that may be confusing. Not sure what the right solution is here. We could conceivably rename document
to documentAST
, although that would be breaking. It would also free up document
to mean the unparsed document though (what is currently called originalDocumentString
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe didResolveDocumentText
or didResolveDocumentString
? I kind of like text even though it's not a term used elsewhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, didResolveDocumentText
to be consistent with documentText
.
7e3c694
to
04283a4
Compare
OK, this takes most of the suggestions above into account (see separate fixup! commits) and also pulls in an executor implementation. |
1f54df8
to
d9570c7
Compare
I think this PR is in a state where it could hypothetically be merged. (Do a @martijnwalraven If I'm not on leave Monday, hopefully you will be able to give it another round of review on Monday? |
this.trace.persistedQueryRegister = true; | ||
} | ||
|
||
// Generally, we'll get queryString here and not parsedQuery; we only get |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment should probably be removed.
@@ -9,7 +9,7 @@ type UnwrapPromise<T> = T extends Promise<infer U> ? U : T; | |||
type DidEndHook<TArgs extends any[]> = (...args: TArgs) => void; | |||
|
|||
export class Dispatcher<T> { | |||
constructor(protected targets: T[]) {} | |||
constructor(public requestListeners: T[]) {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My idea was that we'd be able to keep Dispatcher
generic and possibly extract it into a utility package at some point. I realize it's already bound to the request listener API because it has special provisions for DidEndHook
, but renaming targets
to requestListeners
makes it even more unlikely we'll be able to reuse this.
) { | ||
// XXX Is it safe to use willSendReponse to do this finalization, vs | ||
// something that's more explicitly part of a try/final in requestPipeline | ||
// (eg, a new requestDidEnd callback?) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. I think it's safe right now, but adding an explicit requestDidEnd
hook is probably a better solution.
// We didn't get an AST, possibly because of a parse failure. Let's just | ||
// use the full query string. | ||
// | ||
// XXX This does mean that even if you use a calculateSignature which |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This definitely seems important to resolve before we release this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't a change from the existing code (even the comment) but a user did notice it recently. Thoughts?
}); | ||
executors.push(defaultExecutor); | ||
|
||
for (const executor of executors) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was hoping we could extract this into something like Dispatcher.invokeHookSequentially
, but I realize what's keeping that from working is the ability to pass in an executor
outside of a plugin (which we want for the gateway).
To be honest, using the idea of an executor
to return a response from cache still feels out of place to me. We always invoke executionDidStart
/executionDidEnd
for example, which I don't think we should do for caches. And we may want to rethink other hooks as well, based on whether an execution result or a preformatted response was returned. Right now for example, executors are defined as returning an ExecutionResult
, which contains an errors
property with GraphQLError
instances. Those will need to be fed through formatError
to be serializable. A response from cache on the other hand, will contain GraphQLFormattedError
s.
An alternative might be to separate the notion of an executor
from the ability to short-circuit execution through something like responseForOperation
(which we would use for caching). We could then define executor
as config.executor || defaultExecutor
, similar to how the fieldResolver
property is used in graphql-js
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, you've said this like 5 times now so perhaps I will listen. In that case I'll just back out the executor change entirely and let it be its own PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or really, that means just dropping this PR and rebasing the cache PR on master.
This update mostly migrates from the old GraphQLExtension API to the newer ApolloServerPlugin API. This extends the plugin interface with a few new methods to support apollo-engine-reporting. It does not yet add willResolveField, so for the short term it adds a hacky way for plugins to expose GraphQLExtensions just for the sake of willResolveField. The interface for actually using Engine reporting remains the same: passing configuration to the `engine` parameter of the ApolloServer constructor. This includes: - Changing GraphQLResponse to have the GraphQLFormattedError rather than GraphQLError (which among other things will be easier to serialize for a response cache). Add an explicit didEncounterErrors hook that gets the GraphQLError first. Now we don't have to worry if a plugin/extension gets willSendResponse called before or after FormatErrorExtension gets to it! FormatErrorExtension is removed. This part was based on an implementation by @martijnwalraven. - Add originalDocumentString, persistedQueryHit, and persistedQueryRegister to GraphQLRequestContext. - Fix Dispatcher so it doesn't lose `this` when invoking methods on the GraphQLRequestListener. - Add didResolveDocument to the plugin class, which is run after resolving APQs.
Add `apollo-server-plugin-base` dependency to `apollo-engine-reporting` TypeScript project
Code review feedback
Re-separate Dispatcher and GraphQLExtensionStack initialization. Expose requestListeners on Dispatcher instead. Will use the same field to get executors. Also add a missing TContext on GraphQLRequestListener
Original implementation by @martijnwalraven. This implementation should support both the response cache and the gateway.
d9570c7
to
2e5deff
Compare
No longer needed for the cache plugin. |
This update mostly migrates from the old GraphQLExtension API to the newer
ApolloServerPlugin API.
This extends the plugin interface with a few new methods to support
apollo-engine-reporting. It does not yet add willResolveField, so for the short
term it adds a hacky way for plugins to expose GraphQLExtensions just for the
sake of willResolveField.
The interface for actually using Engine reporting remains the same: passing
configuration to the
engine
parameter of the ApolloServer constructor.This includes:
GraphQLError (which among other things will be easier to serialize for a
response cache). Add an explicit didEncounterErrors hook that gets the
GraphQLError first. Now we don't have to worry if a plugin/extension gets
willSendResponse called before or after FormatErrorExtension gets to it!
FormatErrorExtension is removed. This part was based on an implementation by
@martijnwalraven.
GraphQLRequestContext.
this
when invoking methods on theGraphQLRequestListener.
APQs.
TODO: