Just a different approach to making graphql servers in Go
- Easy to use and not much code required
- Schema based on code
- Build on top of the graphql spec 2021
- No code generators
- Only 1 dependency
- Easy to implement in many web servers, see the gin and fiber examples
- File upload support
- Supports Apollo tracing
- Fast
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":{}}
}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"}These go data kinds should be globally accepted:
boolintall bit sizesuintall bit sizesfloatall bit sizesarrayptrstringstruct
There are also special values:
time.Timeconverted from/to ISO 8601*multipart.FileHeaderget file from multipart form
struct {
// internal fields are ignored
bar string
// ignore public fields
Bar string `gq:"-"`
}struct {
// Change the graphql field name to "bar"
Foo string `gq:"bar"`
}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
}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
}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
}You can add *yarql.Ctx to every resolver of func field to get more information
about the request or user set properties
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",
},
}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{}
}All types that might be nil will be optional fields, by default these fields
are:
- Pointers
- Arrays
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)
}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
}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)
}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
There is a pkg.go.dev mjarkk/go-graphql/tester package available with handy tools for testing the schema
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/opCompared 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- graph-gophers/graphql-go ❤️ The library that inspired me to make this one
- ccbrown/api-fu
- 99designs/gqlgen
- graphql-go/graphql
