Slide 1

Slide 1 text

GRAPHQL IN GO MODERNWEB 2018

Slide 2

Slide 2 text

ABOUT ME Open source contributor 1. Gitea 2. Drone 3. Gin appleboy @ GitHub appleboy @ twitter appleboy @ slideshare appleboy46 @ facebook 2

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

MOVING REST TO GRAPHQL icon from https://bit.ly/2LhOpZA

Slide 5

Slide 5 text

1. LARGE REQUEST IN SINGLE PAGE https://bit.ly/2NpS4Fu

Slide 6

Slide 6 text

1. LARGE REQUEST IN SINGLE PAGE

Slide 7

Slide 7 text

CUSTOM FIELDS /api/scene/1?field=name,description,created_at

Slide 8

Slide 8 text

CUSTOM ENDPOINT /api/scene/1/mobile?field=name,description /api/scene/1/web?field=name,description

Slide 9

Slide 9 text

DOCUMENTATION? HOW TO GENERATE DOCUMENT AUTOMATICALLY? API DOC Swagger

Slide 10

Slide 10 text

UPDATE API GENERATE DOC BACKEND FRONTEND MOBILE TESTING

Slide 11

Slide 11 text

/** * @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. */

Slide 12

Slide 12 text

GRAPHQL

Slide 13

Slide 13 text

WHAT IS GRAPHQL? IT IS A QUERY LANGUAGE

Slide 14

Slide 14 text

{ myScene { name description } } LEXED PARSED VALIDATED EXECUTED

Slide 15

Slide 15 text

{ scene(id: 1) { name description } } LEXED PARSED VALIDATED EXECUTED

Slide 16

Slide 16 text

{ scene(id: 1) { name description items(type: DEVICE) { id name url } } } Graphql Request

Slide 17

Slide 17 text

{ "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

Slide 18

Slide 18 text

/api/scene/1 /api/scene/1/item /api/scene/1/all

Slide 19

Slide 19 text

TYPE SYSTEM GRAPHQL LANGUAGE

Slide 20

Slide 20 text

{ myScene { name description } }

Slide 21

Slide 21 text

{ scene(id: 1) { name description items(type: DEVICE) { id name url } } }

Slide 22

Slide 22 text

type QueryRoot { myScene: Scene scene(id: ID!): Scene }

Slide 23

Slide 23 text

{ scene(id: 1) { name description items(type: DEVICE) { id name url } } } Graphql Request

Slide 24

Slide 24 text

type Scene { name: String! description: String items(type: ItemEnum): [Item] } enum ItemEnum { DEVICE, TEST_DEVICE, URL }

Slide 25

Slide 25 text

{ scene(id: 1) { name description items(type: DEVICE) { id name url } } } Graphql Request

Slide 26

Slide 26 text

type Item { id: ID! name: String! url: String }

Slide 27

Slide 27 text

INTROSPECTION GRAPHQL LANGUAGE

Slide 28

Slide 28 text

{ __schema { queryType { name } } }

Slide 29

Slide 29 text

{ "data": { "__schema": { "queryType": { "name": "Query" } } } }

Slide 30

Slide 30 text

WHY NEED THE INTROSPECTION ▸code generation ▸auto documentation ▸static validation ▸IDE Integration

Slide 31

Slide 31 text

https://github.com/prismagraphql/graphql-playground

Slide 32

Slide 32 text

https://www.prisma.io/blog/introducing-graphql-playground-f1e0a018f05d/

Slide 33

Slide 33 text

RESOLVING FIELDS GRAPHQL LANGUAGE

Slide 34

Slide 34 text

type Item { id: ID type: sceneEnum name: String }

Slide 35

Slide 35 text

"id": &graphql.Field{ Type: graphql.Int, }, INT FLOAT STRING BOOLEAN ID

Slide 36

Slide 36 text

type Item { id: ID type: sceneEnum name: String }

Slide 37

Slide 37 text

"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.", },

Slide 38

Slide 38 text

"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

Slide 39

Slide 39 text

MUTATION GRAPHQL LANGUAGE

Slide 40

Slide 40 text

mutation { createScene( title: "foo", description: "bar" ) { id title description url } }

Slide 41

Slide 41 text

// Schema is the GraphQL schema served by the server. var Schema, _ = graphql.NewSchema(graphql.SchemaConfig{ Query: rootQuery, Mutation: rootMutation, Types: types, }) Query and Mutation

Slide 42

Slide 42 text

SINGLE ENDPOINT POST /graphql

Slide 43

Slide 43 text

GRAPHQL IN GOLANG WHY WE CHOOSE GOLANG?

Slide 44

Slide 44 text

WHY WE CHOOSE GOLANG? ▸Compiles Into Single Binary ▸Static Type System ▸Performance ▸Easy to Deploy ▸Learning Curve

Slide 45

Slide 45 text

BENCHMARK OF GRAPHQL FRAMEWORK IN GOLANG ▸graphql-go/graphql ▸playlyfe/go-graphql ▸graph-gophers/graphql-go ▸samsarahq/thunder

Slide 46

Slide 46 text

https://github.com/appleboy/golang-graphql-benchmark $ go test -v -bench=. -benchmem

Slide 47

Slide 47 text

VEKTAH/GQLGEN GO GENERATE BASED GRAPHQL SERVER LIBRARY https://github.com/vektah/gqlgen https://youtu.be/3dpqYMqyiOg

Slide 48

Slide 48 text

No content

Slide 49

Slide 49 text

API SERVER API SERVER GRAPHQL SERVER GRAPHQL GATEWAY

Slide 50

Slide 50 text

GRAPHQL IN GOLANG PACKAGE IN GOLANG

Slide 51

Slide 51 text

FRAMEWORK IN GO

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

JWT TOKEN SERVER GRAPHQL SERVER

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

GRAPHQL ERROR BETTER ERROR HANDLING

Slide 57

Slide 57 text

{ "data": { "post": null }, "errors": [ { "message": "Internal Error: 404 not found", "locations": [ { "line": 2, "column": 3 } ], "path": [ "post" ] } ] } single message error location

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

GRAPHQL N+1 DATA LOADER

Slide 61

Slide 61 text

1. LARGE REQUEST IN SINGLE PAGE

Slide 62

Slide 62 text

{ myScene(id: 1) { name description role { id user { email } model } } }

Slide 63

Slide 63 text

ORM IN GOLANG GOORM VS XORM

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

"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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

GRAPHQL DATALOADER ONLY SUPPORT MEMORY, LRU OR TTL CACHE

Slide 68

Slide 68 text

INTEGRATE WITH REDIS SERVICE GRAPHQL DATALOADER

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

HOW TO TEST GRAPHQL SCHEMA? GRAPHQL TESTING

Slide 71

Slide 71 text

https://code.likeagirl.io/the-7-steps-to-a-complete-code-review-abdfd39e75f1

Slide 72

Slide 72 text

RAILS-LIKE TEST FIXTURES FOR GO GO TEST FIXTURES https://github.com/go-testfixtures/testfixtures

Slide 73

Slide 73 text

- 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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

all pass testing in sqlite

Slide 77

Slide 77 text

GRAPHQL DEPLOYMENT HOW TO DEPLOY GOLANG APP?

Slide 78

Slide 78 text

GO-GGZ/GGZ AN URL SHORTENER SERVICE WRITTEN IN GO https://github.com/go-ggz/ggz

Slide 79

Slide 79 text

Thanks