graphqlws

package module
v0.2.4 Latest Latest
Warning

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

Go to latest
Published: Oct 1, 2019 License: MIT Imports: 15 Imported by: 0

README

go-graphqlws CircleCI

THIS LIBRARY IS A WORK IN PROGRESS

THIS DOCUMENTATION IS A DRAFT

go-graphqlws extends the graphql-go/graphql by implementing the graphqlws protocol on top of the gorilla websocket server implementation which uses the default net/http library.

❗ In order to get it done, we had to fork graphql-go/graphql and make a couple changes that were not merged (yet, I hope) back in the main repository. The idea is once we get it done, this library will starting using graphql-go/graphql.

This implementation use a lot of the ideas contained in functionalfoundry/graphqlws. But, it is architecturally different. But, some of the code was heavily based on that.

Before you start

Handshake

The endpoint, let's say '/subscriptions' will trigger a graphqlws.Handler that will upgrade the connection and call the connection handler method:

func (conn *graphql.Conn, err error)

This method will inform if any error happened. If not, a connection will be provided. At this point, when the connection is already established, you should add all handlers responsible for dealing with all events happens with the client.

Check the 1st example on the next session (Handlers).

Handlers

Handlers are interfaces that can be implemented and added to the graphqlws.Conn in order to add new behavior to it. On the following example it will adding a message for everytime a connection is closed.

// ...
type connHandler struct {}

func (handler *connHandler) HandleWebsocketClose(code int, text string) error {
	fmt.Println("the connection was closed!")
	return nil
}
// ...
mux.Handle("/subscriptions", graphqlws.NewHttpHandler(
	graphqlws.NewHandlerConfigFactory().Upgrader(graphqlws.NewUpgrader(&websocket.Upgrader{})).Schema(&schema).Build(),
	graphqlws.NewConfigFactory().Build(),
	func(conn *graphqlws.Conn, err error) { // Handshake referred on the previous session
		if err != nil {
			rlog.Error(err)
			return
		}
		conn.AddHandler(&connHandler{conn: conn})
	},
))
// ...

One type struct can implement one or many structs, it totally depends on what is your need and how are you going to arrange them to get your desired behaviour.

type SystemRecoverHandler interface {
	HandlePanic(t RWType, r interface{}) error
}

type ConnectionInitHandler interface {
	HandleConnectionInit(*GQLConnectionInit) error
}

type ConnectionStartHandler interface {
	HandleConnectionStart(*GQLStart) []error
}

type ConnectionStopHandler interface {
	HandleConnectionStop(*GQLStop) error
}

type ConnectionTerminateHandler interface {
	HandleConnectionTerminate(*GQLConnectionTerminate) error
}

type SubscriptionStartHandler interface {
	HandleSubscriptionStart(subscription *Subscription) error
}

type SubscriptionStopHandler interface {
	HandleSubscriptionStop(subscription *Subscription) error
}

type WebsocketPongHandler interface {
	HandleWebsocketPong(message string) error
}

type WebsocketPingHandler interface {
	HandleWebsocketPing() error
}

type WebsocketCloseHandler interface {
	HandleWebsocketClose(code int, text string) error
}

A useful example is at the connection_init step (simple-chat-server-redis example), where the payload is analyzed to check if the user has permission to connect:

func (handler *connectionHandler) HandleConnectionInit(init *graphqlws.GQLConnectionInit) error {
	var at AuthToken
	err := json.Unmarshal(init.Payload, &at)
	if err != nil {
		return err
	}
	if at.AuthToken == "" {
		return errors.New("the user name should be provided")
	}
	handler.user.Name = at.AuthToken

	users[handler.user.Name] = &handler.user

	broadcastJoin(&handler.user)
	return nil
}

This example is simple but introduces the idea. A better approach would use JWT, for example.

Handlers are powerful enough that our Redis implementation uses them to add subscription capabilities.

For more information about what each handler does, please refer to the Godoc.

Getting Started

Even a small example turned out to be too much code to be entered here. So, instead, we recommend you to follow our simple-chat-server-redis example. It is well documented and follow these steps:

  1. Initialize your preferred PubSub technology;

    Today, we only support Redis, but it can easily be extended. PRs are welcome.

  2. Graphql Schema definition;

    Inside of the graphql schema definition you should have:

  3. Subscription definition;

    You should define the Subscribe field of the graphql.

  4. Define a GraphQL handler;

  5. Define a GraphQL WS handler;

  6. Start the http server.

How Subscribe and Resolve are called?

The Subscribe method is called anytime a new subscription is started. This library will call Subscribe method of all subscription fields described in the query sent by the client.

When a field subscribes subscribes to a channel, it will be called anytime that subscription returns any data.

But, as we know, graphqlws subscriptions might subscribe themselves to multiple channels at once. So, we use the channel identification to distinguish between calls.

In the snipped below, extracted from the simple-chat-server-redis example, you will see that the data, received from Redis, is extracted from the p.ResolveParams.Context. If it cannot be found, the Resolve execution is cancelled by returning nil, nil.

// Rest of the graphql schema definition...
Subscription: graphql.NewObject(graphql.ObjectConfig{
	Name: "SubscriptionRoot",
	Fields: graphql.Fields{
		"onJoin": &graphql.Field{
			Type: userType,
			Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
				userRaw, ok := p.Context.Value("onJoin").([]byte)
				if !ok {
					return nil, nil
				}
				// Here onJoin is confirmed.
				// ...
			},
			Subscribe: func(params graphql.SubscribeParams) error {
				// Subscribe the user in the `onJoin` topic.
				return params.Subscriber.SubscriberSubscribe(graphqlws.StringTopic("onJoin"))
			},
		},
		"onLeft": &graphql.Field{
			Type: userType,
			Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
				userRaw, ok := p.Context.Value("onLeft").([]byte)
				if !ok {
					return nil, nil
				}
				// Here onLeft is confirmed.
				// ...
			},
			Subscribe: func(params graphql.SubscribeParams) error {
				// Subscribe the user in the `onLeft` topic.
				return params.Subscriber.SubscriberSubscribe(graphqlws.StringTopic("onLeft"))
			},
		},
		"onMessage": &graphql.Field{
			Type: messageType,
			Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
				messageRaw, ok := p.Context.Value("onMessage").([]byte)
				if !ok {
					return nil, nil
				}
				// Here onMessage is confirmed.
				// ...
			},
			Subscribe: func(params graphql.SubscribeParams) error {
				// Subscribe the user in the `onMessage` topic.
				return params.Subscriber.SubscriberSubscribe(graphqlws.StringTopic("onMessage"))
			},
		},
	},
}),
// Rest of the graphql schema definition...

License

MIT

Documentation

Index

Constants

View Source
const (
	// KB represents a Kilobyte size.
	KB = int64(1024)

	// MB represents a Megabyte size.
	MB = int64(1024) * KB

	// GB represents a Gigabyte.
	GB = int64(1024) * MB
)

Variables

View Source
var (
	ErrConnectionClosed                = errors.New("connection already closed")
	ErrUpgraderRequired                = errors.New("upgrader required")
	ErrSchemaRequired                  = errors.New("schema required")
	ErrClientDoesNotImplementGraphqlWS = errors.New("client does not implement the `graphql-ws` subprotocol")
	ErrSubscriptionNotFound            = errors.New("subscription not found")

	// ErrReinitializationForbidden is triggered when a `gqlConnectionInit` is
	// received twice.
	ErrReinitializationForbidden = errors.New("reinitalization forbidden")

	// ErrConnectionNotFullyEstablished is triggered when a `gqlStart` is received
	// without finishing a `gqlConnectionInit`.
	ErrConnectionNotFullyEstablished = errors.New("connection not established")

	// ErrInvalidTopic is triggered when a `Subscriber.SubscriberSubscribe`
	// receives an invalid `Topic`.
	ErrInvalidTopic = errors.New("invalid topic")

	// ErrInvalidSubscriber is triggered when a `graphql.SubscribeParams.Subscriber`
	// is not a instance that implements the `graphqlws.Subscriber` interface.
	ErrInvalidSubscriber = errors.New("invalid subscriber")
)
View Source
var (
	ErrSubscriptionIDEmpty         = errors.New("subscription ID is empty")
	ErrSubscriptionHasNoConnection = errors.New("subscription is not associated with a connection")
	ErrSubscriptionHasNoQuery      = errors.New("subscription query is empty")
)
View Source
var (
	TraceLevelInternalGQLMessages = 10000
	TraceLevelConnectionEvents    = 10001
)
View Source
var (
	// ConnectionCount is the counter of new connections, also used to identify
	// each individual connection in the log.
	ConnectionCount uint64
)
View Source
var DefaultConfig = Config{
	ReadLimit:    &defaultReadLimit,
	PongWait:     &defaultPongWait,
	WriteTimeout: &defaultWriteTimeout,
}

