wsgraphql

package module
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Jul 23, 2022 License: MIT Imports: 8 Imported by: 0

README

An implementation of apollo graphql websocket protocol for graphql-go.

Inspired by graphqlws

Key features:

  • Subscription support
  • Callbacks at every stage of communication process for easy customization
  • Supports both websockets and plain http queries (with exception of continuing subscriptions)
  • Mutable context allowing to keep global-scoped connection/authentication data and subscription-scoped state

Feel free to leave PR or any feedback.

Documentation

Overview

graphql over websocket transport using apollo websocket protocol

Usage

When subscription operation is present in query, it is called repeatedly until it would return an error or cancel context associated with this operation.

Context provided to operation is also an instance of mutcontext.MutableContext, which supports setting additional values on same instance and holds cancel() function within it.

Implementors of subscribable operation are expected to cast provided context to mutcontext.MutableContext and on first invocation initialize data persisted across calls to it (like, external connection, database cursor or anything like that) as well as cleanup function using mutcontext.MutableContext.SetCleanup(func()), which would be called once operation is complete.

After initialization, subscription would be called repeatedly, expected to return next value at each invocation, until an error is encountered, interruption is requested, or data is normally exhausted, in which case mutcontext.MutableContext is supposed to be issued with .Complete()

After any of that, cleanup function (if any provided) would be called, ensuring operation code could release any resources allocated

Non-subscribable operations are straight-forward stateless invocations.

Subscriptions are also not required to be stateful, however that would mean they would return values as fast as they could produce them, without any timeouts and delays implemented, which would likely to require some state, e.g. time of last invocation.

By default, implementation allows calling any operation once via non-websocket plain http request.

Example
value := 0
var changechan chan int

query := graphql.NewObject(graphql.ObjectConfig{
	Name: "TestQuery",
	Fields: graphql.Fields{
		"get": &graphql.Field{
			Type: graphql.NewNonNull(graphql.Int),
			Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
				return value, nil
			},
		},
	},
})

mutation := graphql.NewObject(graphql.ObjectConfig{
	Name: "TestMutation",
	Fields: graphql.Fields{
		"set": &graphql.Field{
			Type: graphql.Int,
			Args: graphql.FieldConfigArgument{
				"value": &graphql.ArgumentConfig{
					Type: graphql.NewNonNull(graphql.Int),
				},
			},
			Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
				value = p.Args["value"].(int)
				if changechan != nil {
					changechan <- value
				}
				return nil, nil
			},
		},
		"stop": &graphql.Field{
			Type: graphql.Int,
			Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
				if changechan != nil {
					close(changechan)
					changechan = nil
				}
				return nil, nil
			},
		},
	},
})

subscription := graphql.NewObject(graphql.ObjectConfig{
	Name: "TestSubscription",
	Fields: graphql.Fields{
		"watch": &graphql.Field{
			Type: graphql.NewNonNull(graphql.Int),
			Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
				ctx := p.Context.(mutcontext.MutableContext)
				c := ctx.Value("ch")
				if c == nil {
					newc := make(chan int)
					ctx.Set("ch", newc)
					ctx.SetCleanup(func() {
						c := ctx.Value("ch")
						if c != nil {
							close(c.(chan int))
						}
					})
					c = newc
					changechan = newc
				}
				ch := c.(chan int)
				v, ok := <-ch
				if !ok {
					ctx.Set("ch", nil)
					_ = ctx.Cancel()
				}
				return v, nil
			},
		},
		"countdown": &graphql.Field{
			Type: graphql.Int,
			Args: graphql.FieldConfigArgument{
				"value": &graphql.ArgumentConfig{
					Type:         graphql.Int,
					DefaultValue: 0,
				},
			},
			Resolve: func(p graphql.ResolveParams) (interface{}, error) {
				ctx := p.Context.(mutcontext.MutableContext)

				iter := ctx.Value("iter")
				if iter == nil {
					v, ok := p.Args["value"].(int)
					if !ok {
						return nil, nil
					}
					ctx.Set("iter", v)
					iter = v
				}

				i := iter.(int)
				if i <= 0 {
					ctx.Complete()
					return 0, nil
				}

				ctx.Set("iter", i-1)
				return i, nil
			},
		},
	},
})

