How to simplify HTTP post of JSON to GraphQL mutat

2019-08-28 02:12发布

问题:

I would like to HTTP POST values directly as JSON to an addBook resolver already declared in my GraphQL Mutation.

However, the examples I've seen (and proven) use serialisation of parameters from JSON to SDL or re-declaration of variables in SDL to bind from a Query Variable.

Neither approach makes sense because the addBook mutation already has all parameters and validation declared. Using these approaches would lead to unnecessary query serialisation logic having to be created, debugged and maintained.

I have well-formed (schema- edited and -validated) JSON being constructed in the browser which conforms to the data of a declared GraphQLObjectType.

Can anyone explain how to avoid this unnecessary reserialisation or duplication when posting against a mutation resolver?

I've been experimenting with multiple ways of mapping a JSON data structure against the addBook mutation but can't find an example of simply sending the JSON so that property names are be bound against addBook parameter names without apparently pointless reserialisation or boilerplate.

The source code at https://github.com/cefn/graphql-gist/tree/master/mutation-map is a minimal reproducible example which demonstrates the problem. It has an addBook resolver which already has parameter names, types and nullability defined. I can't find a way to use JSON to simply POST parameters against addBook.

I'm using GraphiQL as a reference implementation to HTTP POST values.

I could write code to serialise JSON to SDL. It would end up looking like this which works through GraphiQL:

mutation {addBook(id:"4", name:"Education Course Guide", genre: "Education"){
    id
}}

Alternatively I can write code to explicitly alias each parameter of addBook to a different query which then allows me to post values as a JSON query variable, also proven through GraphiQL:

mutation doAdd($id: String, $name: String!, $genre: String){
  addBook(id:$id, name:$name, genre:$genre){
    id
  }
}

...with the query variable...

{
  name: "Jonathan Livingstone Seagull",
  id: "6"
}

However, I am sure there's some way to directly post this JSON against addBook, telling it to take parameters from a Query Variable. I'm imagining something like...

mutation {addBook($*){
    id
}}

I would like a mutation call against addBook to succeed, taking named values from a JSON Query Variable, but without reserialisation or redeclaration of the properties to parameter names.

回答1:

This boils down to schema design. Instead of having three arguments on your field

type Mutation {
  addBook(id: ID, name: String!, genre: String!): Book
}

you can have a single argument that takes an input object type

type Mutation {
  addBook(input: AddBookInput!): Book
}

input AddBookInput {
  id: ID
  name: String!
  genre: String!
}

Then your query only has to provide a single variable:

mutation AddBook($input: AddBookInput!) {
  addBook(input: $input) {
    id
  }
}

and your variables look something like:

{
  "input": {
    "name": "Jonathan Livingstone Seagull",
    "genre": "Fable"
  }
}

Variables have to be explicitly defining as part of the operation definition because GraphQL and JSON are not interchangeable. A JSON string value could be a String, an ID or some custom scalar (like DateTime) in GraphQL. The variable definitions tell GraphQL how to correctly serialize and validate the provided JSON values. Because variables can be used multiple times throughout a document, their types likewise cannot simply be inferred from the types of the arguments they are used with.

EDIT:

Variables are only declared once per document. Once declared, they may be referred to any number of times throughout the document. Imagine a query like

mutation MyMutation ($id: ID!) {
  flagSomething(somethingId: $id)
  addPropertyToSomething(id: $id, property: "WOW")
}

We declare the variable once and tell GraphQL it's an ID scalar and it's non-nullable (i.e. required). We then use the variable twice -- once as the value of somethingId on flagSomething and again as the value of id on addPropertyToSomething. The same variable could also be used as the value to a directive's argument too -- it's not limited to just field arguments. Notice also that nothing says the variable name has to match the field name -- this is typically only done out of convenience.

The other notable thing here is that there's two validation steps happening here.

First, GraphQL will check if the provided variable (i.e. the JSON value) can be serialized into the type specified. Since we declared the variable as non-null (using !), GraphQL will also verify the variable actually exists and is not equal to null.

GraphQL will also verify that the type you specified for the variable matches the types of the arguments where it's actually used. So an Int variable will throw if it's passed to a String argument and so on. Moreover, nullability is checked here too. So an argument that is an Int! (non-null integer) will only accept variables that are also Int!. However, an argument that is Int (i.e. nullable) will accept either Int or Int! variables.

The syntax that exists is there for a reason. The kind of syntax you're imagining would only make sense in a specific scenario where you're only querying a single root field and using all the variables as arguments to that one field and the variable names match the argument names and you don't need to dynamically set any directive arguments.