yarql

package module
v0.9.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 18, 2022 License: MIT Imports: 17 Imported by: 0

README

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

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Implements

func Implements(interfaceValue interface{}, typeValue interface{}) bool

Implements registers a new type that implementation an interface The interfaceValue should be a pointer to the interface type like: (*InterfaceType)(nil) The typeValue should be a empty struct that implements the interfaceValue

Example:

var _ = Implements((*InterfaceType)(nil), StructThatImplements{})

func TypeRename

func TypeRename(goType interface{}, newName string, force ...bool) string

TypeRename renames the graphql type of the input type By default the typename of the struct is used but you might want to change this form time to time and with this you can

Types

type AttrIsID

type AttrIsID uint8

AttrIsID can be added to a method response to make it a ID field For example:

func (Foo) ResolveExampleMethod() (string, AttrIsID) {
  return "i'm an ID type now", 0
}

Not that the response value doesn't matter

type Ctx

type Ctx struct {
	// contains filtered or unexported fields
}

Ctx contains all runtime information of a query

func (*Ctx) GetContext added in v0.9.0

func (ctx *Ctx) GetContext() context.Context

GetContext returns the Go request context

func (*Ctx) GetPath

func (ctx *Ctx) GetPath() json.RawMessage

GetPath returns the graphql path to the current field json encoded

func (*Ctx) GetValue

func (ctx *Ctx) GetValue(key string) (value interface{})

GetValue returns a user defined value

func (*Ctx) GetValueOk

func (ctx *Ctx) GetValueOk(key string) (value interface{}, found bool)

GetValueOk returns a user defined value with a boolean indicating if the value was found

func (*Ctx) SetContext added in v0.9.0

func (ctx *Ctx) SetContext(newContext context.Context)

SetContext overwrites the request's Go context

func (*Ctx) SetValue

func (ctx *Ctx) SetValue(key string, value interface{})

SetValue sets a user defined value

type Directive

type Directive struct {
	// Required
	Name  string
	Where []DirectiveLocation
	// Should be of type: func(args like any other method) DirectiveModifier
	Method interface{}

	// Not required
	Description string
	// contains filtered or unexported fields
}

Directive is what defines a directive

type DirectiveLocation

type DirectiveLocation uint8

DirectiveLocation defines the location a directive can be used in

const (
	// DirectiveLocationField can be called from a field
	DirectiveLocationField DirectiveLocation = iota
	// DirectiveLocationFragment can be called from a fragment
	DirectiveLocationFragment
	// DirectiveLocationFragmentInline can be called from a inline fragment
	DirectiveLocationFragmentInline
)

func (DirectiveLocation) String

func (l DirectiveLocation) String() string

String returns the DirectiveLocation as a string

func (DirectiveLocation) ToQlDirectiveLocation

func (l DirectiveLocation) ToQlDirectiveLocation() __DirectiveLocation

ToQlDirectiveLocation returns the matching graphql location

type DirectiveModifier

type DirectiveModifier struct {
	// Skip field/(inline)fragment
	Skip bool
}

DirectiveModifier defines modifications to the response Nothing is this struct is required and will be ignored if not set

type ErrorWPath

type ErrorWPath struct {
	// contains filtered or unexported fields
}

ErrorWPath is an error mesage with a graphql path to the field that created the error

func (ErrorWPath) Error

func (e ErrorWPath) Error() string

type RequestOptions

type RequestOptions struct {
	Context     context.Context                                 // Request context can be used to verify
	Values      map[string]interface{}                          // Passed directly to the request context
	GetFormFile func(key string) (*multipart.FileHeader, error) // Get form file to support file uploading
	Tracing     bool                                            // https://github.com/apollographql/apollo-tracing
}

RequestOptions are extra options / arguments for the (*Schema).HandleRequest method

type ResolveOptions

type ResolveOptions struct {
	NoMeta         bool            // Returns only the data
	Context        context.Context // Request context
	OperatorTarget string
	Values         *map[string]interface{}                         // Passed directly to the request context
	GetFormFile    func(key string) (*multipart.FileHeader, error) // Get form file to support file uploading
	Variables      string                                          // Expects valid JSON or empty string
	Tracing        bool                                            // https://github.com/apollographql/apollo-tracing
}

ResolveOptions are options for the (*Schema).Resolve method

type Schema

type Schema struct {
	MaxDepth uint8 // Default 255

	// Zero alloc variables
	Result []byte
	// contains filtered or unexported fields
}

Schema defines the graphql schema

func NewSchema

func NewSchema() *Schema

NewSchema creates a new schema wherevia you can define the graphql types and make queries

func (*Schema) Copy

func (s *Schema) Copy() *Schema

Copy is meant to be used to create a pool of schema objects The function itself is quiet slow so don't use this function in every request

func (*Schema) HandleRequest

func (s *Schema) HandleRequest(
	method string,
	getQuery func(key string) string,
	getFormField func(key string) (string, error),
	getBody func() []byte,
	contentType string,
	options *RequestOptions,
) ([]byte, []error)

HandleRequest handles a http request and returns a response

func (*Schema) Parse

func (s *Schema) Parse(queries interface{}, methods interface{}, options *SchemaOptions) error

Parse parses your queries and methods

func (*Schema) RegisterDirective

func (s *Schema) RegisterDirective(directive Directive) error

RegisterDirective registers a new directive

func (*Schema) RegisterEnum

func (s *Schema) RegisterEnum(enumMap interface{}) (added bool, err error)

RegisterEnum registers a new enum type

func (*Schema) Resolve

func (s *Schema) Resolve(query []byte, opts ResolveOptions) []error

Resolve resolves a query and returns errors if any The result json is written to (*Schema).Result

func (*Schema) SetCacheRules

func (s *Schema) SetCacheRules(
	cacheQueryFromLen *int,
)

SetCacheRules sets the cacheing rules

type SchemaOptions

type SchemaOptions struct {
	SkipGraphqlTypesInjection bool
	// contains filtered or unexported fields
}

SchemaOptions are options for creating a new schema

Directories

Path Synopsis
difflib
Package difflib is a partial port of Python difflib module.
Package difflib is a partial port of Python difflib module.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL