https://github.com/mjarkk/yarql
YarQL!! A different approach to making GraphQL servers in Go
https://github.com/mjarkk/yarql
go golang graphql graphql-server
Last synced: about 1 year ago
JSON representation
YarQL!! A different approach to making GraphQL servers in Go
- Host: GitHub
- URL: https://github.com/mjarkk/yarql
- Owner: mjarkk
- License: mit
- Created: 2021-05-07T13:25:57.000Z (about 5 years ago)
- Default Branch: main
- Last Pushed: 2022-04-18T13:47:13.000Z (about 4 years ago)
- Last Synced: 2025-04-13T11:42:04.566Z (about 1 year ago)
- Topics: go, golang, graphql, graphql-server
- Language: Go
- Homepage:
- Size: 14.7 MB
- Stars: 23
- Watchers: 4
- Forks: 1
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README

[](https://pkg.go.dev/github.com/mjarkk/yarql)
[](https://goreportcard.com/report/github.com/mjarkk/yarql)
[](https://coveralls.io/github/mjarkk/go-graphql?branch=main)
# YarQL, A Graphql library for GoLang
Just a different approach to making graphql servers in Go
## Features
- Easy to use and not much code required
- Schema based on code
- Build on top of the [graphql spec 2021](https://spec.graphql.org/October2021/)
- No code generators
- [Only 1 dependency](go.mod)
- Easy to implement in many web servers, see the
[gin](https://github.com/mjarkk/yarql/blob/main/examples/gin/main.go) and
[fiber](https://github.com/mjarkk/yarql/blob/main/examples/fiber/main.go)
examples
- [File upload support](#file-upload)
- Supports [Apollo tracing](https://github.com/apollographql/apollo-tracing)
- [Fast](#Performance)
## Example
See the [/examples](https://github.com/mjarkk/yarql/tree/main/examples) folder
for more examples
```go
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:
```go
struct {
A string
}
```
A resolver function inside the a struct:
```go
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_
```go
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
```go
struct {
// internal fields are ignored
bar string
// ignore public fields
Bar string `gq:"-"`
}
```
### Rename field
```go
struct {
// Change the graphql field name to "bar"
Foo string `gq:"bar"`
}
```
### Label as ID field
```go
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
```go
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.
```go
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
```go
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`:
```go
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`)
```go
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
```go
// 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`
```go
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/](./examples/relay/backend/)
```go
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](https://spec.graphql.org/October2021/#sec--include)_
- `@skip(if: Boolean!)` _on Fields and fragments,
[spec](https://spec.graphql.org/October2021/#sec--skip)_
To add custom directives:
```go
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](https://github.com/jaydenseric/graphql-multipart-request-spec)
tough this is based on
[graphql-multipart-request-spec #55](https://github.com/jaydenseric/graphql-multipart-request-spec/issues/55)_
In your go code add `*multipart.FileHeader` to a methods inputs
```go
func (SomeStruct) ResolveUploadFile(args struct{ File *multipart.FileHeader }) string {
// ...
}
```
In your graphql query you can now do:
```gql
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](https://pkg.go.dev/github.com/mjarkk/yarql/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_
```sh
# 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](https://github.com/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!
```sh
# 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
- [graph-gophers/graphql-go](https://github.com/graph-gophers/graphql-go)
:heart: The library that inspired me to make this one
- [ccbrown/api-fu](https://github.com/ccbrown/api-fu)
- [99designs/gqlgen](https://github.com/99designs/gqlgen)
- [graphql-go/graphql](https://github.com/graphql-go/graphql)