Skip to content

API Improvement. #44

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
chris-ramon opened this issue Nov 3, 2015 · 15 comments
Closed

API Improvement. #44

chris-ramon opened this issue Nov 3, 2015 · 15 comments

Comments

@chris-ramon
Copy link
Member

Before releasing the first version of the lib, I think we could improve the API, meaning remove verbosity and clean-it-up to be go-idiomatic, so please do share ur thoughts on this! 👍

This is the basic example from the Getting Started section on README (still to merge #43):

package main

import (
    "encoding/json"
    "fmt"
    "log"

    "github.com/chris-ramon/graphql"
)

func main() {
    // Schema
    fields := graphql.FieldConfigMap{
        "hello": &graphql.FieldConfig{
            Type: graphql.String,
            Resolve: func(p graphql.GQLFRParams) interface{} {
                return "world"
            },
        },
    }
    rootQuery := graphql.ObjectConfig{Name: "RootQuery", Fields: fields}
    schemaConfig := graphql.SchemaConfig{Query: graphql.NewObject(rootQuery)}
    schema, err := graphql.NewSchema(schemaConfig)
    if err != nil {
        log.Fatalf("failed to create new schema, error: %v", err)
    }

    // Query
    query := `{hello}`
    params := graphql.Params{Schema: schema, RequestString: query}
    result := make(chan *graphql.Result)
    go graphql.Graphql(params, result)
    r := <-result
    if len(r.Errors) > 0 {
        log.Fatalf("failed to execute graphql operation, errors: %+v", r.Errors)
    }
}

My initial proposal is:

package main

import (
    "encoding/json"
    "fmt"
    "log"

    "github.com/chris-ramon/graphql"
)

func main() {
    // Schema
    helloResolve := func(p graphql.ResolveParams) interface{} {
        return "world"
    }
    fields := []graphql.Fields{
        graphql.Field{Name: "hello", Type: graphql.String, Resolve: helloResolve},
    }
    schema, err := graphql.NewSchema(Name: "RootQuery", Fields: fields)
    if err != nil {
        log.Fatalf("failed to create new schema, error: %v", err)
    }

    // Query
    query := `{hello}`
    result := make(chan *graphql.Result)
    go graphql.Do(schema, query, result) // Like: http.Client.Do
    r := <-result
    if len(r.Errors) > 0 {
        log.Fatalf("failed to execute graphql operation, errors: %+v", r.Errors)
    }
}
@sogko
Copy link
Member

sogko commented Nov 3, 2015

Woah the improvements starting to look good 👍🏻

My humble notes:

1. Resolve function
  • graphql.ResolveParams is a great improvement over graphql.GQLFRParams👍🏻
  • There is a proposal in issue How do you throw errors? #28 to allow user functions to return error instead of forcing them to panic.
    If any, executor will then return nil and add the user-returned error into the graphql.Result
    helloResolve := func(p graphql.ResolveParams) (interface{}, error) {
        return "world", nil
    }
2. graphql.Fields, graphql.Field

This is definitely a much need improvement (i.e. renaming FieldConfigMap and FieldConfig.

In the proposed example, it is a []graphql.Fields{} slice.

How about we retain it as map? Because the field key and the field name can be different.
(The name config is used in the introspection query and for documentation)

    package graphql
    type Field struct
    type Fields map[string]*Field
    ...
    package example
    fields := graphql.Fields{
        "hello": &graphql.Field{Name: "Hello Field", Type: graphql.String, Resolve: helloResolve},
    }
3. graphql.NewSchema() and root query

At first glance, the new NewSchema() method looks cleaner. But it might be veering too much from graphql. The proposed example looks like it can only define a root object for query operation (ie. RootQuery). We still need a way to define mutation root query.

A GraphQL schema is represented by a root type for each kind of operation: query and mutation; this determines the place in the type system where those operations begin.
Source: http://facebook.github.io/graphql/#sec-Type-System

func main() {
    helloResolve := func(p graphql.ResolveParams) interface{} {
        return "world"
    }
    queryFields := graphql.Fields{
        "hello": graphql.Field{Name: "Hello Field", Type: graphql.String, Resolve: helloResolve},
    }
    rootQuery := graphql.NewObject(graphql.ObjectConfig{
        Name: "RootQuery",
        Fields: queryFields,
    })
    schema, err := graphql.NewSchema(graphql.SchemaConfig{Query: rootQuery})
    // or with mutation, 
    // schema, err := graphql.NewSchema(graphql.SchemaConfig{Query: rootQuery, Mutation: mutationQuery})
    if err != nil {
        log.Fatalf("failed to create new schema, error: %v", err)
    }
    ...
}
4. Constructor configs

I wish we can get rid of the config structs for the constructor. There is some hope; there is a proposal for named arguments to be added to the language golang/go#12296 and golang/go#12854, but realistically, it would take awhile.

5. graphql.Do()

I'm in favour of this over the current go graphql.Graphql(...) 👍🏻
Your example shows go graphql.Do(schema, query, result); are you suggesting we replace the graphql.Params?


Cheers!

@FugiTech
Copy link
Contributor

FugiTech commented Nov 3, 2015

I think that

result := make(chan *graphql.Result)
go graphql.Do(schema, query, result)
r := <-result

is a very complicated interface. As far as I can tell, there's no need for the result channel at all (everything in the executor appears to be synchronous), but if it is required then it'd be nice for graphql.Do to handle creating the channel and waiting for the result. In other words,

r := graphql.Do(schema, query)

would be my ideal interface.

@ghost
Copy link

ghost commented Nov 3, 2015

I also prefer the Do syntax.
Even the Fields slice seems easier for typing those long types.

@sogko
Copy link
Member

sogko commented Nov 5, 2015

Update: @Fugiman submitted PR #48 to improve the graphql.Graphql(...)
Updateupdate: Merged 👍🏻

@EmergentBehavior
Copy link
Contributor

Definitely looking forward to more robust error handling and less panic() at the disco :)

@sogko sogko mentioned this issue Nov 18, 2015
@bsr203
Copy link

bsr203 commented Nov 18, 2015

Hi @sogko I would like to get support on custom error reporting as #74

I can think of two approaches

  1. change Resolve function signature to indicate errror as well
 helloResolve := func(p graphql.ResolveParams) (interface{}, error) {
        return "world", nil
    }

This is explained by @sogko above
2. Check returned value is an error interface in executor
http://play.golang.org/p/xoPSMAbEAB

package main

import "fmt"

func Resolve(isErr bool) interface{} {
    if isErr {
        return fmt.Errorf("got an error")
    }
    return 2 + 1
}

func assert(res interface{}) {
    if err, ok := res.(error); ok  {
        fmt.Println(err.Error())

    } else {
        fmt.Printf("got value %d", res)
    }
}

func main() {
    assert(Resolve(true))
    assert(Resolve(false))
}

//returns
got an error
got value 3

tradeoff
Option 1 has the benefit of being explicit and idiomatic in go sense. At the same time need API change.
Option 2 has the benefit of no API change but retuned result has some implicit meaning.

I prefer option 1 and being explicit. I can work on this if you all agree.

Cheers,
bsr

@bsr203
Copy link

bsr203 commented Nov 25, 2015

@chris-ramon @sogko any comment on error handling. currently the framework is catching panic and suppressing the cause of error. I keep hitting this as Iost context information make it hard to track down an error. Please give some priority on this.

@ghost
Copy link

ghost commented Nov 25, 2015

Right now what I do is comment out the defers and recovers in executor.go that really helps and crashes your server a lot. But then that is only for development. We are actually planning to make a graphql implementation using code generation for performance in the future.

@pkieltyka
Copy link

@pyros2097 with code generation? tell us more :)

