SlideShare a Scribd company logo
GRAPHQL IN GO
MODERNWEB 2018
ABOUT ME
Open source contributor
1. Gitea
2. Drone
3. Gin
appleboy @ GitHub
appleboy @ twitter
appleboy @ slideshare
appleboy46 @ facebook
2
AGENDA
▸ Why we moving API from REST to Graphql?
▸ What is Graphql?
▸ Graphql in Golang (Why we choose Golang)
▸ How to testing Graphql in Golang
▸ Deploy Graphql application
MOVING REST TO GRAPHQL
icon from https://bit.ly/2LhOpZA
1. LARGE REQUEST IN SINGLE PAGE
https://bit.ly/2NpS4Fu
1. LARGE REQUEST IN SINGLE PAGE
CUSTOM FIELDS
/api/scene/1?field=name,description,created_at
CUSTOMENDPOINT
/api/scene/1/mobile?field=name,description
/api/scene/1/web?field=name,description
DOCUMENTATION?
HOW TO GENERATE DOCUMENT AUTOMATICALLY?
API DOC
Swagger
UPDATE API
GENERATE DOC
BACKEND
FRONTEND MOBILETESTING
/**
* @api {get} /scene/:id Request Scene information
* @apiName GetScene
* @apiGroup Scene
*
* @apiParam {Number} id Scenes unique ID.
*
* @apiSuccess {String} title Title of the Scene.
* @apiSuccess {String} desc Description of the Scene.
*/
GRAPHQL
WHATISGRAPHQL?
IT IS A QUERY LANGUAGE
{
myScene {
name
description
}
}
LEXED
PARSED
VALIDATED
EXECUTED
{
scene(id: 1) {
name
description
}
}
LEXED
PARSED
VALIDATED
EXECUTED
{
scene(id: 1) {
name
description
items(type: DEVICE) {
id
name
url
}
}
}
Graphql Request
{
"scene": {
"name": "foo",
"description": “bar",
"items": [{
"id": 1,
"name": "test_1",
"url": "http://foo.com"
}, {
"id": 2,
"name": "test_2",
"url": "http://bar.com"
}]
}
}
JSON Response
/api/scene/1
/api/scene/1/item
/api/scene/1/all
TYPE SYSTEM
GRAPHQL LANGUAGE
{
myScene {
name
description
}
}
{
scene(id: 1) {
name
description
items(type: DEVICE) {
id
name
url
}
}
}
type QueryRoot {
myScene: Scene
scene(id: ID!): Scene
}
{
scene(id: 1) {
name
description
items(type: DEVICE) {
id
name
url
}
}
}
Graphql Request
type Scene {
name: String!
description: String
items(type: ItemEnum): [Item]
}
enum ItemEnum {
DEVICE,
TEST_DEVICE,
URL
}
{
scene(id: 1) {
name
description
items(type: DEVICE) {
id
name
url
}
}
}
Graphql Request
type Item {
id: ID!
name: String!
url: String
}
INTROSPECTION
GRAPHQL LANGUAGE
{
__schema {
queryType {
name
}
}
}
{
"data": {
"__schema": {
"queryType": {
"name": "Query"
}
}
}
}
WHY NEED THE INTROSPECTION
▸code generation
▸auto documentation
▸static validation
▸IDE Integration
https://github.com/prismagraphql/graphql-playground
https://www.prisma.io/blog/introducing-graphql-playground-f1e0a018f05d/
RESOLVINGFIELDS
GRAPHQL LANGUAGE
type Item {
id: ID
type: sceneEnum
name: String
}
    "id": &graphql.Field{
      Type: graphql.Int,
    },
INT FLOAT STRING BOOLEAN ID
type Item {
id: ID
type: sceneEnum
name: String
}
    "TEST_DEVICE": &graphql.EnumValueConfig{
      Value: int(model.TypeTestDevice),
      Description: "test device.",
    },
    "DEVICE": &graphql.EnumValueConfig{
      Value: int(model.TypeDevice),
      Description: "device.",
    },
    "URL": &graphql.EnumValueConfig{
      Value: int(model.TypeURL),
      Description: "url.",
    },