DefaultConfig holds de default param initialization values for NewConfigFactory.

Functions

func ErrorsFromGraphQLErrors

func ErrorsFromGraphQLErrors(errors []gqlerrors.FormattedError) []error

ErrorsFromGraphQLErrors convert from GraphQL errors to regular errors.

func NewConfigFactory

func NewConfigFactory() *configFactory

NewConfigFactory creates Config instances using sugar syntax.

Example:

config := NewConfigFactory().ReadLimit(2048).PongWait(time.Second*60).Build()

You can call Build how many times you wish, it will always return a new instance with the same configuration set.

func NewHandlerConfigFactory

func NewHandlerConfigFactory() *handlerConfigFactory

NewHandlerConfigFactory creates HandlerConfig instances using sugar syntax.

Example:

config := NewHandlerConfigFactory().Schema(&schemas).Upgrader(upgrader).Build()

You can call Build how many times you wish, it will always return a new instance with the same configuration set.

func NewHttpHandler

func NewHttpHandler(handlerConfig HandlerConfig, connectionConfig Config, handler func(*Conn, error)) http.Handler

NewHttpHandler returns a `http.Handler` ready for being used.

`handler`: Is triggered when a connection is established. There, you should add handlers to the conn and keep track when it is active.

IMPORTANT: If `conn` is not finished. It will stay on forever.

func NewSubscriber

func NewSubscriber() *subscriber

NewSubscriber creates a default implementation of a subscriber.

func SubscriptionFieldNamesFromDocument

func SubscriptionFieldNamesFromDocument(doc *ast.Document) []string

func ValidateSubscription

func ValidateSubscription(s *Subscription) []error

Types

type Config

type Config struct {
	// ReadLimit is the maximum size of the buffer used to receive raw messages
	// from the websocket.
	ReadLimit *int64

	// PongWait is how much time we will wait without sending any message to the
	// client before sending a PONG.
	PongWait *time.Duration

	// WriteTimeout is how much time is wait for sending a message to the
	// websocket before triggering a timeout error.
	WriteTimeout *time.Duration
}

Config holds the configuration for a graphqlws connection.

See Also the NewConfigFactory for easily create Config structs.

type Conn

type Conn struct {
	Logger        rlog.Logger
	Schema        *graphql.Schema
	Subscriptions sync.Map

	Handlers []Handler
	// contains filtered or unexported fields
}

Conn is a connection with a client.

func NewConn

func NewConn(conn *websocket.Conn, schema *graphql.Schema, config *Config) (*Conn, error)

NewConn initializes a `Conn` instance.

func (*Conn) AddHandler

func (c *Conn) AddHandler(handler Handler)

AddHandler adds a `Handler` to the connection.

See also `Handler`

func (*Conn) Close

func (c *Conn) Close() error

Close finishes the connection.

func (*Conn) RemoveHandler

func (c *Conn) RemoveHandler(handler Handler)

RemoveHandler removes a `Handler` from the connection.

See also `Handler`

func (*Conn) SendData

func (c *Conn) SendData(message *OperationMessage)