@ghost
Copy link

ghost commented Nov 25, 2015

By we I mean my company. Well we have 3 Type systems one was our postgres types, then go types and then graphql types. So to map go types to postgres types and tables we had done it using code generation using go generate. But didn't have time to do it for graphql types also. So say you have a struct

type User struct {
  ID string `json:"id"`
  name string `json:"id"`
}

we had to manually create a graphql type for each struct and that was a bit painful as we already know what types ID and name were and even needed to create custom scalar types for validation like PLPassword(which I think should be done in Go and on the struct). So you techincally wouldn't need to create all graphql types, maybe you might only need to resolve the fields.

And another thing we found out was that a lot of reflection was used for serialization and de-serialization and the current graphql implementation we need to resolve interfaces or map[string]interfaces{} which can be improved if you know the type you need to json encode.
We are planning to have some thing that can do something like a rails orm does for you but with also graphql types ex: sql -> structs -> graphql objects.
And by the way we have recently finished 2 projects and are currently testing it out.
Awesome work @sogko and @chris-ramon thanks to you guys we didn't need write an rpc layer between go and nodejs 👍

@atrniv Any word on the implementation you were talking about? (PS He is the code generation guy you can look at all his projects they all have code generation in them even the js ones )

@bsr203
Copy link

bsr203 commented Nov 25, 2015

@pyros2097 thanks for the tip. that helps :-)

@chris-ramon
Copy link
Member Author

Thanks for sharing your thoughts and concerns about api/lib design/improvement guys! 🌟

I've started working on extending graphql.FieldResolveFn to return error as second parameter: (interface{}, error), as @bsr203 mentioned above, work under progress on: #80

@bsr203
Copy link

bsr203 commented Nov 25, 2015

@chris-ramon you rock :-) I can see the effort you putting in this. tx.
Can you also look at #65 with this as you may be touching the same functions :-) Even if we don't use context, just being consistent is enough now map[string]interface{} -> interface{}

chris-ramon added a commit that referenced this issue Nov 26, 2015
adds query and mutation tests to check error from  is being handled
chris-ramon added a commit that referenced this issue Nov 26, 2015
chris-ramon added a commit that referenced this issue Nov 26, 2015
adds query and mutation test to check correct returning error from
chris-ramon added a commit that referenced this issue Nov 26, 2015
adds query and mutation test to check correct returning error from graphql.FieldResolveFn
chris-ramon added a commit that referenced this issue Nov 27, 2015
executor does handle potential errors for executing graphql.FieldResolveFn
@ghost
Copy link

ghost commented Apr 30, 2016

@pkieltyka We have implemented it and using it on our live servers you can check the tests out although there is no documentation whatsoever ... https://github.com/playlyfe/go-graphql

@chris-ramon
Copy link
Member Author

Let's close this one; and have a new conversation on API improvements on new issues, thanks a lot for bringing great ideas guys 👍 🌟

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants