gingraphql

package module
v1.0.2 Latest Latest
Warning

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

Go to latest
Published: Feb 4, 2024 License: GPL-3.0 Imports: 10 Imported by: 0

README

GraphQL handler for gin

go workflow

This is a small package to provide a GraphQL handler that can be used with Gin Framework.

Features

  1. Fully tested.
  2. Supports context managers so user can add their application specific data to be used in resolver functions.
  3. Supports file upload out of the box.
  4. Fully compliant with GraphQL multipart specification, so client libraries like Apollo Upload Client will work out of the box.
  5. Allows adding additional http headers either by gin middleware, or right from the resolver functions.

Installation

To add the package to your project run -

go get -u github.com/asif-mahmud/gingraphql

Documentation

godoc: https://pkg.go.dev/github.com/asif-mahmud/gingraphql examples: https://pkg.go.dev/github.com/asif-mahmud/gingraphql#pkg-examples

Version history

Version 1.0.2
  • Updated go and package versions
  • Fixed github workflow
  • Fixed package name issue
Version 1.0.0

Stable version release

Version 0.9.0

A clone of the original package https://pkg.go.dev/github.com/asif-mahmud/graphqlgin to fix versioning.

Documentation

Index

Examples

Constants

View Source
const GinContextKey = "GinContext"

Key for setting `*gin.Context` value of the current request to the context

Variables

View Source
var UploadType = graphql.NewScalar(
	graphql.ScalarConfig{
		Name:        "Upload",
		Description: "File upload scalar",
		Serialize: func(value interface{}) interface{} {

			return value
		},
	},
)

GraphQL scalar to represent file upload variable

Functions

func GetGinContext

func GetGinContext(ctx context.Context) *gin.Context

Extracts and returns the current `*gin.Context` value from the context `ctx`.

func GinContextProvider

func GinContextProvider(c *gin.Context, ctx context.Context) context.Context

Returns a `ContextProviderFn` that will add the current `*gin.Context` value to the context passed down to resolver functions.

Types

type ContextProviderFn

type ContextProviderFn func(c *gin.Context, ctx context.Context) context.Context

Function to update or modify the context passed down to the resolver functions

type GraphQLApp

type GraphQLApp struct {
	Schema           graphql.Schema
	ContextProviders []ContextProviderFn
}

GraphQL app structure

Example (Add_custom_headers)
// Create schema
schema, _ := graphql.NewSchema(graphql.SchemaConfig{
	Query: graphql.NewObject(graphql.ObjectConfig{
		Name: "Query",
		Fields: graphql.Fields{
			"header": &graphql.Field{
				Type: graphql.String,
				Resolve: func(p graphql.ResolveParams) (interface{}, error) {
					c := GetGinContext(p.Context)
					c.Header("Custom-Header", "some-header-value")
					return "hello", nil
				},
			},
		},
	}),
})

// Create graphql app
app := New(schema)

// Create router
router := gin.Default()

// Add graphql handler
router.POST("/graphql", app.Handler())

// Run server
// router.Run()