SendData enqueues a message to be sent by the writePump.

func (*Conn) SendError

func (c *Conn) SendError(err error) error

SendError sends an error to the client.

type ConnState

type ConnState int

ConnState is the state of the connection.

type ConnectionInitHandler

type ConnectionInitHandler interface {
	Handler
	HandleConnectionInit(*GQLConnectionInit) error
}

ConnectionInitHandler describes the handler that will be called when a GQL_CONNECTION_INIT is happens.

More information abuot GQL_CONNECTION_INIT at https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md#gql_connection_init

type ConnectionStartHandler

type ConnectionStartHandler interface {
	Handler
	HandleConnectionStart(*GQLStart) []error
}

ConnectionStartHandler describes the handler that will be called when a GQL_START is happens.

More information about GQL_START at https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md#gql_start

type ConnectionStopHandler

type ConnectionStopHandler interface {
	Handler
	HandleConnectionStop(*GQLStop) error
}

ConnectionStopHandler describes the handler that will be called when a GQL_STOP is happens.

More information abuot GQL_STOP at https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md#gql_stop

type ConnectionTerminateHandler

type ConnectionTerminateHandler interface {
	Handler
	HandleConnectionTerminate(*GQLConnectionTerminate) error
}

ConnectionTerminateHandler describes the handler that will be called when a GQL_CONNECTION_TERMINATE is happens.

More information abuot GQL_CONNECTION_TERMINATE at https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md#gql_connection_terminate

type GQLConnectionError

type GQLConnectionError struct {
	Payload interface{} `json:"payload"`
}

type GQLConnectionInit

type GQLConnectionInit struct {
	Payload json.RawMessage `json:"payload"`
}

GQLConnectionInit is sent from the client after the websocket connection is started.

The server will response only with GQL_CONNECTION_ACK + GQL_CONNECTION_KEEP_ALIVE (if used) or GQL_CONNECTION_ERROR to this message.

