graphql-go / graphql

An implementation of GraphQL for Go / Golang
MIT License
9.9k stars 841 forks source link

NewInputObject vs NewObject - Using NewObject type as arg for mutation? #234

Open davejohnston opened 7 years ago

davejohnston commented 7 years ago

I'm writing a schema, and have found I needed to create both NewObject and NewInputObjects. If I want to use a type as an arg to a mutation/query then it must be an NewInputObject. But if I want to return the type in response to a query, then it needs to be a NewObject type.

While the solution works, it seems like alot of duplication. Am I missing something here? See code below.

Here I have a model for my data:

type Author struct {
    Name string `json:"name"`
    Age int  `json:"age"`
    Books []Book `json:"books"`
}

type Book struct {
    Title   string  `json:"title"`
    Editions []string `json:"editions"`
}

I have described these types as graphql objects

    bookType := graphql.NewObject(graphql.ObjectConfig{
        Name: "Book",
        Fields: graphql.Fields{
            "title": &graphql.Field{Type: graphql.String},
            "editions": &graphql.Field{Type: graphql.NewList(graphql.String)},
        },
    })

    authorType := graphql.NewObject(graphql.ObjectConfig{
        Name: "Author",
        Fields: graphql.Fields{
            "name": &graphql.Field{Type: graphql.String},
            "age": &graphql.Field{Type: graphql.Int},
            "books": &graphql.Field{Type: graphql.NewList(bookType)},
        },
    })

And now I have a mutation to create authors: N.B I have 3 args name : string age : int books : list of bookType

var rootMutation = graphql.NewObject(graphql.ObjectConfig{
        Name: "RootMutation",
        Fields: graphql.Fields{
            "createAuthor": &graphql.Field{
                Type:        authorType, // the return type for this field
                Description: "Execute a new command - this creates a new command record",
                Args: graphql.FieldConfigArgument{
                    "name": &graphql.ArgumentConfig{
                        Type: graphql.String,
                    },
                    "age": &graphql.ArgumentConfig{
                        Type: graphql.Int,
                    },
                    "books": &graphql.ArgumentConfig{
                        Type: graphql.NewList(bookType),
                    },
                },
                Resolve: func(params graphql.ResolveParams) (interface{}, error) {
                    log.Printf("Args: %v", params.Args)
                    jsonString, err := json.Marshal(params.Args)
                    if err != nil {
                        fmt.Println("Error encoding JSON")
                        return nil, nil
                    }

                    author := Author{}
                    json.Unmarshal([]byte(jsonString), &author)

                    return author, nil
                },
            },
        },
    })

If I make a request with the following curl I get an error: N.B books is passed in as a variable using json.

curl -XPOST http://localhost:8080/graphql -H 'Content-Type: application/json' -d \
'{
  "query": "mutation CreateAuthor($name:String,$age:Int,$books:[Book]){createAuthor(name:$name,age:$age,books:$books){name age books {title}}}",
  "variables": "{\"name\": \"Dave J\", \"age\": \"99\", \"books\": [ { \"title\" : \"My First Book\", \"editions\" : [ \"one\", \"two\"] }, {\"title\" : \"My Second Book\", \"editions\" : [ \"one\", \"two\"]}] }"
}'
message": "Variable \"$books\" cannot be non-input type \"[Book]\"."

So after some googling I changed the NewObject to be NewInputObject

    bookType := graphql.NewInputObject(graphql.InputObjectConfig{
        Name: "BookType",
        Fields: graphql.InputObjectConfigFieldMap{
            "title": &graphql.InputObjectFieldConfig{Type: graphql.String},
            "editions": &graphql.InputObjectFieldConfig{Type: graphql.NewList(graphql.String)},
        },
    })

The mutation now succeeds and returns this data: N.B the author details have been returned, but the nestled type books is null

{
  "data": {
    "createAuthor": {
      "age": 99,
      "books": [
        null,
        null
      ],
      "name": "Doyle"
    }
  },

So I now create two types. A bookType (NewObject) and a bookInputType (NewInputObject). In my mutation the arg is changed bookInputType and in the curl request the type is also changed to BookInput.

    bookInputType := graphql.NewInputObject(graphql.InputObjectConfig{
        Name: "BookInput",
        Fields: graphql.InputObjectConfigFieldMap{
            "title": &graphql.InputObjectFieldConfig{Type: graphql.String},
            "editions": &graphql.InputObjectFieldConfig{Type: graphql.NewList(graphql.String)},
        },
    })

    bookType := graphql.NewObject(graphql.ObjectConfig{
        Name: "Book",
        Fields: graphql.Fields{
            "title": &graphql.Field{Type: graphql.String},
            "editions": &graphql.Field{Type: graphql.NewList(graphql.String)},
        },
    })

Now when I make a request, everything works. Is this expected, or is there a better way to implement this?

williangd commented 6 years ago

I have the same "problem". Is there any way to avoid this duplication of code?

niondir commented 6 years ago

Same here. A quick fix could be some converter function that derives an graphql.InputObjectConfigFieldMap from a given graphql.Fields. Should not be too hard, but will not support fieldwise definition of default values and NonNull fields.

Otherwise I see no good solution yet. Maybe in future some Stuct tags might help here.

atombender commented 5 years ago

This is unfortunately how the GraphQL spec:

The fields on an input object type can themselves refer to input object types, but you can't mix input and output types in your schema. Input object types also can't have arguments on their fields.

You might think that one could modify graphql-go to just let you use objects everywhere, and simply respond to introspection queries with input schemas for all your objects. But GraphQL only has a single namespace. So you can't have:

type Foo { ... }
input Foo { ... }

I think this was a mistake on part of the GraphQL designers. Obviously this leads to duplication. For example, if you have a type LatLng that you want to describe query results with, if you want to take a lat/lng as an input, you'll have to define an identical type and give it some other name like LatLngInput.

As far as I know, there are only two reasons inputs need to be a distinct kind of type in GraphQL: One is that input fields can have default values (but there's no reason normal fields couldn't), the other is that input fields can't have arguments (but a solution would be to simply disallow arguments in input values).

karterlizard commented 4 years ago

hello , can you share your project, I'm still confused about Graphql.