Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Subscription support #347

Merged
merged 40 commits into from
Aug 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
0fbd83c
first version supporting subscription
matthieu4294967296moineau Aug 24, 2023
39049b5
added basic tests
matthieu4294967296moineau Aug 25, 2023
9c73c20
corrected according to comments
matthieu4294967296moineau Aug 30, 2023
4bf2101
doc
matthieu4294967296moineau Aug 30, 2023
5463429
changed dataUpdated chan to respChan
matthieu4294967296moineau Aug 31, 2023
31d1f5a
using CloseConnection instead of doneChan
matthieu4294967296moineau Aug 31, 2023
7fa1ac6
Dont have to create wsClient separately from graphQL client
matthieu4294967296moineau Sep 1, 2023
2be16a4
added exampleWebSocket
matthieu4294967296moineau Sep 4, 2023
55f09a8
rename
matthieu4294967296moineau Sep 4, 2023
8091484
defer close
matthieu4294967296moineau Sep 4, 2023
f64a3da
Merge pull request #1 from Interstellar-Lab/mm/feature/subscription
matthieu4294967296moineau Sep 4, 2023
34c3cd5
change doc generation for operations using {{.Type}}
matthieu4294967296moineau Sep 12, 2023
2196d65
generate doc for subscriptions in go
matthieu4294967296moineau Sep 12, 2023
b1468fd
renamed var to err_; regenerated all tests
matthieu4294967296moineau Sep 12, 2023
3cd742c
private websocket constants
matthieu4294967296moineau Sep 12, 2023
b60f9e2
private websocket const
matthieu4294967296moineau Sep 13, 2023
2ab3664
refactored DialContext so that it wors with x/net/websocket
matthieu4294967296moineau Sep 14, 2023
2a85383
separate WebSocketClient interface
matthieu4294967296moineau Sep 14, 2023
d032eef
WebSocketClient.CloseWebSocket doc
matthieu4294967296moineau Sep 14, 2023
f8a085a
moved websocket 'example' to documentation; added changelog
matthieu4294967296moineau Sep 14, 2023
e0e1c31
Merge pull request #2 from Interstellar-Lab/mm/refactoring/comments
matthieu4294967296moineau Sep 14, 2023
049f7f1
removed package gorilla/websocket from lint file
matthieu4294967296moineau Oct 6, 2023
5b6f4bf
added link to doc
matthieu4294967296moineau Oct 6, 2023
f5d2e92
remove useless underscore
matthieu4294967296moineau Oct 6, 2023
7066139
bis
matthieu4294967296moineau Oct 6, 2023
28987e1
possible to define other websocket protocol via header
matthieu4294967296moineau Oct 6, 2023
5487b0b
removed buffer from channels
matthieu4294967296moineau Oct 6, 2023
cf8b2c6
checking context
matthieu4294967296moineau Oct 6, 2023
0781863
only 1 goroutine; multiple subscriptions per connection
matthieu4294967296moineau Oct 9, 2023
495be60
check ctx
matthieu4294967296moineau Oct 9, 2023
0255a35
Merge pull request #4 from Interstellar-Lab/mm/refactoring/comments2
matthieu4294967296moineau Oct 9, 2023
0d88cfb
CloseWebSocket no-op
matthieu4294967296moineau Oct 12, 2023
8a4bc0c
revert golangci rules
matthieu4294967296moineau Jul 18, 2024
fe08b7f
corrected according to comments
matthieu4294967296moineau Jul 19, 2024
cc76a22
fix closing
matthieu4294967296moineau Jul 19, 2024
42baecf
Merge pull request #5 from Interstellar-Lab/mm/refactoring/closeChannels
matthieu4294967296moineau Jul 19, 2024
d18e6b5
Merge remote-tracking branch 'origin/main' into HEAD
benjaminjkraft Aug 16, 2024
828a1f9
docs nits
benjaminjkraft Aug 16, 2024
b32d4d9
update snapshots after merge
benjaminjkraft Aug 16, 2024
65b9ba9
lint after merge
benjaminjkraft Aug 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ linters-settings:
- gopkg.in/yaml.v2
- github.com/alexflint/go-arg
- github.com/bmatcuk/doublestar/v4
- github.com/google/uuid

forbidigo:
forbid:
Expand Down
2 changes: 2 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ In addition to several new features and bugfixes, along with this release comes

### New features:

