Skip to content

mjarkk/yarql

Repository files navigation

Banner

Go Reference Go Report Card Coverage Status

YarQL, A Graphql library for GoLang

Just a different approach to making graphql servers in Go

Features

Example

See the /examples folder for more examples

package main

import (
    "log"
    "github.com/mjarkk/yarql"
)

type Post struct {
	Id    uint `gq:",ID"`
	Title string `gq:"name"`
}

type QueryRoot struct{}

func (QueryRoot) ResolvePosts() []Post {
	return []Post{
		{1, "post 1"},
		{2, "post 2"},
		{3, "post 3"},
	}
}

type MethodRoot struct{}

func main() {
	s := yarql.NewSchema()

    err := s.Parse(QueryRoot{}, MethodRoot{}, nil)
	if err != nil {
		log.Fatal(err)
	}

	errs := s.Resolve([]byte(`
		{
			posts {
				id
				name
			}
		}
	`), yarql.ResolveOptions{})
	for _, err := range errs {
		log.Fatal(err)
	}

    fmt.Println(string(s.Result))
    // {"data": {
    //   "posts": [
    //     {"id": "1", "name": "post 1"},
    //     {"id": "2", "name": "post 2"},
    //     {"id": "3", "name": "post 3"}
    //   ]
    // },"errors":[],"extensions":{}}
}

Docs

Defining a field

All fields names are by default changed to graphql names, for example VeryNice changes to veryNice. There is one exception to the rule when the second letter is also upper case like FOO will stay FOO

In a struct:

struct {
	A string
}

A resolver function inside the a struct:

struct {
	A func() string
}

A resolver attached to the struct.

Name Must start with Resolver followed by one uppercase letter

The resolve identifier is trimmed away in the graphql name

type A struct {}
func (A) ResolveA() string {return "Ahh yea"}

Supported input and output value types

These go data kinds should be globally accepted:

  • bool
  • int all bit sizes
  • uint all bit sizes
  • float all bit sizes
  • array
  • ptr
  • string
  • struct

There are also special values:

  • time.Time converted from/to ISO 8601
  • *multipart.FileHeader get file from multipart form

Ignore fields

struct {
	// internal fields are ignored
	bar string

	// ignore public fields
	Bar string `gq:"-"`
}

Rename field

struct {
	// Change the graphql field name to "bar"
	Foo string `gq:"bar"`
}

Label as ID field

struct Foo {
	// Notice the "," before the id
	Id string `gq:",id"`

	// Pointers and numbers are also supported
	// NOTE NUMBERS WILL BE CONVERTED TO STRINGS IN OUTPUT
	PostId *int `gq:",id"`
}

// Label method response as ID using AttrIsID
// The value returned for AttrIsID is ignored
// You can also still just fine append an error: (string, AttrIsID, error)
func (Foo) ResolveExampleMethod() (string, AttrIsID) {
	return "i'm an ID type", 0
}

Methods and field arguments

Add a struct to the arguments of a resolver or func field to define arguments

func (A) ResolveUserID(args struct{ Id int }) int {
	return args.Id
}

Resolver error response

You can add an error response argument to send back potential errors.

These errors will appear in the errors array of the response.

func (A) ResolveMe() (*User, error) {
	me, err := fetchMe()
	return me, err
}

Context

You can add *yarql.Ctx to every resolver of func field to get more information about the request or user set properties

Context values

The context can store values defined by a key. You can add values by using the 'SetVelue' method and obtain values using the GetValue method

func (A) ResolveMe(ctx *yarql.Ctx) User {
	ctx.SetValue("resolved_me", true)
	return ctx.GetValue("me").(User)
}

You can also provide values to the RequestOptions:

yarql.RequestOptions{
	Values: map[string]interface{}{
		"key": "value",
	},
}

GoLang context

You can also have a GoLang context attached to our context (yarql.Ctx) by providing the RequestOptions with a context or calling the SetContext method on our context (yarql.Ctx)

import "context"

yarql.RequestOptions{
	Context: context.Background(),
}

func (A) ResolveUser(ctx *yarql.Ctx) User {
	c := ctx.GetContext()
	c = context.WithValue(c, "resolved_user", true)
	ctx.SetContext(c)

	return User{}
}

Optional fields

All types that might be nil will be optional fields, by default these fields are:

  • Pointers
  • Arrays

Enums

Enums can be defined like so

Side note on using enums as argument, It might return a nullish value if the user didn't provide a value

// The enum type, everywhere where this value is used it will be converted to an enum in graphql
// This can also be a: string, int(*) or uint(*)
type Fruit uint8

const (
	Apple Fruit = iota
	Peer
	Grapefruit
)

