Graphql-go: Documentation

Created on 31 Dec 2016  路  11Comments  路  Source: graph-gophers/graphql-go

Is there any documentation or guide for resolvers? The starwars example seems pretty convoluted.

Most helpful comment

I'm figuring it out as I go but I'll contribute 1-2 examples to this repo in the next 2 monthseventually, with persistence and authentication+authorization. (I have those things working right now, but want some time to check if my implementations have gaps)

For now, this is what I've been doing:

schema.graphql

type Schema {
    query: Query
    mutation: Mutation
}
type Query {
    foo(id: ID!): Foo
}
type Mutation {
    createFoo(bar: String!, baz: String): Foo
}
type Foo {
    id: ID!
    bar: String!
    baz: String
}

main.go

func main() {
    db, _ db.Open("/DSN")
    rawSchema, _ := ioutil.ReadFile("/path/to/schema.graphql")
    parsedSchema, _ := graphql.ParseSchema(string(rawSchema), NewResolver(db))
    if os.Getenv("production") != "true" {
        http.HandleFunc("/", graphiqlHandler)
    }
    http.Handle("/graphql", Middleware(injectViewerToContext(&relay.Handler{Schema: parsedSchema})))
    http.ListenAndServe(":8080", nil)
}

func injectViewerToContext(next http.Handler) http.Handler {
    return func(w http.ResponseWriter, r *http.Request) {
        tokenString := r.Header.Get("Authorization")
        viewerID, authenticated := authenticate(tokenString) // custom 401 function
        if !authenticated {
            w.Write(http.StatusUnauthorized)
            return
        }

        next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), "Authorization", viewerID))) // could pass a custom viewer type with roles, etc
    }
}

resolver.go

type Resolver { db: *sql.DB }
func NewResolver(db *sql.DB) *Resolver {
    return &Resolver{db: db}
}

func (r *Resolver) Foo(ctx context.Context, args *struct{ ID string }) (*FooResolver, error) {
    viewerID, _ := context.Value("auth", ctx).(uuid.UUID)
    return NewFooResolver(viewerID, args.ID, r.db)
}

func (r *Resolver) CreateFoo(ctx context.Context, args *struct{
    Bar string
    Baz *string
}) (*FooResolver, error) {
    viewerID, _ := context.Value("auth", ctx).(uuid.UUID)
    id, _ := NewFoo(viewerID, args.Bar, args.Baz, r.db)
    return NewFooResolver(viewerID, id, r.db)
}

foo.go

func NewFoo(viewerID uuid.UUID, bar string, bazPtr *string, db *sql.DB) (*FooResolver, error) {
    // do some 403
    if !mayCreateFoo(viewerID) {
        return nil, errors.New("403")
    }

    id := uuid.NewV1()
    tx, _ := db.Begin()
    _, _ = tx.Exec("INSERT INTO foo_bar (id, bar) VALUES (?, ?)", id.Bytes(), args.Bar)
    if bazPtr != nil {
        _, _ = tx.Exec("INSERT INTO foo_baz (id, baz) VALUES (?, ?)", id.Bytes(), *args.Baz)
    }
    _ = tx.Commit()
    return NewFooResolver(viewerID, id, db), nil
}

type FooResolver struct {
    viewerID    uuid.UUID
    id      uuid.UUID
    db      *sql.DB
}

func NewFooResolver(viewerID, id uuid.UUID, db *sql.DB) (*FooResolver, error) {
    return &FooResolver{
        viewerID: viewerID,
        id: id,
        db: db,
    }, nil
}

func (r *FooResolver) ID() graphql.ID {
    return graphql.ID(r.id.String())
}

func (r *FooResolver) Bar() (string, error) {
    // do some 403
    if !mayViewBar(r.viewerID, id) {
        return "", errors.New("403")
    }

    var bar string
    _ := r.db.QueryRow("SELECT bar FROM foo_bar WHERE id=?", r.id.Bytes()).Scan(&bar)
    return bar, nil
}

