RFC: Query/Mutation/Subscription Pre-Processor
See original GitHub issueTarget
JavaScript / TypeScript Apollo Client other platforms would ideally support as well
Description
I have the pleasure of developing a React Native mobile application that has both offline and online needs regarding its data dependencies. Apollo supports this usually with the @client
directive and as long as the type or field does not reside on the remote server this isn’t a problem.
The problem comes into play when there are needs for the data to exist both remotely and locally. To this end I have put together a proof of concept that allows me to solve this problem, but without changing every query that exists out there when the application is offline vs. online, the query, mutation or subscription needs to be modified before it is executed.
The proof of concept introduces a local @persistent
directive that is not actually registered in either the remote or local typeDefs
SDL. Instead queries would be modified one time for fields that have a local backing (in SQLite for us). This new @persistent
directive gets swapped for a @client
directive when the application is offline and removed entirely when the application is online.
This swapping of directives occurs in the proposed lifecycle of an operation. That proposal is to have the ability to register a pre-op processor. Currently, this is achieved in the proof of concept by wrapping the .query
, .mutate
and .subscribe
methods of the Apollo Client with higher order functions that invoke the originals after processing the SDL first.
Here is an example of the .mutate
pre-processor. It is almost identical for .query
and .subscribe
client[Symbol.for('mutate')] = client.mutate
client.mutate = function (options) {
// create a copy of the supplied SDL/AST to prevent mutation
let newOpSDL = gql`${print(options.mutation)}`
newOpSDL = swapDirective(
newOpSDL,
'persistent',
() => online() ? undefined : 'client'
) || options.mutation
return client[Symbol.for('mutate')]({ ...options, mutation: newOpSDL })
}
The proof of concept swapDirective()
function is here. The optional chaining technique would make this code cleaner but this is a proof of concept and it wasn’t written that way.
/****************************************************************************
* Create a function that will swap out a directive with a new value in
* a given GQL / SDL document object.
*
* @param {GQL.ASTNode} document the document to search for a directive in
* @param {String|Function} searchFor a String or function that returns a
* string, that indicates the directive to search for
* @param {String|Function} replaceWith a String or function that returns a
* string, that indicates the directive value to swap in. Note that if this
* value is not-truthy the directive will be removed.
* @return an altered document if changes were made or null otherwise
****************************************************************************/
function swapDirective(document, searchFor, replaceWith) {
let alteredDocument = document
const isFn = (o) => /Function/.test(Object.prototype.toString.call(o))
const target = isFn(searchFor) ? searchFor() : String(searchFor)
const newValue = isFn(replaceWith) ? replaceWith() : String(replaceWith)
const getter = (n) => n && n.name && n.name.value || null
const setter = (n, v) => { n && n.name && (n.name.value = v); return n }
const hasTarget = (n) => new RegExp(target, 'i').test(getter(n))
alteredDocument = visit(document, {
Field: {
enter(node, key, parent, path, ancestors) {
if (node.directives && node.directives.some(hasTarget)) {
if (!!!newValue) {
for (let i = 0; i < node.directives.length; i++) {
if (new RegExp(target, 'i').test(getter(node.directives[i]))) {
node.directives.splice(i, 1)
i--
}
}
}
else {
// Thought process here is to swap @persistent for @client
// when we are offline.
node.directives = node.directives
.filter(directive => new RegExp(target, 'i').test(getter(directive)))
.map(directive => {
return setter(directive, newValue)
})
}
return node
}
},
}
})
return alteredDocument || document
}
Proposal
The idea here would be to have the Apollo Client allow both the registration and de-registration of an AST/SDL preprocessor. It would, ideally, also allow reorganization of the order in which the preprocessor could be arranged. Registered function would process the SDL/ASTs provided before they are used normally by the Apollo GraphQL Client code.
It may work and function as seen here
let client = new ApolloClient(...);
// Grab some constant values from the client class (should maybe just also have a PROCESS_ALL constant)
let { PROCESS_QUERY, PROCESS_MUTATION, PROCESS_SUBSCRIPTION } = ApolloClient;
// Choose which items our preprocessor applies to
let processedOperations = PROCESS_QUERY | PROCESS_MUTATION | PROCESS_SUBSCRIPTION;
// Create a function to do the pre-processing
function processor(sdl, ast) {
// create a copy of the supplied SDL/AST to prevent mutation
let newOpSDL = gql`${print(ast)}`
newOpSDL = swapDirective(
newOpSDL,
'persistent',
() => online() ? undefined : 'client'
) || ast
return client[Symbol.for('mutate')]({ ...options, mutation: newOpSDL })
}
client.registerPreprocessor(processedOperations, processor);
// Perform queries, mutations or subscriptions normally
await client.query(...);
await client.mutate(...);
await client.subscribe(...);
// Optionally remove the preprocessor for all calls to `.mutate`
client.deregisterPreprocessor(PROCESS_MUTATION, processor);
// etc...
Issue Analytics
- State:
- Created a year ago
- Reactions:1
- Comments:14 (7 by maintainers)
Top GitHub Comments
Hey @nyteshade! I just opened https://github.com/apollographql/apollo-client/pull/10346 which adds the ability to get
@client
fields in the link chain. Once that PR is merged, we will release it in an upcoming3.8.0-alpha.x
version.Feel free to take a look at the PR and add any comments/questions/feedback. I hope this will work well for your use case!
Hey @nyteshade! As per our discussion earlier, we’d like to introduce a new boolean option that enables
@client
directives to make it to the link chain so that you can do the processing you need on the query. I’ve opened a new issue (https://github.com/apollographql/apollo-client/issues/10303) to track this as we see it as part of the work to move local resolvers back to the link chain (#10060).As such, I’m going to close this RFC. We sincerely appreciate your thoughts and discussion as its really helped us figure out how to move forward with this. @jpvajda will follow up with you to set expectations on timing and how we plan to release this change. If you have followup questions, feel free to post them in #10303!