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:
bool
int
all bit sizesuint
all bit sizesfloat
all bit sizesarray
ptr
string
struct
There are also special values:
time.Time
converted from/to ISO 8601*multipart.FileHeader
get 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/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
- graph-gophers/graphql-go ❤️ The library that inspired me to make this one
- ccbrown/api-fu
- 99designs/gqlgen
- graphql-go/graphql