GithubHelp home page GithubHelp logo

functionalfoundry / graphqlws Goto Github PK

View Code? Open in Web Editor NEW
150.0 7.0 29.0 41 KB

Implementation of the GraphQL over WebSocket protocol in Go.

Home Page: https://godoc.org/github.com/functionalfoundry/graphqlws

License: MIT License

Go 100.00%
golang go graphql websockets server backend

graphqlws's Introduction

Note: We're looking for a new maintainer for graphqlws. Please reach out via [email protected] if you're interested.


graphqlws

Implementation of the GraphQL over WebSocket protocol in Go. Brought to you by Functional Foundry.

API Documentation

Build Status Go Report

Getting started

  1. Install dependencies:
    go get github.com/sirupsen/logrus
    go get github.com/x-cray/logrus-prefixed-formatter
    go get github.com/google/uuid
    go get github.com/gorilla/websocket
    go get github.com/graphql-go/graphql
  2. Clone the repository:
    mkdir -p "$GOPATH/src/github.com/functionalfoundry"
    cd "$GOPATH/src/github.com/functionalfoundry"
    git clone https://github.com/functionalfoundry/graphqlws
  3. Run the tests:
    cd graphqlws
    go test
  4. Run the example server:
    go run graphqlws/examples/server

Usage

Setup

package main

import (
	"net/http"

	"github.com/functionalfoundry/graphqlws"
	"github.com/graphql-go/graphql"
)

func main() {
	// Create a GraphQL schema
	schema, err := graphql.NewSchema(...)

	// Create a subscription manager
	subscriptionManager := graphqlws.NewSubscriptionManager(&schema)

	// Create a WebSocket/HTTP handler
	graphqlwsHandler := graphqlws.NewHandler(graphqlws.HandlerConfig{
		// Wire up the GraphqL WebSocket handler with the subscription manager
		SubscriptionManager: subscriptionManager,

		// Optional: Add a hook to resolve auth tokens into users that are
		// then stored on the GraphQL WS connections
		Authenticate: func(authToken string) (interface{}, error) {
			// This is just a dumb example
			return "Joe", nil
		},
	})

	// The handler integrates seamlessly with existing HTTP servers
	http.Handle("/subscriptions", graphqlwsHandler)
	http.ListenAndServe(":8080", nil)
}

Working with subscriptions

// This assumes you have access to the above subscription manager
subscriptions := subscriptionManager.Subscriptions()

for conn, _ := range subscriptions {
	// Things you have access to here:
	conn.ID()   // The connection ID
	conn.User() // The user returned from the Authenticate function

	for _, subscription := range subscriptions[conn] {
		// Things you have access to here:
		subscription.ID            // The subscription ID (unique per conn)
		subscription.OperationName // The name of the operation
		subscription.Query         // The subscription query/queries string
		subscription.Variables     // The subscription variables
		subscription.Document      // The GraphQL AST for the subscription
		subscription.Fields        // The names of top-level queries
		subscription.Connection    // The GraphQL WS connection

		// Prepare an execution context for running the query
		ctx := context.Context()

		// Re-execute the subscription query
		params := graphql.Params{
			Schema:         schema, // The GraphQL schema
			RequestString:  subscription.Query,
			VariableValues: subscription.Variables,
			OperationName:  subscription.OperationName,
			Context:        ctx,
		}
		result := graphql.Do(params)

		// Send query results back to the subscriber at any point
		data := graphqlws.DataMessagePayload{
			// Data can be anything (interface{})
			Data:   result.Data,
			// Errors is optional ([]error)
			Errors: graphqlws.ErrorsFromGraphQLErrors(result.Errors),
		}
		subscription.SendData(&data)
	}
}

Logging

graphqlws uses logrus for logging. In the future we might remove those logs entirely to leave logging entirely to developers using graphqlws. Given the current solution, you can control the logging level of graphqlws by setting it through logrus:

import (
  log "github.com/sirupsen/logrus"
)

...

log.SetLevel(log.WarnLevel)

License

Copyright © 2017-2019 Functional Foundry, LLC.

Licensed under the MIT License.

graphqlws's People

Contributors

gabrielruiu avatar hitochan777 avatar jannis avatar matiasanaya-ffx avatar zerim avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

graphqlws's Issues

Subscription should guarantee at least one gqlData response to a gqlStart request

The Problem

According to what I've read, it seems that if a client sends a "gqlStart" to the server then the server should guarantee at least one "gqlData" response. Currently this doesn't happen, as "gqlData" responses are only sent when the server logic explicitly calls "Subscription.SendData()", which may never happen if the subscribed content isn't being updated.