"name": &graphql.Field{
Type: graphql.String,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
o, _ := p.Source.(*model.Scene)
return "Hello, " + o.Name, nil
},
},
Return Custom Value
MUTATION
GRAPHQL LANGUAGE
mutation {
createScene(
title: "foo",
description: "bar"
) {
id
title
description
url
}
}
// Schema is the GraphQL schema served by the server.
var Schema, _ = graphql.NewSchema(graphql.SchemaConfig{
  Query: rootQuery,
  Mutation: rootMutation,
  Types: types,
})
Query and Mutation
SINGLE ENDPOINT
POST /graphql
GRAPHQL IN GOLANG
WHY WE CHOOSE GOLANG?
WHY WE CHOOSE GOLANG?
▸Compiles Into Single Binary
▸Static Type System
▸Performance
▸Easy to Deploy
▸Learning Curve
BENCHMARKOFGRAPHQLFRAMEWORKINGOLANG
▸graphql-go/graphql
▸playlyfe/go-graphql
▸graph-gophers/graphql-go
▸samsarahq/thunder
https://github.com/appleboy/golang-graphql-benchmark
$	go	test	-v	-bench=.	-benchmem
VEKTAH/GQLGEN
GO GENERATE BASED GRAPHQL SERVER
LIBRARY
https://github.com/vektah/gqlgenhttps://youtu.be/3dpqYMqyiOg
GraphQL IN Golang
API SERVER
API SERVER
GRAPHQLSERVER
GRAPHQL GATEWAY
GRAPHQLINGOLANG
PACKAGE IN GOLANG
FRAMEWORKINGO
func Handler() gin.HandlerFunc {
  h := handler.New(&handler.Config{
    Schema: &schema.Schema,
    Pretty: true,
  })
  return func(c *gin.Context) {
    h.ServeHTTP(c.Writer, c.Request)
  }
}
graphql schema
JWT TOKEN
SERVER
GRAPHQL
SERVER
func Check() gin.HandlerFunc {
  return func(c *gin.Context) {
    token := c.GetHeader("Authorization")
    user, err := authUser(token)
    if token != "" && err != nil {
      logrus.Errorf("authUser error: %s", err.Error())
    }
    if user != nil && user.Email != "" {
      c.Set("email", user.Email)
      c.Set("user_id", user.UserID)
    }
    ctx := context.WithValue(
      c.Request.Context(),
      config.ContextKeyUser,
      user,
    )
    c.Request = c.Request.WithContext(ctx)
  }
}
store user data
var Schema, _ = graphql.NewSchema(graphql.SchemaConfig{
  Query: rootQuery,
  Mutation: rootMutation,
})
var rootMutation = graphql.NewObject(
  graphql.ObjectConfig{
    Name: "RootMutation",
    Description: "Root Mutation",
    Fields: graphql.Fields{
      "updateUser": &updateUser,
      "createScene": &createScene,
      "updateScene": &updateScene,
      "deleteScene": &deleteScene,
    },
  })
var rootQuery = graphql.NewObject(
  graphql.ObjectConfig{
    Name: "RootQuery",
    Description: "Root Query",
    Fields: graphql.Fields{
      "queryUser": &queryUser,
      "searchUser": &searchUser,
      "queryScene": &queryScene,
    },
  })