func (r *FooResolver) Baz) (*string, error) {
    // do some 403
    if !mayViewBaz(r.viewerID, id) {
        return "", errors.New("403")
    }

    var baz string
    _ := r.db.QueryRow("SELECT baz FROM foo_baz WHERE id=?", r.id.Bytes()).Scan(&baz)
    return &baz, nil
}

Notes:

  • It looks like type-asserting the viewer from context is the "best" available way right now
  • If your type in your graphql schema is nullable, then your resolver must return a pointer, with a nil pointer resolving to null
  • The above is a fairly naive implementation. Still want to add caching, for one. I've exhausted my database's connections quite quickly nesting 5 layers in. I'm really interested in some type of middleware layer that adds onto dataloader by also parsing the SQL queries and combining columns. Hopefully, some of that goes into Vitess' query optimizer too
  • One practice I think is good that I go partway towards is to handle your 401 before enterring graphql and putting your 403 logic in your custom type's constructor (not the resolver, but your domain types). Make your struct's fields unexported and have only one exported constructor. Now, on your exported methods on your custom type, handle 403 within that so that anytime you have data, you know that it's already been 403'd. That would mean moving the SELECT queries out of Bar() and Baz(), and moving them into the methods on that custom struct.

All 11 comments

Can you be a little more specific on what is confusing to you about the example given? I could take a shot at writing an example based off of an approach I've taken, but it would help to know which points are most unclear.

I just need to see how mutations work. How to expose the mutation over the graphql api and then how to make a function, like with anonymous functions/lambda expressions, or calling a method.

I'm figuring it out as I go but I'll contribute 1-2 examples to this repo in the next 2 monthseventually, with persistence and authentication+authorization. (I have those things working right now, but want some time to check if my implementations have gaps)

For now, this is what I've been doing:

schema.graphql

type Schema {
    query: Query
    mutation: Mutation
}
type Query {
    foo(id: ID!): Foo
}
type Mutation {
    createFoo(bar: String!, baz: String): Foo
}
type Foo {
    id: ID!
    bar: String!
    baz: String
}

main.go

func main() {
    db, _ db.Open("/DSN")
    rawSchema, _ := ioutil.ReadFile("/path/to/schema.graphql")
    parsedSchema, _ := graphql.ParseSchema(string(rawSchema), NewResolver(db))
    if os.Getenv("production") != "true" {
        http.HandleFunc("/", graphiqlHandler)
    }
    http.Handle("/graphql", Middleware(injectViewerToContext(&relay.Handler{Schema: parsedSchema})))
    http.ListenAndServe(":8080", nil)
}

func injectViewerToContext(next http.Handler) http.Handler {
    return func(w http.ResponseWriter, r *http.Request) {
        tokenString := r.Header.Get("Authorization")
        viewerID, authenticated := authenticate(tokenString) // custom 401 function
        if !authenticated {
            w.Write(http.StatusUnauthorized)
            return
        }

        next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), "Authorization", viewerID))) // could pass a custom viewer type with roles, etc
    }
}

resolver.go

type Resolver { db: *sql.DB }
func NewResolver(db *sql.DB) *Resolver {
    return &Resolver{db: db}
}

func (r *Resolver) Foo(ctx context.Context, args *struct{ ID string }) (*FooResolver, error) {
    viewerID, _ := context.Value("auth", ctx).(uuid.UUID)
    return NewFooResolver(viewerID, args.ID, r.db)
}

func (r *Resolver) CreateFoo(ctx context.Context, args *struct{
    Bar string
    Baz *string
}) (*FooResolver, error) {
    viewerID, _ := context.Value("auth", ctx).(uuid.UUID)
    id, _ := NewFoo(viewerID, args.Bar, args.Baz, r.db)
    return NewFooResolver(viewerID, id, r.db)
}

foo.go

func NewFoo(viewerID uuid.UUID, bar string, bazPtr *string, db *sql.DB) (*FooResolver, error) {
    // do some 403
    if !mayCreateFoo(viewerID) {
        return nil, errors.New("403")
    }

    id := uuid.NewV1()
    tx, _ := db.Begin()
    _, _ = tx.Exec("INSERT INTO foo_bar (id, bar) VALUES (?, ?)", id.Bytes(), args.Bar)
    if bazPtr != nil {
        _, _ = tx.Exec("INSERT INTO foo_baz (id, baz) VALUES (?, ?)", id.Bytes(), *args.Baz)
    }
    _ = tx.Commit()
    return NewFooResolver(viewerID, id, db), nil
}