schema, err := graphql.NewSchema(graphql.SchemaConfig{
	Query: graphql.NewObject(graphql.ObjectConfig{
		Name: "RootQuery",
		Fields: graphql.Fields{
			"test": &graphql.Field{
				Type: query,
				Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
					return query, nil
				},
			},
		},
	}),
	Mutation: graphql.NewObject(graphql.ObjectConfig{
		Name: "RootMutation",
		Fields: graphql.Fields{
			"test": &graphql.Field{
				Type: mutation,
				Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
					return mutation, nil
				},
			},
		},
	}),
	Subscription: graphql.NewObject(graphql.ObjectConfig{
		Name: "RootSubscription",
		Fields: graphql.Fields{
			"test": &graphql.Field{
				Type: subscription,
				Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
					return subscription, nil
				},
			},
		},
	}),
})

server, err := NewServer(Config{
	Schema: &schema,
})
if err != nil {
	panic(err)
}
http.Handle("/graphql", server)

err = http.ListenAndServe(":8080", nil)
if err != nil {
	fmt.Println(err)
}
Output:

Example (Authentication)
var schema graphql.Schema
server, err := NewServer(Config{
	Schema: &schema,
	OnPlainInit: func(globalctx mutcontext.MutableContext, r *http.Request, w http.ResponseWriter) {
		remote := r.RemoteAddr
		idx := strings.LastIndex(remote, ":")
		globalctx.Set("remote", remote[:idx])
	},
	OnConnect: func(globalctx mutcontext.MutableContext, parameters interface{}) error {
		mapparams := parameters.(map[string]interface{})
		v, ok := mapparams["token"].(string)
		if !ok {
			return errors.New("invalid token type")
		}
		remoteip := globalctx.Value("remote").(string)
		type User struct {
			Name string
		}
		authenticateUser := func(token, remote string) *User {
			if token == "123" {
				return &User{Name: "admin"}
			}
			return nil
		}

		user := authenticateUser(v, remoteip)
		globalctx.Set("user", user)
		return nil
	},
})
if err != nil {
	panic(err)
}
http.Handle("/graphql", server)

err = http.ListenAndServe(":8080", nil)
if err != nil {
	fmt.Println(err)
}
Output:

Index

Examples

Constants

View Source
const (
	KeyHttpRequest = common.KeyHttpRequest
)

Variables

View Source
var (
	ErrSchemaRequired   = common.ErrSchemaRequired
	ErrPlainHttpIgnored = common.ErrPlainHttpIgnored
)

Functions

func NewServer

func NewServer(config Config) (http.Handler, error)

Returns new server instance using supplied config (which could be zero-value)

Types

type Config

type Config struct {
	// websocket upgrader
	// default: one that simply negotiates 'graphql-ws' protocol
	Upgrader *websocket.Upgrader

	// graphql schema, required
	// default: nil
	Schema *graphql.Schema

	// called when new client is connecting with new parameters or new plain request started
	// default: nothing
	OnConnect FuncConnectCallback

	// called when new operation started
	// default: nothing
	OnOperation FuncOperationCallback

	// called when operation is complete
	// default: nothing
	OnOperationDone FuncOperationDoneCallback

	// called when websocket connection is closed or plain request is served
	// default: nothing
	OnDisconnect FuncDisconnectCallback

	// called when new http connection is established
	// default: nothing
	OnPlainInit FuncPlainInit

	// called when failure occured at plain http stages
	// default: writes back error text
	OnPlainFail FuncPlainFail

	// if true, plain http connections that can't be upgraded would be ignored and not served as one-off requests
	// default: false
	IgnorePlainHttp bool

	// keep alive period, at which server would send keep-alive messages
	// default: 20 seconds
	KeepAlive time.Duration
}

Config for websocket graphql server

type FuncConnectCallback

type FuncConnectCallback common.FuncConnectCallback

type FuncDisconnectCallback

type FuncDisconnectCallback common.FuncDisconnectCallback

type FuncOperationCallback

type FuncOperationCallback common.FuncOperationCallback

type FuncOperationDoneCallback

type FuncOperationDoneCallback common.FuncOperationDoneCallback

type FuncPlainFail

type FuncPlainFail common.FuncPlainFail

type FuncPlainInit

type FuncPlainInit common.FuncPlainInit

Directories

Path Synopsis
Mutable context, allows for easy setting additional values shared across all context and it's children
Mutable context, allows for easy setting additional values shared across all context and it's children
Implemenentation of GraphQL over WebSocket Protocol by apollographql https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md
Implemenentation of GraphQL over WebSocket Protocol by apollographql https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md
graphql websocket http handler implementation
graphql websocket http handler implementation

Jump to

Keyboard shortcuts

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