query
mutation
GRAPHQL ERROR
BETTER ERROR HANDLING
{	
		"data":	{	
				"post":	null	
		},	
		"errors":	[	
				{	
						"message":	"Internal	Error:	404	not	found",	
						"locations":	[	
								{	
										"line":	2,	
										"column":	3	
								}	
						],	
						"path":	[	
								"post"	
						]	
				}	
		]	
}	
single message
error location
func (g FormattedError) MarshalJSON() ([]byte, error)
{
  m := map[string]interface{}{}
  for k, v := range g.Extensions {
    m[k] = v
  }
  m["message"] = g.Message
  m["locations"] = g.Locations
  return json.Marshal(m)
}
Marshal JSON
custom key value
func FormatError(err error, arg ...interface{}) gqlerrors.FormattedError
{
  switch err := err.(type) {
  case gqlerrors.FormattedError:
    return err
  case *Error:
    return gqlerrors.FormattedError{
      Message: fmt.Sprintf(err.Error(), arg...),
      Extensions: gqlerrors.ErrorExtensions{
        "code": err.Type.Code(),
        "type": err.Type,
      },
    }
} custom error
original error
GRAPHQL N+1
DATA LOADER
1. LARGE REQUEST IN SINGLE PAGE
{
myScene(id: 1) {
name
description
role {
id
user {
email
}
model
}
}
}
ORM IN GOLANG
GOORM VS XORM
type Scene struct {
  ID int64 `xorm:"pk autoincr" json:"id"`
  Image string `json:"image,omitempty"`
  CreatedAt time.Time `json:"createdAt,omitempty"`
  UpdatedAt time.Time `json:"updatedAt,omitempty"`
  DeletedAt time.Time `xorm:"deleted"`
  // reference
  Items []*SceneItem `xorm:"-" json:"items,omitempty"`
  User *User `xorm:"-" json:"user,omitempty"`
  Role *SceneAccess `xorm:"-" json:"role,omitempty"`
}
Data Model
user role permission
 "user": &graphql.Field{
   Type: userType,
   Resolve: func(p graphql.ResolveParams) (interface{}, error) {
     o, ok := p.Source.(*model.Shorten)
     if !ok {
       return nil, errMissingSource
     }
     if o.User != nil {
       return o.User, nil
     }
     return getUserFromLoader(p.Context, o.UserID)
   },
 },
exist in model?
fetch from loader
GET DATA FROM DATABASE IF NOT EXIST
func userBatch(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
  var results []*dataloader.Result
  id, _ := helper.GetCacheID(keys[0].String())
  user, err := model.GetUserByID(id.(int64))
  results = append(results, &dataloader.Result{
    Data: user,
    Error: err,
  })
  return results
}
fetch from DB
GRAPHQLDATALOADER
ONLY SUPPORT MEMORY, LRU OR TTL
CACHE
INTEGRATE WITH REDIS SERVICE
GRAPHQLDATALOADER
func	batchFunc(_	context.Context,	keys	dataloader.Keys)	[]*dataloader.Result	
{	
		results	:=	make([]*dataloader.Result,	len(keys))		
		//	get	what	you	can	from	redis	
		values,	_	:=	redisClient.MGet(...keys.Keys()).Result()	
		//	make	a	list	of	everything	that	was	not	found	in	redis	
		var	cacheMisses	map[int]string	
		for	i	:=	range	keys	{	
				if	values[i]	==	redis.Nil	{	
						cacheMisses[i]	=	keys[i].String()	
				}	else	{	
						results[i]	=	&dataloader.Result{values[i],	nil}	
				}	
		}	
		//	get	the	missing	items	from	more	expensive	location	(like	DB)	
		for	idx,	key	:=	range	cacheMisses	{	
				value,	err	:=	db.GetValues(key)	//	Pseudo	code!	
				redisClient.Set(key,	value)	
				results[idx]	=	&dataloader.Result{value,	err}	
		}	
		return	results	
}	
miss from redis
fetch from DB
HOW TO TEST GRAPHQL SCHEMA?
GRAPHQL TESTING
https://code.likeagirl.io/the-7-steps-to-a-complete-code-review-abdfd39e75f1
RAILS-LIKE TEST FIXTURES FOR GO
GO TEST FIXTURES
https://github.com/go-testfixtures/testfixtures
-
id: 1
email: foo@gmail.com
full_name: foo
avatar: http://foo.com
avatar_email: foo@gmail.com
-
id: 2
email: bar@gmail.com
full_name: bar
avatar: http://bar.com
avatar_email: bar@gmail.com
real data in DB
    test := T{
      Query: `
query QueryShortenURL (
$slug: String!
) {
QueryShortenURL(slug: $slug) {
url
}
}
   `,
      Schema: Schema,
      Expected: &graphql.Result{
        Data: map[string]interface{}{
          "QueryShortenURL": map[string]interface{}{
            "url": "http://example.com",
          },
        },
      },
    }
graphql query
expect data
  params := graphql.Params{
    Schema: test.Schema,
    RequestString: test.Query,
    Context: ctx,
    VariableValues: map[string]interface{}{
      "slug": "abcdef",
    },
  }
  testGraphql(test, params, t)
ctx := newContextWithUser(context.TODO(), user)
user data in context
graphql variable
all pass
testing in sqlite
GRAPHQLDEPLOYMENT
HOW TO DEPLOY GOLANG APP?
GO-GGZ/GGZ
AN URL SHORTENER SERVICE WRITTEN IN GO
https://github.com/go-ggz/ggz
Thanks

More Related Content

GraphQL IN Golang