Search code examples
graphqlgithub-api

GraphQL: Query and mutate in one network call


I'm going to give a specific example with the GitHub GraphQL API (which is the particular problem I want to solve), but the question seems generalizable to GraphQL in general.

Suppose I want to add a label 'high priority' to an issue #1234 on repository pytorch/pytorch. According to the API documentation, I should use the mutation https://developer.github.com/v4/mutation/addlabelstolabelable/ The input demands:

labelIds ([ID!]!)
The ids of the labels to add.

labelableId (ID!)
The id of the labelable object to add labels to.

OK, how do I get the IDs? In my code, I know I want the label 'high priority', but to actually communicate this to this mutation, I have to first resolve 'high priority' into an ID but doing another GraphQL call first. The same goes for what I want to label: I have a unique identifier in the form of pytorch/pytorch#1234, but I don't have the ID, I've got to look it up.)

So at the end of the day, I have to do three API calls to just label something, when I could have done it in REST instead with one call. In general, I see lots of GraphQL mutation APIs that talk only in IDs (even when there is some other canonical identifier available for the system), and I end up having to do extra round trips. Am I doing it wrong? Or is this really how GraphQL was designed?


Solution

  • According to the spec, a GraphQL document may contain any number of operations, where an operation is one of query, mutation or subscription.

    • query – a read‐only fetch.
    • mutation – a write followed by a fetch.
    • subscription – a long‐lived request that fetches data in response to source events.

    Only one operation may be executed for a particular request (if more than one is provided, an operationName must be provided to specify which one is to be executed).

    However, within that operation, any number of fields may be requested. So if you need two or more root-level query fields (colloquially referred to as "queries"), they may be lumped together inside a single operation:

    query ArbitraryOperationName {
      getSomething
      getSomethingElse
    }
    

    Ditto for mutations -- you can execute two or more mutations (in sequence):

    mutation ArbitraryOperationName {
      doSomething
      doSomethingElse
      doAThirdAction
    }
    

    Therefore, the only scenarios where you would need to split your request into multiple requests are:

    • You need to perform both a query and mutation -- these are separate operations so they must be sent separately.
    • You need to provide part of the result of one query as input to another.

    You should be able to get both the label ID and the issue ID inside a single query:

    query {
      repository(owner: "graphql", name: "graphql-js") {
        label(name: "help wanted") {
          id
        }
        issue(number: 100) {
          id
        }
      }
    }
    

    However, your mutation would have to be a separate request because 1) it's a different operation and 2) it requires as input data returned by the above query.

    That said, the fact that addlabelstolabelable requires you to pass in IDs instead of names for labels and IDs instead of numbers for issues/PRs is a design choice on the part of Github. While it's fairly common to see entities referenced explicitly by some kind of id field in mutation arguments, there's nothing in the spec that prohibits a mutation from accepting other identifiers as input.