Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
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: 27 days 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 (over 3 years ago)
- Default Branch: main
- Last Pushed: 2022-04-18T13:47:13.000Z (over 2 years ago)
- Last Synced: 2024-10-01T16:27:36.215Z (about 1 month 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
![Banner](https://github.com/mjarkk/yarql/blob/main/banner.png?raw=true)
[![Go Reference](https://pkg.go.dev/badge/github.com/mjarkk/yarql.svg)](https://pkg.go.dev/github.com/mjarkk/yarql)
[![Go Report Card](https://goreportcard.com/badge/github.com/mjarkk/yarql)](https://goreportcard.com/report/github.com/mjarkk/yarql)
[![Coverage Status](https://coveralls.io/repos/github/mjarkk/go-graphql/badge.svg?branch=main)](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 mainimport (
"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 uint8const (
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)