I have two proposals for fixing this issue:

Solution 1: execute the query within this library within the "AddSubscription" function.

This would leave the exported functions of the library unchanged, but denies the server logic the ability to customize behaviour such as adding objects to the graphql query context.

Solution 2: allow or require the server logic to provide OnConnect callback functions

If the server logic could provide callback functions to the SubscriptionManager then the solution is entirely in the hands of the developer. As one possible way this could be approached, this would be near-identical to the existing implementation, except that instead of being created with a single argument, it would now also need a callback argument.

Before:

NewSubscriptionManager(schema *graphql.Schema) SubscriptionManager

After:

NewSubscriptionManager(schema *graphql.Schema, onConnectCallback func(s *Subscription)) SubscriptionManager

It would even be possible to make this change to the existing implementation of SubscriptionManager directly without breaking any existing code by accepting a variadic number of callbacks, as the callback definitions would then be completely optional.

Alternative:

NewSubscriptionManager(schema *graphql.Schema, onConnectCallbacks ...func(s *Subscription)) SubscriptionManager

Rare bug caused by concurrent map read and write

From 'handler.go':

line 63
delete(connections, conn)

line 94
connections[conn] = true

All of this logic occurs within a 'http.Handler', and is therefore multi-threaded. There are currently no safeguards against concurrent map read and map write. Perhaps this map should be replaced with a 'sync.Map' from the standard library?

Websock connection closing abnormally when running inside docker container

I am trying to run the graphqlws inside a docker container. The code is straight forward and if I test locally it works fine. My UI App is able to establish a Websocket connection and receive messages coming from Server.

But when I run inside docker, the path to reach my server is as follows.
Browser -> UI POD (https) -> nginx -> proxy to graphql/Server pod

I have already added the following headers in my Nginx to allow proper traffic

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "keep-alive, upgrade";
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
proxy_pass http://graphql;

But still, the following error is thrown immediately

time="2022-08-10T10:40:05Z" level=info msg="Created connection" prefix="graphqlws/connection/d2751293-304b-448c-8e44-0c97f108feec"                                                  ││ time="2022-08-10T10:40:49Z" level=warning msg="Closing connection" prefix="graphqlws/connection/d2751293-304b-448c-8e44-0c97f108feec" reason="websocket: close 1006 (abnormal closu ││ time="2022-08-10T10:41:19Z" level=info msg="Remove subscriptions" conn=d2751293-304b-448c-8e44-0c97f108feec prefix="graphqlws/subscriptions"                                        ││ time="2022-08-10T10:41:21Z" level=info msg="Closed connection" prefix="graphqlws/connection/d2751293-304b-448c-8e44-0c97f108feec"           
  • UI is accessing this with wss:// and nginx routes to http:// as its pod to pod traffic
  • I tried to Run the Server Application locally and able to access the endpoint on ws:// locally, it works perfectly fine.
  • I tried to port-forward the server pod and use connection with ws:// it did not work either. Same error as above is thrown.

Below is the handler implementation

func (g *Handle) NewSubscriptionHandler(kubeconfig, apiHost string, loginHandler login.Handler) http.Handler {
	return graphqlws.NewHandler(graphqlws.HandlerConfig{
		SubscriptionManager: subscription.Manager(),
		Authenticate: func(token string) (interface{}, error) {
                            // Auth code, removed for obvious purpose
		},
	})
}

Could you please advise how to handle or fix this issue ?

subscription.SendData does not include errors when sending JSON over websocket

So I have configured a subscription manager, and it is successfully creating subscriptions, but I wanted to validate certain variables that are sent over with the Subscription when it is created. If they aren't valid, I execute the following code:

data := graphqlws.DataMessagePayload{
	Errors: []error{errors.New("subscription failed: invalid.")},
}
sub.SendData(&data)

This is all well and fine, except that the message that is getting written out to the websocket looks like this:

{"id":"dave","type":"data","payload":{"data":null,"errors":[{}]}}

I certainly expected the data key in the payload to be null or empty, but I expect the errors to be written out to the errors key in the payload JSON instead of being [{}]. Am I doing something wrong here?

Thanks

Fix crash when trying to send data over an already closed connection

Stacktrace from a test project:

[0993] DEBUG graphqlws/handler: Closing connection conn=8fab3541-9ad2-4aca-aa88-601b8d60ebb5 user=<nil>
[0993]  INFO graphqlws/subscriptions: Remove subscriptions conn=8fab3541-9ad2-4aca-aa88-601b8d60ebb5
[0993]  INFO graphqlws/subscriptions: Remove subscription conn=8fab3541-9ad2-4aca-aa88-601b8d60ebb5 subscription=1
[0993]  INFO graphqlws/subscriptions: Remove subscription conn=8fab3541-9ad2-4aca-aa88-601b8d60ebb5 subscription=2
[0993]  INFO graphqlws/subscriptions: Remove subscription conn=8fab3541-9ad2-4aca-aa88-601b8d60ebb5 subscription=8
[0993]  INFO graphqlws/subscriptions: Remove subscription conn=8fab3541-9ad2-4aca-aa88-601b8d60ebb5 subscription=4
[0993]  INFO graphqlws/subscriptions: Remove subscription conn=8fab3541-9ad2-4aca-aa88-601b8d60ebb5 subscription=10
panic: send on closed channel

goroutine 13 [running]:
github.com/functionalfoundry/graphqlws.(*connection).SendData(0xc4203095c0, 0xc420357fc8, 0x1, 0xc420b8b7a0)
	$GOPATH/go/src/github.com/functionalfoundry/graphqlws/connections.go:170 +0x134
github.com/functionalfoundry/graphqlws.NewHandler.func2.2.1(0xc4203c44d0, 0xc420b8b7a0)
	$GOPATH/go/src/github.com/functionalfoundry/graphqlws/handler.go:83 +0x4b
github.com/***/***-graphql-server/graphqlws.(*subscriptionManager).PushChange(0xc4202a0e00, 0x15609b2, 0xa, 0xc420960cc0, 0x14, 0x153e400, 0xc4204e6ff0)
	$GOPATH/go/src/github.com/***/***-graphql-server/graphqlws/subscriptions.go:123 +0xd5
main.main.func2(0x17d7360, 0xc420162680, 0x15609b2, 0xa, 0xc420960cc0, 0x14, 0x153e400, 0xc4204e6ff0)
	$GOPATH/go/src/github.com/***/***-graphql-server/main.go:104 +0x6e
github.com/***/***-graphql-server/buntdb.(*store).Set(0xc420162680, 0x15609b2, 0xa, 0xc420960cc0, 0x14, 0x153e400, 0xc4204e6ff0)
	$GOPATH/go/src/github.com/***/***-graphql-server/buntdb/store.go:107 +0x11e
github.com/***/***-graphql-server/dbchanges.NewDatabaseChangeHandler.func1(0x15609b2, 0xa, 0x153e400, 0xc4204e6ff0)
	$GOPATH/go/src/github.com/***/***-graphql-server/dbchanges/dbchanges.go:19 +0x1e5
github.com/***/***-graphql-server/mockdb.(*databaseListener).createMockData(0xc42014f570)
	$GOPATH/go/src/github.com/***/***-graphql-server/mockdb/dblistener.go:438 +0x7bb
created by github.com/***/***-graphql-server/mockdb.(*databaseListener).SetChangeHandler
	$GOPATH/go/src/github.com/***/***-graphql-server/mockdb/dblistener.go:82 +0x149
exit status 2

Issues installing graphqlws with dep due to logrus dependency

(44)      try github.com/functionalfoundry/graphqlws@master
(45)  ✗   case-only variation in dependency on "github.com/sirupsen/logrus"; "github.com/Sirupsen/logrus" already established by:
(45)    (root)
(45)    github.com/Lobaro/go-util@master
(45)    github.com/Lobaro/webapp@master
Solving failure: No versions of github.com/functionalfoundry/graphqlws met constraints:
        master: Could not introduce github.com/functionalfoundry/graphqlws@master due to a case-only variation: it depends on "github.com/sirupsen/logrus", but "github.com/Sirupsen/logrus" was already established as the case variant for that project root by the
 following other dependers:
        (root)
        github.com/Lobaro/go-util@master
        github.com/Lobaro/webapp@master

None of the project has a Gopkg.toml - nay ideas how to solve this?

Add authentication check on subsequent web socket messages

Currently we only run the UserFromAuthToken function when the WS connection is initialized. We should run it on subsequent messages as well in case the auth token expires, etc.

Can leave it up to the user to decide if they want to memoize etc. to keep that function call inexpensive.

Needs a working pubsub example

The title is self-explanatory. I am still unsure how to integrate this with and pubsub mechanism, such as DB pubsubs and RabbitMQ.

Allow to control logger

I can not change the logger in NewConnection, NewHandler, . It always spams my log and I just need a way to change the log level and format.

For the SubscriptionManager this is already possible in NewSubscriptionManagerWithLogger

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.