- genqlient now supports subscriptions; the websocket protocol is by default `graphql-transport-ws` but can be set to another value.
See the [documentation](FAQ.md) for how to `subscribe to an API 'subscription' endpoint`.
- The new `optional: generic` allows using a generic type to represent optionality. See the [documentation](genqlient.yaml) for details.
- For schemas with enum values that differ only in casing, it's now possible to disable smart-casing in genqlient.yaml; see the [documentation](genqlient.yaml) for `casing` for details.
- genqlient now supports .graphqls and .gql file extensions for schemas and queries.
Expand Down
2 changes: 1 addition & 1 deletion docs/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ You want the schema in GraphQL [Schema Definition Language (SDL)](https://graphq

## Step 2: Write your queries

Next, write your GraphQL query. This is often easiest to do in an interactive explorer like [GraphiQL](https://github.com/graphql/graphiql/tree/main/packages/graphiql#readme); the syntax is just standard [GraphQL syntax](https://graphql.org/learn/queries/) and supports both queries and mutations. Put it in `genqlient.graphql`:
Next, write your GraphQL query. This is often easiest to do in an interactive explorer like [GraphiQL](https://github.com/graphql/graphiql/tree/main/packages/graphiql#readme); the syntax is just standard [GraphQL syntax](https://graphql.org/learn/queries/) and supports queries, mutations and subscriptions. Put it in `genqlient.graphql`:
```graphql
query getUser($login: String!) {
user(login: $login) {
Expand Down
122 changes: 122 additions & 0 deletions docs/subscriptions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Using genqlient with GraphQL subscriptions

This document describes how to use genqlient to make GraphQL subscriptions. It assumes you already have the basic [client](./client_config.md) set up. Subscription support is fairly new; please report any bugs or missing features!

## Client setup

You will need to use a different client calling `graphql.NewClientUsingWebSocket`, passing as a parameter your own websocket client.

Here is how to configure your webSocket client to match the interfaces:

### Example using `github.com/gorilla/websocket`

```go
type MyDialer struct {
*websocket.Dialer
}

func (md *MyDialer) DialContext(ctx context.Context, urlStr string, requestHeader http.Header) (graphql.WSConn, error) {
conn, _, err := md.Dialer.DialContext(ctx, urlStr, requestHeader)
return graphql.WSConn(conn), err
}
```

### Example using `golang.org/x/net/websocket`

```go
type MyDialer struct {
dialer *net.Dialer
}

type MyConn struct {
conn *websocket.Conn
}

func (c MyConn) ReadMessage() (messageType int, p []byte, err error) {
if err := websocket.Message.Receive(c.conn, &p); err != nil {
return websocket.UnknownFrame, nil, err
}
return messageType, p, err
}

func (c MyConn) WriteMessage(_ int, data []byte) error {
err := websocket.Message.Send(c.conn, data)
return err
}

func (c MyConn) Close() error {
c.conn.Close()
return nil
}

func (md *MyDialer) DialContext(ctx context.Context, urlStr string, requestHeader http.Header) (graphql.WSConn, error) {
if md.dialer == nil {
return nil, fmt.Errorf("nil dialer")
}
config, err := websocket.NewConfig(urlStr, "http://localhost")
if err != nil {
fmt.Println("Error creating WebSocket config:", err)
return nil, err
}
config.Dialer = md.dialer
config.Protocol = append(config.Protocol, "graphql-transport-ws")

// Connect to the WebSocket server
conn, err := websocket.DialConfig(config)
if err != nil {
return nil, err
}
return graphql.WSConn(MyConn{conn: conn}), err
}
```

## Making subscriptions

Once your websocket client matches the interfaces, you can get your `graphql.WebSocketClient` and listen in
a loop for incoming messages and errors:

```go
graphqlClient := graphql.NewClientUsingWebSocket(
"ws://localhost:8080/query",
&MyDialer{Dialer: dialer},
headers,
)

errChan, err := graphqlClient.Start(ctx)
if err != nil {
return
}

dataChan, subscriptionID, err := count(ctx, graphqlClient)
if err != nil {
return
}

defer graphqlClient.Close()
for loop := true; loop; {
select {
case msg, more := <-dataChan:
if !more {
loop = false
break
}
if msg.Data != nil {
fmt.Println(msg.Data.Count)
}
if msg.Errors != nil {
fmt.Println("error:", msg.Errors)
loop = false
}
case err = <-errChan:
return
case <-time.After(time.Minute):
err = wsClient.Unsubscribe(subscriptionID)
loop = false
}
}
```

To change the websocket protocol from its default value `graphql-transport-ws`, add the following header before calling `graphql.NewClientUsingWebSocket()`:
```go
headers.Add("Sec-WebSocket-Protocol", "graphql-ws")
```
22 changes: 10 additions & 12 deletions example/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions generate/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,6 @@ func (g *generator) baseTypeForOperation(operation ast.Operation) (*ast.Definiti
case ast.Mutation:
return g.schema.Mutation, nil
case ast.Subscription:
if !g.Config.AllowBrokenFeatures {
return nil, errorf(nil, "genqlient does not yet support subscriptions")
}
return g.schema.Subscription, nil
default:
return nil, errorf(nil, "unexpected operation: %v", operation)
Expand Down
4 changes: 3 additions & 1 deletion generate/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,6 @@ func (g *generator) preprocessQueryDocument(doc *ast.QueryDocument) {
func (g *generator) validateOperation(op *ast.OperationDefinition) error {
_, err := g.baseTypeForOperation(op.Operation)
if err != nil {
// (e.g. operation has subscriptions, which we don't support)
return err
}

Expand Down Expand Up @@ -282,6 +281,9 @@ func (g *generator) addOperation(op *ast.OperationDefinition) error {
if commentLines != "" {
docComment = "// " + strings.ReplaceAll(commentLines, "\n", "\n// ")
}
if op.Operation == ast.Subscription {
docComment += "\n// To unsubscribe, use [graphql.WebSocketClient.Unsubscribe]"
}

// If the filename is a pseudo-filename filename.go:startline, just
// put the filename in the export; we don't figure out the line offset
Expand Down
49 changes: 42 additions & 7 deletions generate/operation.go.tmpl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// The query or mutation executed by {{.Name}}.
// The {{.Type}} executed by {{.Name}}.
const {{.Name}}_Operation = `{{$.Body}}`

{{.Doc}}
Expand All @@ -7,15 +7,15 @@ func {{.Name}}(
ctx_ {{ref .Config.ContextType}},
{{end}}
{{- if not .Config.ClientGetter -}}
client_ {{ref "github.com/Khan/genqlient/graphql.Client"}},
client_ {{if eq .Type "subscription"}}{{ref "github.com/Khan/genqlient/graphql.WebSocketClient"}}{{else}}{{ref "github.com/Khan/genqlient/graphql.Client"}}{{end}},
{{end}}
{{- if .Input -}}
{{- range .Input.Fields -}}
{{/* the GraphQL name here is the user-specified variable-name */ -}}
{{.GraphQLName}} {{.GoType.Reference}},
{{end -}}
{{end -}}
) (*{{.ResponseName}}, {{if .Config.Extensions -}}map[string]interface{},{{end}} error) {
) ({{if eq .Type "subscription"}}dataChan_ chan {{.Name}}WsResponse, subscriptionID_ string,{{else}}data_ *{{.ResponseName}}, {{if .Config.Extensions -}}ext_ map[string]interface{},{{end}}{{end}} err_ error) {
req_ := &graphql.Request{
OpName: "{{.Name}}",
Query: {{.Name}}_Operation,
Expand All @@ -27,7 +27,6 @@ func {{.Name}}(
},
{{end -}}
}
var err_ error
{{if .Config.ClientGetter -}}
var client_ graphql.Client

Expand All @@ -36,14 +35,50 @@ func {{.Name}}(
return nil, {{if .Config.Extensions -}}nil,{{end -}} err_
}
{{end}}
var data_ {{.ResponseName}}
resp_ := &graphql.Response{Data: &data_}
{{if eq .Type "subscription"}}
dataChan_ = make(chan {{.Name}}WsResponse)
subscriptionID_, err_ = client_.Subscribe(req_, dataChan_, {{.Name}}ForwardData)
{{else}}
data_ = &{{.ResponseName}}{}
resp_ := &graphql.Response{Data: data_}

err_ = client_.MakeRequest(
{{if ne .Config.ContextType "-"}}ctx_{{else}}nil{{end}},
req_,
resp_,
)
{{end}}

return &data_, {{if .Config.Extensions -}}resp_.Extensions,{{end -}} err_
return {{if eq .Type "subscription"}}dataChan_, subscriptionID_,{{else}}data_, {{if .Config.Extensions -}}resp_.Extensions,{{end -}}{{end}} err_
}

{{if eq .Type "subscription"}}
type {{.Name}}WsResponse struct {
Data *{{.ResponseName}} `json:"data"`
Extensions map[string]interface{} `json:"extensions,omitempty"`
Errors error `json:"errors"`
}

func {{.Name}}ForwardData(interfaceChan interface{}, jsonRawMsg json.RawMessage) error {
var gqlResp graphql.Response
var wsResp {{.Name}}WsResponse
err := json.Unmarshal(jsonRawMsg, &gqlResp)
if err != nil {
return err
}
if len(gqlResp.Errors) == 0 {
err = json.Unmarshal(jsonRawMsg, &wsResp)
if err != nil {
return err
}
} else {
wsResp.Errors = gqlResp.Errors
}
dataChan_, ok := interfaceChan.(chan {{.Name}}WsResponse)
if !ok {
return errors.New("failed to cast interface into 'chan {{.Name}}WsResponse'")
}
dataChan_ <- wsResp
return nil
}
{{end}}
1 change: 1 addition & 0 deletions generate/testdata/queries/SimpleSubscription.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
subscription SimpleSubscription { count }
4 changes: 4 additions & 0 deletions generate/testdata/queries/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ input IntComparisonExp {
_nin: [Int!]
}

type Subscription {
count: Int!
}

input InputWithDefaults {
field: String! = "input field omitted"
nullableField: String = "nullable input field omitted"
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading