github.com/mstephano/gqlgen-schemagen@v0.0.0-20230113041936-dd2cd4ea46aa/docs/content/recipes/subscriptions.md (about)

     1  ---
     2  title: "Subscriptions"
     3  description: Subscriptions allow for streaming real-time events to your clients. This is how to do that with gqlgen.
     4  linkTitle: "Subscriptions"
     5  menu: { main: { parent: "recipes" } }
     6  ---
     7  
     8  GraphQL Subscriptions allow you to stream events to your clients in real-time.
     9  This is easy to do in gqlgen and this recipe will show you how to setup a quick example.
    10  
    11  ## Preparation
    12  
    13  This recipe starts with the empty project after the quick start steps were followed.
    14  Although the steps are the same in an existing, more complex projects you will need
    15  to be careful to configure routing correctly.
    16  
    17  In this recipe you will learn how to
    18  
    19  1. add WebSocket Transport to your server
    20  2. add the `Subscription` type to your schema
    21  3. implement a real-time resolver.
    22  
    23  ## Adding WebSocket Transport
    24  
    25  To send real-time data to clients, your GraphQL server needs to have an open connection
    26  with the client. This is done using WebSockets.
    27  
    28  To add the WebSocket transport change your `main.go` by calling `AddTransport(&transport.Websocket{})`
    29  on your query handler.
    30  
    31  **If you are using an external router, remember to send _ALL_ `/query`-requests to your handler!**
    32  **Not just POST requests!**
    33  
    34  ```go
    35  package main
    36  
    37  import (
    38  	"log"
    39  	"net/http"
    40  	"os"
    41  
    42  	"github.com/mstephano/gqlgen-schemagen/graphql/handler"
    43  	"github.com/mstephano/gqlgen-schemagen/graphql/handler/transport"
    44  	"github.com/mstephano/gqlgen-schemagen/graphql/playground"
    45  	"github.com/example/test/graph"
    46  	"github.com/example/test/graph/generated"
    47  )
    48  
    49  const defaultPort = "8080"
    50  
    51  func main() {
    52  	port := os.Getenv("PORT")
    53  	if port == "" {
    54  		port = defaultPort
    55  	}
    56  
    57  	srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}}))
    58  
    59  	srv.AddTransport(&transport.Websocket{}) // <---- This is the important part!
    60  
    61  	http.Handle("/", playground.Handler("GraphQL playground", "/query"))
    62  	http.Handle("/query", srv)
    63  
    64  	log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
    65  	log.Fatal(http.ListenAndServe(":"+port, nil))
    66  }
    67  ```
    68  
    69  ## Adding Subscriptions to your Schema
    70  
    71  Next you'll have to define the subscriptions in your schema in the `Subscription` top-level type.
    72  
    73  ```graphql
    74  """
    75  Make sure you have at least something in your `Query` type.
    76  If you don't have a query the playground will be unable
    77  to introspect your schema!
    78  """
    79  type Query {
    80  	placeholder: String
    81  }
    82  
    83  """
    84  `Time` is a simple type only containing the current time as
    85  a unix epoch timestamp and a string timestamp.
    86  """
    87  type Time {
    88  	unixTime: Int!
    89  	timeStamp: String!
    90  }
    91  
    92  """
    93  `Subscription` is where all the subscriptions your clients can
    94  request. You can use Schema Directives like normal to restrict
    95  access.
    96  """
    97  type Subscription {
    98  	"""
    99  	`currentTime` will return a stream of `Time` objects.
   100  	"""
   101  	currentTime: Time!
   102  }
   103  ```
   104  
   105  ## Implementing your Resolver
   106  
   107  After regenerating your code with `go run github.com/mstephano/gqlgen-schemagen generate` you'll find a
   108  new resolver for your subscription. It will look like any other resolver, except it expects
   109  a `<-chan *model.Time` (or whatever your type is). This is a
   110  [channel](https://go.dev/tour/concurrency/2). Channels in Go are used to send objects to a
   111  single receiver.
   112  
   113  The resolver for our example `currentTime` subscription looks as follows:
   114  
   115  ```go
   116  // CurrentTime is the resolver for the currentTime field.
   117  func (r *subscriptionResolver) CurrentTime(ctx context.Context) (<-chan *model.Time, error) {
   118  	// First you'll need to `make()` your channel. Use your type here!
   119  	ch := make(chan *model.Time)
   120  
   121  	// You can (and probably should) handle your channels in a central place outside of `schema.resolvers.go`.
   122  	// For this example we'll simply use a Goroutine with a simple loop.
   123  	go func() {
   124  		for {
   125  			// In our example we'll send the current time every second.
   126  			time.Sleep(1 * time.Second)
   127  			fmt.Println("Tick")
   128  
   129  			// Prepare your object.
   130  			currentTime := time.Now()
   131  			t := &model.Time{
   132  				UnixTime:  int(currentTime.Unix()),
   133  				TimeStamp: currentTime.Format(time.RFC3339),
   134  			}
   135  
   136  			// The channel may have gotten closed due to the client disconnecting.
   137  			// To not have our Goroutine block or panic, we do the send in a select block.
   138  			// This will jump to the default case if the channel is closed.
   139  			select {
   140  			case ch <- t: // This is the actual send.
   141  				// Our message went through, do nothing
   142  			default: // This is run when our send does not work.
   143  				fmt.Println("Channel closed.")
   144  				// You can handle any deregistration of the channel here.
   145  				return // We'll just return ending the routine.
   146  			}
   147  		}
   148  	}()
   149  
   150  	// We return the channel and no error.
   151  	return ch, nil
   152  }
   153  ```
   154  
   155  ## Trying it out
   156  
   157  To try out your new subscription visit your GraphQL playground. This is exposed on
   158  `http://localhost:8080` by default.
   159  
   160  Use the following query:
   161  
   162  ```graphql
   163  subscription {
   164  	currentTime {
   165  		unixTime
   166  		timeStamp
   167  	}
   168  }
   169  ```
   170  
   171  Run your query and you should see a response updating with the current timestamp every
   172  second. To gracefully stop the connection click the `Execute query` button again.
   173  
   174  ## Full Files
   175  
   176  Here are all files at the end of this tutorial. Only files changed from the end
   177  of the quick start are listed.
   178  
   179  ### main.go
   180  
   181  ```go
   182  package main
   183  
   184  import (
   185  	"log"
   186  	"net/http"
   187  	"os"
   188  
   189  	"github.com/mstephano/gqlgen-schemagen/graphql/handler"
   190  	"github.com/mstephano/gqlgen-schemagen/graphql/handler/transport"
   191  	"github.com/mstephano/gqlgen-schemagen/graphql/playground"
   192  	"github.com/example/test/graph"
   193  	"github.com/example/test/graph/generated"
   194  )
   195  
   196  const defaultPort = "8080"
   197  
   198  func main() {
   199  	port := os.Getenv("PORT")
   200  	if port == "" {
   201  		port = defaultPort
   202  	}
   203  
   204  	srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}}))
   205  
   206  	srv.AddTransport(&transport.Websocket{})
   207  
   208  	http.Handle("/", playground.Handler("GraphQL playground", "/query"))
   209  	http.Handle("/query", srv)
   210  
   211  	log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
   212  	log.Fatal(http.ListenAndServe(":"+port, nil))
   213  }
   214  ```
   215  
   216  ### schema.graphqls
   217  
   218  ```graphql
   219  type Query {
   220  	placeholder: String
   221  }
   222  
   223  type Time {
   224  	unixTime: Int!
   225  	timeStamp: String!
   226  }
   227  
   228  type Subscription {
   229  	currentTime: Time!
   230  }
   231  ```
   232  
   233  ### schema.resolvers.go
   234  
   235  ```go
   236  package graph
   237  
   238  // This file will be automatically regenerated based on the schema, any resolver implementations
   239  // will be copied through when generating and any unknown code will be moved to the end.
   240  
   241  import (
   242  	"context"
   243  	"fmt"
   244  	"time"
   245  
   246  	"github.com/example/test/graph/generated"
   247  	"github.com/example/test/graph/model"
   248  )
   249  
   250  // Placeholder is the resolver for the placeholder field.
   251  func (r *queryResolver) Placeholder(ctx context.Context) (*string, error) {
   252  	str := "Hello World"
   253  	return &str, nil
   254  }
   255  
   256  // CurrentTime is the resolver for the currentTime field.
   257  func (r *subscriptionResolver) CurrentTime(ctx context.Context) (<-chan *model.Time, error) {
   258  	ch := make(chan *model.Time)
   259  
   260  	go func() {
   261  		for {
   262  			time.Sleep(1 * time.Second)
   263  			fmt.Println("Tick")
   264  
   265  			currentTime := time.Now()
   266  
   267  			t := &model.Time{
   268  				UnixTime:  int(currentTime.Unix()),
   269  				TimeStamp: currentTime.Format(time.RFC3339),
   270  			}
   271  
   272  			select {
   273  			case ch <- t:
   274  				// Our message went through, do nothing
   275  			default:
   276  				fmt.Println("Channel closed.")
   277  				return
   278  			}
   279  
   280  		}
   281  	}()
   282  	return ch, nil
   283  }
   284  
   285  // Query returns generated.QueryResolver implementation.
   286  func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
   287  
   288  // Subscription returns generated.SubscriptionResolver implementation.
   289  func (r *Resolver) Subscription() generated.SubscriptionResolver { return &subscriptionResolver{r} }
   290  
   291  type queryResolver struct{ *Resolver }
   292  type subscriptionResolver struct{ *Resolver }
   293  ```