// Sample output capture
req, _ := http.NewRequest(
	"POST",
	"/graphql",
	bytes.NewBuffer(
		[]byte(`{"query": "query { header }", "operationName": "", "variables": {}}`),
	),
)
req.Header.Add("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)

headers := w.Header()
body := string(w.Body.Bytes())

fmt.Println("Headers:")
fmt.Println(headers)
fmt.Println("Body:")
fmt.Println(body)
Output:

Headers:
map[Content-Type:[application/json; charset=utf-8] Custom-Header:[some-header-value]]
Body:
{"data":{"header":"hello"}}
Example (Context_usage)
// Your example database
userStore := []map[string]interface{}{
	{
		"name":  "John",
		"email": "a@b.c",
	},
	{
		"name":  "Kratos",
		"email": "c@b.a",
	},
}

// Create schema
schema, _ := graphql.NewSchema(graphql.SchemaConfig{
	Query: graphql.NewObject(graphql.ObjectConfig{
		Name: "Query",
		Fields: graphql.Fields{
			"users": &graphql.Field{
				Type: graphql.NewList(graphql.NewObject(graphql.ObjectConfig{
					Name: "User",
					Fields: graphql.Fields{
						"name": &graphql.Field{
							Type: graphql.String,
						},
						"email": &graphql.Field{
							Type: graphql.String,
						},
					},
				})),
				Resolve: func(p graphql.ResolveParams) (interface{}, error) {
					// retrieve the store/database from the context
					users := p.Context.Value("userStore").([]map[string]interface{})
					// use it as you want
					return users, nil
				},
			},
		},
	}),
})

// Create graphql app
// You can add your context provider here or when attaching the handler to a route
app := New(schema, func(c *gin.Context, ctx context.Context) context.Context {
	return context.WithValue(ctx, "userStore", userStore)
})

// Create router
router := gin.Default()

// Add handler
router.POST("/graphql", app.Handler())
// You could add your context providers here too
// router.POST("/graphql", app.Handler(func(c *gin.Context, ctx context.Context) context.Context {
// 	return context.WithValue(ctx, "userStore", userStore)
// }))

// Run server
// router.Run()

// Sample output capture
req, _ := http.NewRequest(
	"POST",
	"/graphql",
	bytes.NewBuffer(
		[]byte(
			`{"query": "query { users { name email } }", "operationName": "", "variables": {}}`,
		),
	),
)
req.Header.Add("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)

body := string(w.Body.Bytes())

fmt.Println(body)
Output:

{"data":{"users":[{"email":"a@b.c","name":"John"},{"email":"c@b.a","name":"Kratos"}]}}
Example (Multiple_file_upload)
//  create your own json representation of the uploaded file
uploadInfoType := graphql.NewObject(graphql.ObjectConfig{
	Name: "UploadInfo",
	Fields: graphql.Fields{
		"filename": &graphql.Field{
			Type: graphql.String,
			Resolve: func(p graphql.ResolveParams) (interface{}, error) {
				file := p.Source.(*multipart.FileHeader)
				return file.Filename, nil
			},
		},
		"size": &graphql.Field{
			Type: graphql.Int,
			Resolve: func(p graphql.ResolveParams) (interface{}, error) {
				file := p.Source.(*multipart.FileHeader)
				return file.Size, nil
			},
		},
	},
})
// Construct graphql schema
schema, _ := graphql.NewSchema(graphql.SchemaConfig{
	// just a dummy query, have to include a query with at least one field
	Query: graphql.NewObject(graphql.ObjectConfig{
		Name: "Query",
		Fields: graphql.Fields{
			"hello": &graphql.Field{
				Type: graphql.String,
				Resolve: func(p graphql.ResolveParams) (interface{}, error) {
					return "world", nil
				},
			},
		},
	}),
	Mutation: graphql.NewObject(graphql.ObjectConfig{
		Name: "Mutation",
		Fields: graphql.Fields{
			"uploadFiles": &graphql.Field{
				// use graphqlgin.UploadType like this for file upload scalar
				Args: graphql.FieldConfigArgument{
					"files": &graphql.ArgumentConfig{
						Type: graphql.NewList(UploadType),
					},
				},
				// use your own type to respond
				Type: graphql.NewList(uploadInfoType),
				// handle the uploaded file anyway you want
				Resolve: func(p graphql.ResolveParams) (interface{}, error) {
					return p.Args["files"], nil
				},
			},
		},
	}),
})

// Create graphql app
app := New(schema)

// Create router
router := gin.Default()

// Add graphql handler to the router
router.POST("/graphql", app.Handler())

// Run graphql server
// router.Run()

// Sample output capturing
buffer := bytes.NewBuffer(nil)
form := multipart.NewWriter(buffer)
form.WriteField(
	"operations",
	`{"query": "mutation ($files: [Upload!]!) { uploadFiles(files: $files) { filename size } }",
                  "operationName": "",
                  "variables": { "files": [null, null] }
                 }`,
)
form.WriteField(
	"map",
	`{"0": ["variables.files.0"], "1": ["variables.files.1"]}`,
)
w, _ := form.CreateFormFile("0", "hello.txt")
w.Write([]byte("Hello, World"))
w2, _ := form.CreateFormFile("1", "bingo.txt")
w2.Write([]byte("Bingo"))
form.Close()
req, _ := http.NewRequest("POST", "/graphql", buffer)
req.Header.Add("Content-Type", form.FormDataContentType())
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
body := rec.Body.Bytes()
fmt.Println(string(body))
Output:

{"data":{"uploadFiles":[{"filename":"hello.txt","size":12},{"filename":"bingo.txt","size":5}]}}
Example (Simple_usage)
// Construct graphql schema
schema, _ := graphql.NewSchema(graphql.SchemaConfig{
	Query: graphql.NewObject(graphql.ObjectConfig{
		Name: "Query",
		Fields: graphql.Fields{
			"hello": &graphql.Field{
				Type: graphql.String,
				Resolve: func(p graphql.ResolveParams) (interface{}, error) {
					return "world", nil
				},
			},
		},
	}),
})

// Create graphql app instance
app := New(schema)

// Create gin router
router := gin.Default()

// Add graphql handler to the router
router.POST("/graphql", app.Handler())

// Run app server
// router.Run()

// Example capturing of output
req, _ := http.NewRequest(
	"POST",
	"/graphql",
	bytes.NewBuffer([]byte(
		`{"query": "query Hello { hello }", "operationName": "Hello", "variables": {}}`,
	)))
req.Header.Add("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
data := w.Body.Bytes()
fmt.Println(string(data))
Output:

{"data":{"hello":"world"}}
Example (Single_file_upload)
//  create your own json representation of the uploaded file
uploadInfoType := graphql.NewObject(graphql.ObjectConfig{
	Name: "UploadInfo",
	Fields: graphql.Fields{
		"filename": &graphql.Field{
			Type: graphql.String,
			Resolve: func(p graphql.ResolveParams) (interface{}, error) {
				file := p.Source.(*multipart.FileHeader)
				return file.Filename, nil
			},
		},
		"size": &graphql.Field{
			Type: graphql.Int,
			Resolve: func(p graphql.ResolveParams) (interface{}, error) {
				file := p.Source.(*multipart.FileHeader)
				return file.Size, nil
			},
		},
	},
})
// Construct graphql schema
schema, _ := graphql.NewSchema(graphql.SchemaConfig{
	// just a dummy query, have to include a query with at least one field
	Query: graphql.NewObject(graphql.ObjectConfig{
		Name: "Query",
		Fields: graphql.Fields{
			"hello": &graphql.Field{
				Type: graphql.String,
				Resolve: func(p graphql.ResolveParams) (interface{}, error) {
					return "world", nil
				},
			},
		},
	}),
	Mutation: graphql.NewObject(graphql.ObjectConfig{
		Name: "Mutation",
		Fields: graphql.Fields{
			"uploadFile": &graphql.Field{
				// use graphqlgin.UploadType like this for file upload scalar
				Args: graphql.FieldConfigArgument{
					"file": &graphql.ArgumentConfig{
						Type: UploadType,
					},
				},
				// use your own type to respond
				Type: uploadInfoType,
				// handle the uploaded file anyway you want
				Resolve: func(p graphql.ResolveParams) (interface{}, error) {
					return p.Args["file"], nil
				},
			},
		},
	}),
})

// Create graphql app
app := New(schema)

// Create router
router := gin.Default()

// Add graphql handler to the router
router.POST("/graphql", app.Handler())

// Run graphql server
// router.Run()

// Sample output capturing
buffer := bytes.NewBuffer(nil)
form := multipart.NewWriter(buffer)
form.WriteField(
	"operations",
	`{"query": "mutation UploadFile ($file: Upload!) { uploadFile(file: $file) { filename size } }",
                  "operationName": "UploadFile",
                  "variables": { "file": null }
                 }`,
)
form.WriteField(
	"map",
	`{"file": ["variables.file"]}`,
)
w, _ := form.CreateFormFile("file", "hello.txt")
w.Write([]byte("Hello, World"))
form.Close()
req, _ := http.NewRequest("POST", "/graphql", buffer)
req.Header.Add("Content-Type", form.FormDataContentType())
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
body := rec.Body.Bytes()
fmt.Println(string(body))
Output:

{"data":{"uploadFile":{"filename":"hello.txt","size":12}}}

func New

func New(schema graphql.Schema, contextProviders ...ContextProviderFn) *GraphQLApp

Constructs a new GraphQL app

func (*GraphQLApp) Handler

func (app *GraphQLApp) Handler(contextProviders ...ContextProviderFn) gin.HandlerFunc

Factory function to create `gin.HandlerFunc` for the GraphQL application.

Each `contextProviders` will be called before running `graphql.Do` to generate/construct the context, and this context will be passed down to the resolver by `graphql.Do` function. Any context provider added before or with this function will be executed sequentially for each request.

type GraphQLRequest

type GraphQLRequest struct {
	GraphQLRequestParams
	OperationsString string `json:"-" form:"operations"`
	MapString        string `json:"-" form:"map"`
}

GraphQL request parameters including file upload maps and operations

type GraphQLRequestParams

type GraphQLRequestParams struct {
	RequestString  string                 `json:"query"         form:"query"`
	VariableValues map[string]interface{} `json:"variables"     form:"variables"`
	OperationName  string                 `json:"operationName" form:"operationName"`
}

Basic GraphQL request parameters

Jump to

Keyboard shortcuts

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