func main() {
	s := yarql.NewSchema()

	// The map key is the enum it's key in graphql
	// The map value is the go value the enum key is mapped to or the other way around
	// Also the .RegisterEnum(..) method must be called before .Parse(..)
	s.RegisterEnum(map[string]Fruit{
		"APPLE":      Apple,
		"PEER":       Peer,
		"GRAPEFRUIT": Grapefruit,
	})

	s.Parse(QueryRoot{}, MethodRoot{}, nil)
}

Interfaces

Graphql interfaces can be created using go interfaces

This library needs to analyze all types before you can make a query and as we cannot query all types that implement a interface you'll need to help the library with this by calling Implements for every implementation. If Implements is not called for a type the response value for that type when inside a interface will always be null

type QuerySchema struct {
	Bar      BarWImpl
	Baz      BazWImpl
	BarOrBaz InterfaceType
}

type InterfaceType interface {
	// Interface fields
	ResolveFoo() string
	ResolveBar() string
}

type BarWImpl struct{}

// Implements hints this library to register BarWImpl
// THIS MUST BE CALLED FOR EVERY TYPE THAT IMPLEMENTS InterfaceType
var _ = yarql.Implements((*InterfaceType)(nil), BarWImpl{})

func (BarWImpl) ResolveFoo() string { return "this is bar" }
func (BarWImpl) ResolveBar() string { return "This is bar" }

type BazWImpl struct{}
var _ = yarql.Implements((*InterfaceType)(nil), BazWImpl{})
func (BazWImpl) ResolveFoo() string { return "this is baz" }
func (BazWImpl) ResolveBar() string { return "This is baz" }
Relay Node example

For a full relay example see examples/relay/backend/

type Node interface {
	ResolveId() (uint, yarql.AttrIsID)
}

type User struct {
	ID    uint `gq:"-"` // ignored because of (User).ResolveId()
	Name  string
}

var _ = yarql.Implements((*Node)(nil), User{})

// ResolveId implements the Node interface
func (u User) ResolveId() (uint, yarql.AttrIsID) {
	return u.ID, 0
}

Directives

These directives are added by default:

  • @include(if: Boolean!) on Fields and fragments, spec
  • @skip(if: Boolean!) on Fields and fragments, spec

To add custom directives:

func main() {
	s := yarql.NewSchema()

	// Also the .RegisterEnum(..) method must be called before .Parse(..)
	s.RegisterDirective(Directive{
		// What is the name of the directive
		Name: "skip_2",

		// Where can this directive be used in the query
		Where: []DirectiveLocation{
			DirectiveLocationField,
			DirectiveLocationFragment,
			DirectiveLocationFragmentInline,
		},

		// This methods's input work equal to field arguments
		// tough the output is required to return DirectiveModifier
		// This method is called always when the directive is used
		Method: func(args struct{ If bool }) DirectiveModifier {
			return DirectiveModifier{
				Skip: args.If,
			}
		},

		// The description of the directive
		Description: "Directs the executor to skip this field or fragment when the `if` argument is true.",
	})

	s.Parse(QueryRoot{}, MethodRoot{}, nil)
}

File upload

NOTE: This is NOT graphql-multipart-request-spec tough this is based on graphql-multipart-request-spec #55

In your go code add *multipart.FileHeader to a methods inputs

func (SomeStruct) ResolveUploadFile(args struct{ File *multipart.FileHeader }) string {
	// ...
}

In your graphql query you can now do:

uploadFile(file: "form_file_field_name")

In your request add a form file with the field name: form_file_field_name

Testing

There is a pkg.go.dev mjarkk/go-graphql/tester package available with handy tools for testing the schema

Performance

Below shows a benchmark of fetching the graphql schema (query parsing + data fetching)

Note: This benchmark also profiles the cpu and that effects the score by a bit

# go test -benchmem -bench "^(BenchmarkResolve)\$"
# goos: darwin
# cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkResolve-12    	   13246	     83731 ns/op	    1344 B/op	      47 allocs/op
Compared to other libraries

Injecting resolver_benchmark_test.go > BenchmarkHelloWorldResolve into appleboy/golang-graphql-benchmark results in the following:

Take these results with a big grain of salt, i didn't use the last version of the libraries thus my result might be garbage compared to the others by now!

# go test -v -bench=Master -benchmem
# goos: darwin
# goarch: amd64
# pkg: github.com/appleboy/golang-graphql-benchmark
# cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkGoGraphQLMaster
BenchmarkGoGraphQLMaster-12          	   24992	     48180 ns/op	   26895 B/op	     445 allocs/op
BenchmarkPlaylyfeGraphQLMaster-12    	  320289	      3770 ns/op	    2797 B/op	      57 allocs/op
BenchmarkGophersGraphQLMaster-12     	  391269	      3114 ns/op	    3634 B/op	      38 allocs/op
BenchmarkThunderGraphQLMaster-12     	  708327	      1707 ns/op	    1288 B/op	      30 allocs/op
BenchmarkMjarkkGraphQLGoMaster-12    	 2560764	       466.5 ns/op	      80 B/op	       1 allocs/op

Alternatives