type FooResolver struct {
    viewerID    uuid.UUID
    id      uuid.UUID
    db      *sql.DB
}

func NewFooResolver(viewerID, id uuid.UUID, db *sql.DB) (*FooResolver, error) {
    return &FooResolver{
        viewerID: viewerID,
        id: id,
        db: db,
    }, nil
}

func (r *FooResolver) ID() graphql.ID {
    return graphql.ID(r.id.String())
}

func (r *FooResolver) Bar() (string, error) {
    // do some 403
    if !mayViewBar(r.viewerID, id) {
        return "", errors.New("403")
    }

    var bar string
    _ := r.db.QueryRow("SELECT bar FROM foo_bar WHERE id=?", r.id.Bytes()).Scan(&bar)
    return bar, nil
}

func (r *FooResolver) Baz) (*string, error) {
    // do some 403
    if !mayViewBaz(r.viewerID, id) {
        return "", errors.New("403")
    }

    var baz string
    _ := r.db.QueryRow("SELECT baz FROM foo_baz WHERE id=?", r.id.Bytes()).Scan(&baz)
    return &baz, nil
}

Notes:

  • It looks like type-asserting the viewer from context is the "best" available way right now
  • If your type in your graphql schema is nullable, then your resolver must return a pointer, with a nil pointer resolving to null
  • The above is a fairly naive implementation. Still want to add caching, for one. I've exhausted my database's connections quite quickly nesting 5 layers in. I'm really interested in some type of middleware layer that adds onto dataloader by also parsing the SQL queries and combining columns. Hopefully, some of that goes into Vitess' query optimizer too
  • One practice I think is good that I go partway towards is to handle your 401 before enterring graphql and putting your 403 logic in your custom type's constructor (not the resolver, but your domain types). Make your struct's fields unexported and have only one exported constructor. Now, on your exported methods on your custom type, handle 403 within that so that anytime you have data, you know that it's already been 403'd. That would mean moving the SELECT queries out of Bar() and Baz(), and moving them into the methods on that custom struct.

One other thing I've noticed while poking around the code is that there aren't many comments in general. It'd be really useful in general to at least have godoc comments. I'd like to be able to contribute to the code if possible, but it makes it that much more difficult to start without any docs or comments.

I'd be happy to contribute to a documentation, especially if there were clear governance and guidelines !

A tutorial would also be helpful, as obviously the hard part is getting started, even though the starwars example aswell as this repo help a lot.

I've been working on a closer-to-real-life example that you may find helpful: https://github.com/tonyghita/graphql-go-example

It just wraps the API at https://swapi.co and implements some patterns I've found useful in my year of experience working with GraphQL+Go.

I haven't had time to go through and document all the concepts, but hopefully it helps someone out as it is.

@tonyghita well that's the repo I linked in my previous message!
Thanks it indeed helped a lot, I greatly inspired from your work!

@SCKelemen Have you found out how mutations work? Any idea what that query looks like?

I've been looking at all the examples trying to implement whatever I can but I'm getting errors saying all fields in args need to be pointers. Has this changed?

@tendolukwago I actually haven't. I haven't been working with GraphQL lately. I've mostly switched to gRPC. I've had less issues, and the documentation is better. (It's slightly different, though) I may be revisiting this in a few months, depending on how my priorities are set, by my team.

@abradley2 i think you're getting that error because fields in your schema (files with the .graphql extension) aren't required or are left optional (Field! is required, Field is optional).

@SCKelemen interesting...haven't had a chance to check it out yet, but this ended up working for me (via POST request body):

{ "query": "mutation{create(name:\"Name\"){name, created}}"}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

bsr203 picture bsr203  路  6Comments

vladimiroff picture vladimiroff  路  6Comments

rogchap picture rogchap  路  5Comments

saward picture saward  路  6Comments

0xTanvir picture 0xTanvir  路  5Comments