question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

RFC: Query/Mutation/Subscription Pre-Processor

See original GitHub issue

Target

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:closed
  • Created a year ago
  • Reactions:1
  • Comments:14 (7 by maintainers)

github_iconTop GitHub Comments

1reaction
jerelmillercommented, Dec 10, 2022

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 upcoming 3.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!

1reaction
jerelmillercommented, Nov 21, 2022

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!

Read more comments on GitHub >

github_iconTop Results From Across the Web

RFC: Query/Mutation/Subscription Pre-Processor · Issue #10228
The proof of concept introduces a local @persistent directive that is not actually registered in either the remote or local typeDefs SDL.
Read more >
GraphQL Queries, Mutations and Subscriptions - Medium
In my first article, I talked about GraphQL types and relationships. This time, I'll be focusing on queries, mutations and subscriptions.
Read more >
Subscriptions - Apollo GraphQL Docs
Defining a subscription. You define a subscription on both the server side and the client side, just like you do for queries and...
Read more >
Component Reference - Apache JMeter - User's Manual
GraphQL query (or mutation) variables in a valid JSON string. Note: If the input string is not a valid JSON string, this will...
Read more >
Understanding Subscriptions — absinthe v1.7.0 - HexDocs
Like queries and mutations, subscriptions are not intrinsically tied to any particular transport, and they're built within Absinthe itself to be able to ......
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found