See Also [graphql-ws GQL_CONNECTION_INIT PROTOCOL](https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md#gql_connection_init)

type GQLConnectionTerminate

type GQLConnectionTerminate struct{}

GQLConnectionTerminate is sent from the client to temrinate the connection and all its operations.

See Also [graphql-ws GQL_CONNECTION_TERMINATE PROTOCOL](https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md#gql_connection_terminate)

type GQLData

type GQLData struct {
	ID      string        `json:"id"`
	Payload GQLDataObject `json:"payload"`
}

type GQLDataObject

type GQLDataObject struct {
	Data   interface{} `json:"data"`
	Errors []error     `json:"errors,omitempty"`
}

type GQLObject

type GQLObject struct {
	Query         string                 `json:"query"`
	Variables     map[string]interface{} `json:"variables,omitempty"`
	OperationName string                 `json:"operationName,omitempty"`
}

GQLObject represents the payload af a GQLStart command.

type GQLStart

type GQLStart struct {
	ID      string    `json:"id"`
	Payload GQLObject `json:"payload"`
}

GQLStart is sent from the client to be execute as a GraphQL command.

See Also [graphql-ws GQL_START PROTOCOL](https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md#gql_start)

type GQLStop

type GQLStop struct {
	ID string `json:"id"`
}

GQLStop is sent from the client to stop a running GraphQL operation execution.

See Also [graphql-ws GQL_START PROTOCOL](https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md#gql_stop)

type GQLType

type GQLType string

type Handler

type Handler interface {
}

Handler is an abstraction of a callback for a specific action in the system.

type HandlerConfig

type HandlerConfig struct {
	Upgrader WebSocketUpgrader
	Schema   *graphql.Schema
}

HandlerConfig holds the configuration for the http Handler.

type HandlerError

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

func (*HandlerError) Error

func (err *HandlerError) Error() string

func (*HandlerError) PreventDefault

func (err *HandlerError) PreventDefault() *HandlerError

PreventDefault set a flag for not executing the default implementation of an event.

func (*HandlerError) StopPropagation

func (err *HandlerError) StopPropagation() *HandlerError

StopPropagation set a flag for not executing the subsequent handlers of an event.

type OperationMessage

type OperationMessage struct {
	ID      string          `json:"id,omitempty"`
	Type    GQLType         `json:"type,omitempty"`
	Payload json.RawMessage `json:"payload"`
}

OperationMessage represents all messages sent the customer to the server.

type RWType

type RWType int
const (
	Read RWType = iota
	Write
)

type StringTopic

type StringTopic string

StringTopic is a simple implementaiton of `Topic` for those PubSub systems that use simple strings as topics.

func (StringTopic) ID

func (topic StringTopic) ID() interface{}

type Subscriber

type Subscriber interface {
	// Topics returns the array of topics subscribed.
	// It is designed for accumulating subscriptions before applying it to a
	// connection.
	Topics() []Topic

	// Subscribe does a subcription, or accumulate it (depends on the
	// implementation).
	Subscribe(topic Topic) error
}

SubscriptionSubscriber does subscriptions in behalf a single Subscription

type Subscription

type Subscription struct {
	ID            string
	Query         string
	Variables     map[string]interface{}
	OperationName string
	Document      *ast.Document
	Fields        []string
	Schema        *graphql.Schema
	Connection    *Conn
	Logger        rlog.Logger
}

Subscription holds all information about a GraphQL subscription made by a client, including a function to send data back to the client when there are updates to the subscription query result.

From https://github.com/functionalfoundry/graphqlws

func (*Subscription) MatchesField

func (s *Subscription) MatchesField(field string) bool

MatchesField returns true if the subscription is for data that belongs to the given field.

func (*Subscription) SendData

func (s *Subscription) SendData(data *GQLDataObject) error

type SubscriptionStartHandler

type SubscriptionStartHandler interface {
	Handler
	HandleSubscriptionStart(subscription *Subscription) error
}

SubscriptionStartHandler describes the handler that will be called when a subscription starts.

type SubscriptionStopHandler

type SubscriptionStopHandler interface {
	Handler
	HandleSubscriptionStop(subscription *Subscription) error
}

SubscriptionStopHandler describes the handler that will be called when a subscription stops.

type SystemRecoverHandler

type SystemRecoverHandler interface {
	Handler
	HandlePanic(t RWType, r interface{}) error
}

SystemRecoverHandler describes the handler that will be called when any panic happens while interacting with the client.

type Topic

type Topic interface {
	// ID will return the structure ID of the topic on the technology used for
	// that purpose. For example, using a Redis PubSub system, this method would
	// return a string containing identifier of the channel.
	ID() interface{}
}

Topic represents a custom interface that represents a topic that will be used along with a PubSub system.

type WebSocketUpgrader

type WebSocketUpgrader interface {
	AddSubprotocol(protocol string)
	Upgrade(w http.ResponseWriter, r *http.Request) (*websocket.Conn, error)
}

WebSocketUpgrader is an interface that serve as a proxy to the original `gorilla.Upgrader`. It is needed to enable developers to customize their process of upgrade a HTTP connection to a WebSocket connection.

Also, it is a good way to customize the `responseHeader`.

func NewUpgrader

func NewUpgrader(upgrader *websocket.Upgrader) WebSocketUpgrader

NewUpgrader implements a `WeSocketUpgrader` interface.

type WebsocketCloseHandler

type WebsocketCloseHandler interface {
	Handler
	HandleWebsocketClose(code int, text string) error
}

WebsocketCloseHandler describes the handler that will be called when the gorilla websocket close handler is called.

type WebsocketPingHandler

type WebsocketPingHandler interface {
	Handler
	HandleWebsocketPing() error
}

WebsocketPongHandler describes the handler that will be called when the gorilla websocket pong handler is called.

type WebsocketPongHandler

type WebsocketPongHandler interface {
	Handler
	HandleWebsocketPong(message string) error
}

WebsocketPongHandler describes the handler that will be called when the gorilla websocket pong handler is called.

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

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