From da7c1e45788d1b9b2dca9dc080d70c96729a3c89 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 3 Feb 2020 17:17:25 +1100 Subject: [PATCH] Update getting started docs --- cmd/init.go | 145 ++++++--- codegen/config/package.go | 1 - codegen/config/package_test.go | 4 +- codegen/config/resolver.go | 3 + codegen/config/resolver_test.go | 11 +- docs/content/config.md | 90 +++--- docs/content/getting-started-dep.md | 290 ------------------ docs/content/getting-started.md | 218 ++++++------- docs/content/reference/dataloaders.md | 132 ++++---- docs/static/main.css | 10 +- docs/static/syntax.css | 10 - ...chema_resolvers.go => schema.resolvers.go} | 0 .../{todo_resolvers.go => todo.resolvers.go} | 0 plugin/resolvergen/resolver.go | 7 +- plugin/resolvergen/resolver_test.go | 2 +- .../testdata/followschema/out/resolver.go | 6 - ...chema_resolvers.go => schema.resolvers.go} | 0 plugin/servergen/server.gotpl | 3 +- 18 files changed, 324 insertions(+), 608 deletions(-) delete mode 100644 docs/content/getting-started-dep.md rename example/config/{schema_resolvers.go => schema.resolvers.go} (100%) rename example/config/{todo_resolvers.go => todo.resolvers.go} (100%) delete mode 100644 plugin/resolvergen/testdata/followschema/out/home/vektah/projects/99designs/gqlgen/plugin/resolvergen/testdata/followschema/out/resolver.go rename plugin/resolvergen/testdata/followschema/out/{schema_resolvers.go => schema.resolvers.go} (100%) diff --git a/cmd/init.go b/cmd/init.go index c0b7418a91..0dff3ddeea 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -3,28 +3,74 @@ package cmd import ( "bytes" "fmt" + "html/template" "io/ioutil" "os" + "path/filepath" "strings" "github.com/99designs/gqlgen/api" - "github.com/99designs/gqlgen/plugin/servergen" - "github.com/99designs/gqlgen/codegen/config" - "github.com/pkg/errors" + "github.com/99designs/gqlgen/internal/code" + "github.com/99designs/gqlgen/plugin/servergen" "github.com/urfave/cli" - yaml "gopkg.in/yaml.v2" ) -var configComment = ` -# .gqlgen.yml example -# -# Refer to https://gqlgen.com/config/ -# for detailed .gqlgen.yml documentation. -` +var configTemplate = template.Must(template.New("name").Parse( + `# Where are all the schema files located? globs are supported eg src/**/*.graphqls +schema: + - graph/*.graphqls + +# Where should the generated server code go? +exec: + filename: graph/generated/generated.go + package: generated + +# Where should any generated models go? +model: + filename: graph/model/models_gen.go + package: model + +# Where should the resolver implementations go? +resolver: + layout: follow-schema + dir: graph + package: graph + +# Optional: turn on use ` + "`" + `gqlgen:"fieldName"` + "`" + ` tags in your models +# struct_tag: json -var schemaDefault = ` -# GraphQL schema example +# Optional: turn on to use []Thing instead of []*Thing +# omit_slice_element_pointers: false + +# Optional: set to speed up generation time by not performing a final validation pass. +# skip_validation: true + +# gqlgen will search for any type names in the schema in these go packages +# if they match it will use them, otherwise it will generate them. +autobind: + - "{{.}}/graph/model" + +# This section declares type mapping between the GraphQL and go type systems +# +# The first line in each type will be used as defaults for resolver arguments and +# modelgen, the others will be allowed when binding to fields. Configure them to +# your liking +models: + ID: + model: + - github.com/99designs/gqlgen/graphql.ID + - github.com/99designs/gqlgen/graphql.Int + - github.com/99designs/gqlgen/graphql.Int64 + - github.com/99designs/gqlgen/graphql.Int32 + Int: + model: + - github.com/99designs/gqlgen/graphql.Int + - github.com/99designs/gqlgen/graphql.Int64 + - github.com/99designs/gqlgen/graphql.Int32 +`)) + +var schemaDefault = `# GraphQL schema example # # https://gqlgen.com/getting-started/ @@ -60,14 +106,25 @@ var initCmd = cli.Command{ Flags: []cli.Flag{ cli.BoolFlag{Name: "verbose, v", Usage: "show logs"}, cli.StringFlag{Name: "config, c", Usage: "the config filename"}, - cli.StringFlag{Name: "server", Usage: "where to write the server stub to", Value: "server/server.go"}, - cli.StringFlag{Name: "schema", Usage: "where to write the schema stub to", Value: "schema.graphql"}, + cli.StringFlag{Name: "server", Usage: "where to write the server stub to", Value: "server.go"}, + cli.StringFlag{Name: "schema", Usage: "where to write the schema stub to", Value: "graph/schema.graphqls"}, }, Action: func(ctx *cli.Context) { + configFilename := ctx.String("config") + serverFilename := ctx.String("server") + + pkgName := code.ImportPathForDir(".") + if pkgName == "" { + fmt.Fprintln(os.Stderr, "unable to determine import path for current directory, you probably need to run go mod init first") + os.Exit(1) + } + initSchema(ctx.String("schema")) - initConfig(ctx) + if !configExists(configFilename) { + initConfig(configFilename, pkgName) + } - GenerateGraphServer(ctx.String("server")) + GenerateGraphServer(serverFilename) }, } @@ -76,59 +133,41 @@ func GenerateGraphServer(serverFilename string) { if err != nil { fmt.Fprintln(os.Stderr, err.Error()) } - err = api.Generate(cfg, api.AddPlugin(servergen.New(serverFilename))) - if err != nil { + + if err := api.Generate(cfg, api.AddPlugin(servergen.New(serverFilename))); err != nil { fmt.Fprintln(os.Stderr, err.Error()) } fmt.Fprintf(os.Stdout, "Exec \"go run ./%s\" to start GraphQL server\n", serverFilename) } -func initConfig(ctx *cli.Context) { +func configExists(configFilename string) bool { var cfg *config.Config - var err error - configFilename := ctx.String("config") + if configFilename != "" { - cfg, err = config.LoadConfig(configFilename) + cfg, _ = config.LoadConfig(configFilename) } else { - cfg, err = config.LoadConfigFromDefaultLocations() - } - - if cfg != nil { - fmt.Fprintf(os.Stderr, "init failed: a configuration file already exists\n") - os.Exit(1) - } - - if !os.IsNotExist(errors.Cause(err)) { - fmt.Fprintln(os.Stderr, err.Error()) - os.Exit(1) + cfg, _ = config.LoadConfigFromDefaultLocations() } + return cfg != nil +} +func initConfig(configFilename string, pkgName string) { if configFilename == "" { configFilename = "gqlgen.yml" } - cfg = config.DefaultConfig() - cfg.Resolver = config.ResolverConfig{ - Type: "Resolver", - Layout: config.LayoutFollowSchema, - DirName: ".", + if err := os.MkdirAll(filepath.Dir(configFilename), 0755); err != nil { + fmt.Fprintln(os.Stderr, "unable to create config dir: "+err.Error()) + os.Exit(1) } - cfg.SchemaFilename = config.StringList{ctx.String("schema")} var buf bytes.Buffer - buf.WriteString(strings.TrimSpace(configComment)) - buf.WriteString("\n\n") - var b []byte - b, err = yaml.Marshal(cfg) - if err != nil { - fmt.Fprintln(os.Stderr, "unable to marshal yaml: "+err.Error()) - os.Exit(1) + if err := configTemplate.Execute(&buf, pkgName); err != nil { + panic(err) } - buf.Write(b) - err = ioutil.WriteFile(configFilename, buf.Bytes(), 0644) - if err != nil { + if err := ioutil.WriteFile(configFilename, buf.Bytes(), 0644); err != nil { fmt.Fprintln(os.Stderr, "unable to write cfg file: "+err.Error()) os.Exit(1) } @@ -140,8 +179,12 @@ func initSchema(schemaFilename string) { return } - err = ioutil.WriteFile(schemaFilename, []byte(strings.TrimSpace(schemaDefault)), 0644) - if err != nil { + if err := os.MkdirAll(filepath.Dir(schemaFilename), 0755); err != nil { + fmt.Fprintln(os.Stderr, "unable to create schema dir: "+err.Error()) + os.Exit(1) + } + + if err = ioutil.WriteFile(schemaFilename, []byte(strings.TrimSpace(schemaDefault)), 0644); err != nil { fmt.Fprintln(os.Stderr, "unable to write schema file: "+err.Error()) os.Exit(1) } diff --git a/codegen/config/package.go b/codegen/config/package.go index 9e34ea6e34..a964593819 100644 --- a/codegen/config/package.go +++ b/codegen/config/package.go @@ -12,7 +12,6 @@ import ( type PackageConfig struct { Filename string `yaml:"filename,omitempty"` Package string `yaml:"package,omitempty"` - Type string `yaml:"type,omitempty"` } func (c *PackageConfig) ImportPath() string { diff --git a/codegen/config/package_test.go b/codegen/config/package_test.go index 239797e6aa..524c5c59e4 100644 --- a/codegen/config/package_test.go +++ b/codegen/config/package_test.go @@ -21,7 +21,7 @@ func TestPackageConfig(t *testing.T) { require.Equal(t, "github.com/99designs/gqlgen/codegen/config/testdata", p.Pkg().Path()) require.Contains(t, filepath.ToSlash(p.Filename), "codegen/config/testdata/example.go") - require.Contains(t, p.Dir(), "codegen/config/testdata") + require.Contains(t, filepath.ToSlash(p.Dir()), "codegen/config/testdata") }) t.Run("when given both", func(t *testing.T) { @@ -37,7 +37,7 @@ func TestPackageConfig(t *testing.T) { require.Equal(t, "github.com/99designs/gqlgen/codegen/config/testdata", p.Pkg().Path()) require.Contains(t, filepath.ToSlash(p.Filename), "codegen/config/testdata/example.go") - require.Contains(t, p.Dir(), "codegen/config/testdata") + require.Contains(t, filepath.ToSlash(p.Dir()), "codegen/config/testdata") }) t.Run("when given nothing", func(t *testing.T) { diff --git a/codegen/config/resolver.go b/codegen/config/resolver.go index 39d6480b81..9859d6e3a0 100644 --- a/codegen/config/resolver.go +++ b/codegen/config/resolver.go @@ -28,6 +28,9 @@ func (r *ResolverConfig) Check() error { if r.Layout == "" { r.Layout = LayoutSingleFile } + if r.Type == "" { + r.Type = "Resolver" + } switch r.Layout { case LayoutSingleFile: diff --git a/codegen/config/resolver_test.go b/codegen/config/resolver_test.go index 0c158faab4..271bdfd630 100644 --- a/codegen/config/resolver_test.go +++ b/codegen/config/resolver_test.go @@ -13,6 +13,7 @@ func TestResolverConfig(t *testing.T) { p := ResolverConfig{Filename: "testdata/example.go"} require.True(t, p.IsDefined()) + require.NoError(t, p.Check()) require.NoError(t, p.Check()) require.Equal(t, p.Package, "config_test_data") @@ -22,13 +23,14 @@ func TestResolverConfig(t *testing.T) { require.Equal(t, "github.com/99designs/gqlgen/codegen/config/testdata", p.Pkg().Path()) require.Contains(t, filepath.ToSlash(p.Filename), "codegen/config/testdata/example.go") - require.Contains(t, p.Dir(), "codegen/config/testdata") + require.Contains(t, filepath.ToSlash(p.Dir()), "codegen/config/testdata") }) t.Run("when given both", func(t *testing.T) { p := ResolverConfig{Filename: "testdata/example.go", Package: "wololo"} require.True(t, p.IsDefined()) + require.NoError(t, p.Check()) require.NoError(t, p.Check()) require.Equal(t, p.Package, "wololo") @@ -38,7 +40,7 @@ func TestResolverConfig(t *testing.T) { require.Equal(t, "github.com/99designs/gqlgen/codegen/config/testdata", p.Pkg().Path()) require.Contains(t, filepath.ToSlash(p.Filename), "codegen/config/testdata/example.go") - require.Contains(t, p.Dir(), "codegen/config/testdata") + require.Contains(t, filepath.ToSlash(p.Dir()), "codegen/config/testdata") }) t.Run("when given nothing", func(t *testing.T) { @@ -76,6 +78,7 @@ func TestResolverConfig(t *testing.T) { p := ResolverConfig{Layout: LayoutFollowSchema, DirName: "testdata"} require.True(t, p.IsDefined()) + require.NoError(t, p.Check()) require.NoError(t, p.Check()) require.Equal(t, p.Package, "config_test_data") @@ -92,6 +95,7 @@ func TestResolverConfig(t *testing.T) { p := ResolverConfig{Layout: LayoutFollowSchema, DirName: "testdata", Package: "wololo"} require.True(t, p.IsDefined()) + require.NoError(t, p.Check()) require.NoError(t, p.Check()) require.Equal(t, p.Package, "wololo") @@ -105,9 +109,10 @@ func TestResolverConfig(t *testing.T) { }) t.Run("when given a filename", func(t *testing.T) { - p := ResolverConfig{Layout: LayoutFollowSchema, DirName: "testdata", Filename: "asdf.go"} + p := ResolverConfig{Layout: LayoutFollowSchema, DirName: "testdata", Filename: "testdata/asdf.go"} require.True(t, p.IsDefined()) + require.NoError(t, p.Check()) require.NoError(t, p.Check()) require.Equal(t, p.Package, "config_test_data") diff --git a/docs/content/config.md b/docs/content/config.md index 20d2bbc25e..cbc51d4ccb 100644 --- a/docs/content/config.md +++ b/docs/content/config.md @@ -11,70 +11,58 @@ gqlgen can be configured using a `gqlgen.yml` file, by default it will be loaded Example: ```yml -# You can pass a single schema file -schema: schema.graphql - -# Or multiple files -schema: - - schema.graphql - - user.graphql - -# Or you can use globs +# Where are all the schema files located? globs are supported eg src/**/*.graphqls schema: - - "*.graphql" + - graph/*.graphqls -# Or globs from a root directory -schema: - - "schema/**/*.graphql" - -# Let gqlgen know where to put the generated server +# Where should the generated server code go? exec: filename: graph/generated/generated.go package: generated -# Let gqlgen know where to put the generated models (if any) -# Set to null to disable +# Where should any generated models go? model: - filename: models/generated.go - package: models + filename: graph/model/models_gen.go + package: model -# Optional, turns on resolver stub generation +# Where should the resolver implementations go? resolver: - filename: resolver.go # where to write them - type: Resolver # what's the resolver root implementation type called? + layout: follow-schema + dir: graph + package: graph -# Optional, turns on binding to field names by tag provided -struct_tag: json +# Optional: turn on use ` + "`" + `gqlgen:"fieldName"` + "`" + ` tags in your models +# struct_tag: json -# Optional, set to true if you prefer []Thing over []*Thing -omit_slice_element_pointers: false +# Optional: turn on to use []Thing instead of []*Thing +# omit_slice_element_pointers: false -# Optional, set to speed up generation time by not performing a final validation pass -skip_validation: true +# Optional: set to speed up generation time by not performing a final validation pass. +# skip_validation: true -# Instead of listing out every model like below, you can automatically bind to any matching types -# within the given path by using `model: User` or `model: models.User`. EXPERIMENTAL in v0.9.1 +# gqlgen will search for any type names in the schema in these go packages +# if they match it will use them, otherwise it will generate them. autobind: - - github.com/my/app/models + - "{{.}}/graph/model" -# Tell gqlgen about any existing models you want to reuse for -# graphql. These normally come from the db or a remote api. +# This section declares type mapping between the GraphQL and go type systems +# +# The first line in each type will be used as defaults for resolver arguments and +# modelgen, the others will be allowed when binding to fields. Configure them to +# your liking models: - User: - model: models.User # can use short paths when the package is listed in autobind - Todo: - model: github.com/my/app/models.Todo # or full paths if you need to go elsewhere - fields: - id: - resolver: true # force a resolver to be generated - fieldName: todoId # bind to a different go field name - # model also accepts multiple backing go types. When mapping onto structs - # any of these types can be used, the first one is used as the default for - # resolver args. ID: model: - - github.com/99designs/gqlgen/graphql.IntID - github.com/99designs/gqlgen/graphql.ID + - github.com/99designs/gqlgen/graphql.Int + - github.com/99designs/gqlgen/graphql.Int64 + - github.com/99designs/gqlgen/graphql.Int32 + Int: + model: + - github.com/99designs/gqlgen/graphql.Int + - github.com/99designs/gqlgen/graphql.Int64 + - github.com/99designs/gqlgen/graphql.Int32 + ``` Everything has defaults, so add things as you need. @@ -86,14 +74,14 @@ gqlgen ships with some builtin directives that make it a little easier to manage To start using them you first need to define them: ```graphql -directive @goModel(model: String, models: [String!]) on OBJECT - | INPUT_OBJECT - | SCALAR - | ENUM - | INTERFACE +directive @goModel(model: String, models: [String!]) on OBJECT + | INPUT_OBJECT + | SCALAR + | ENUM + | INTERFACE | UNION -directive @goField(forceResolver: Boolean, name: String) on INPUT_FIELD_DEFINITION +directive @goField(forceResolver: Boolean, name: String) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION ``` diff --git a/docs/content/getting-started-dep.md b/docs/content/getting-started-dep.md deleted file mode 100644 index 93c8410c37..0000000000 --- a/docs/content/getting-started-dep.md +++ /dev/null @@ -1,290 +0,0 @@ ---- -linkTitle: Getting Started Using dep -title: Building GraphQL servers in golang -description: Get started building type-safe GraphQL servers in Golang using gqlgen -weight: -7 -hidden: true ---- - -> Deprecated -> -> This tutorial uses the `dep` tool to manage dependencies instead of Go Modules and should be considered a deprecated way to use gqlgen. Read out new [Getting Started]({{< ref "getting-started.md" >}}) guide for instructions for using Go Modules. - -This tutorial will take you through the process of building a GraphQL server with gqlgen that can: - - - Return a list of todos - - Create new todos - - Mark off todos as they are completed - -You can find the finished code for this tutorial [here](https://github.com/vektah/gqlgen-tutorials/tree/master/gettingstarted) - -## Install gqlgen - -This article uses [`dep`](https://github.com/golang/dep) to install gqlgen. [Follow the instructions for your environment](https://github.com/golang/dep) to install. - -Assuming you already have a working [Go environment](https://golang.org/doc/install), create a directory for the project in your `$GOPATH`: - -```sh -$ mkdir -p $GOPATH/src/github.com/[username]/gqlgen-todos -``` - -Add the following file to your project under `scripts/gqlgen.go`: - -```go -// +build ignore - -package main - -import "github.com/99designs/gqlgen/cmd" - -func main() { - cmd.Execute() -} -``` - -Lastly, initialise dep. This will inspect any imports you have in your project, and pull down the latest tagged release. - -```sh -$ dep init -``` - -## Building the server - -### Define the schema - -gqlgen is a schema-first library — before writing code, you describe your API using the GraphQL -[Schema Definition Language](http://graphql.org/learn/schema/). This usually goes into a file called `schema.graphql`: - -```graphql -type Todo { - id: ID! - text: String! - done: Boolean! - user: User! -} - -type User { - id: ID! - name: String! -} - -type Query { - todos: [Todo!]! -} - -input NewTodo { - text: String! - userId: String! -} - -type Mutation { - createTodo(input: NewTodo!): Todo! -} -``` - -### Create the project skeleton - -```bash -$ go run scripts/gqlgen.go init -``` - -This has created an empty skeleton with all files you need: - - - `gqlgen.yml` — The gqlgen config file, knobs for controlling the generated code. - - `generated.go` — The GraphQL execution runtime, the bulk of the generated code. - - `models_gen.go` — Generated models required to build the graph. Often you will override these with your own models. Still very useful for input types. - - `resolver.go` — This is where your application code lives. `generated.go` will call into this to get the data the user has requested. - - `server/server.go` — This is a minimal entry point that sets up an `http.Handler` to the generated GraphQL server. - - Now run dep ensure, so that we can ensure that the newly generated code's dependencies are all present: - - ```sh - $ dep ensure - ``` - -### Create the database models - -The generated model for Todo isn't right, it has a user embeded in it but we only want to fetch it if the user actually requested it. So instead lets make a new model in `todo.go`: - -```go -package gettingstarted - -type Todo struct { - ID string - Text string - Done bool - UserID string -} -``` - -Next tell gqlgen to use this new struct by adding it to `gqlgen.yml`: - -```yaml -models: - Todo: - model: github.com/[username]/gqlgen-todos/gettingstarted.Todo -``` - -Regenerate by running: - -```bash -$ go run scripts/gqlgen.go -v -Unable to bind Todo.user to github.com/[username]/gqlgen-todos/gettingstarted.Todo - no method named user - no field named user - Adding resolver method -``` - -> Note -> -> The verbose flag `-v` is here to show what gqlgen is doing. It has looked at all the fields on the model and found matching methods for all of them, except user. For user it has added a resolver to the interface you need to implement. *This is the magic that makes gqlgen work so well!* - -### Implement the resolvers - -The generated runtime has defined an interface for all the missing resolvers that we need to provide. Lets take a look in `generated.go` - -```go -// NewExecutableSchema creates an ExecutableSchema from the ResolverRoot interface. -func NewExecutableSchema(cfg Config) graphql.ExecutableSchema { - return &executableSchema{ - resolvers: cfg.Resolvers, - directives: cfg.Directives, - } -} - -type Config struct { - Resolvers ResolverRoot - Directives DirectiveRoot -} - -type ResolverRoot interface { - Mutation() MutationResolver - Query() QueryResolver - Todo() TodoResolver -} - -type DirectiveRoot struct { -} -type MutationResolver interface { - CreateTodo(ctx context.Context, input NewTodo) (Todo, error) -} -type QueryResolver interface { - Todos(ctx context.Context) ([]Todo, error) -} -type TodoResolver interface { - User(ctx context.Context, obj *Todo) (User, error) -} -``` - -Notice the `TodoResolver.User` method? Thats gqlgen saying "I dont know how to get a User from a Todo, you tell me.". -Its worked out how to build everything else for us. - -For any missing models (like NewTodo) gqlgen will generate a go struct. This is usually only used for input types and -one-off return values. Most of the time your types will be coming from the database, or an API client so binding is -better than generating. - -### Write the resolvers - -This is a work in progress, we have a way to generate resolver stubs, but it cannot currently update existing code. We can force it to run again by deleting `resolver.go` and re-running gqlgen: - -```bash -$ rm resolver.go -$ go run scripts/gqlgen.go -``` - -Now we just need to fill in the `not implemented` parts. Update `resolver.go` - -```go -package gettingstarted - -import ( - context "context" - "fmt" - "math/rand" -) - -type Resolver struct { - todos []Todo -} - -func (r *Resolver) Mutation() MutationResolver { - return &mutationResolver{r} -} -func (r *Resolver) Query() QueryResolver { - return &queryResolver{r} -} -func (r *Resolver) Todo() TodoResolver { - return &todoResolver{r} -} - -type mutationResolver struct{ *Resolver } - -func (r *mutationResolver) CreateTodo(ctx context.Context, input NewTodo) (*Todo, error) { - todo := &Todo{ - Text: input.Text, - ID: fmt.Sprintf("T%d", rand.Int()), - UserID: input.UserID, - } - r.todos = append(r.todos, *todo) - return todo, nil -} - -type queryResolver struct{ *Resolver } - -func (r *queryResolver) Todos(ctx context.Context) ([]Todo, error) { - return r.todos, nil -} - -type todoResolver struct{ *Resolver } - -func (r *todoResolver) User(ctx context.Context, obj *Todo) (*User, error) { - return &User{ID: obj.UserID, Name: "user " + obj.UserID}, nil -} - -``` - -We now have a working server, to start it: -```bash -go run server/server.go -``` - -then open http://localhost:8080 in a browser. here are some queries to try: -```graphql -mutation createTodo { - createTodo(input:{text:"todo", userId:"1"}) { - user { - id - } - text - done - } -} - -query findTodos { - todos { - text - done - user { - name - } - } -} -``` - -## Finishing touches - -At the top of our `resolver.go` add the following line: - -```go -//go:generate go run scripts/gqlgen.go -v -``` - -This magic comment tells `go generate` what command to run when we want to regenerate our code. To run go generate recursively over your entire project, use this command: - -```go -go generate ./... -``` - -> Note -> -> Ensure that the path to your `gqlgen` binary is relative to the file the generate command is added to. diff --git a/docs/content/getting-started.md b/docs/content/getting-started.md index 7f7d0dfd01..22116a641c 100644 --- a/docs/content/getting-started.md +++ b/docs/content/getting-started.md @@ -1,7 +1,7 @@ --- linkTitle: Getting Started title: Building GraphQL servers in golang -description: Get started building type-safe GraphQL servers in Golang using gqlgen +description: Get started building type-safe GraphQL servers in Golang using gqlgen menu: main weight: -7 --- @@ -14,10 +14,6 @@ This tutorial will take you through the process of building a GraphQL server wit You can find the finished code for this tutorial [here](https://github.com/vektah/gqlgen-tutorials/tree/master/gettingstarted) -> Note -> -> This tutorial uses Go Modules and requires Go 1.11+. If you want to use this tutorial without Go Modules, take a look at our [Getting Started Using dep]({{< ref "getting-started-dep.md" >}}) guide instead. - ## Setup Project Create a directory for your project, and initialise it as a Go Module: @@ -26,15 +22,40 @@ Create a directory for your project, and initialise it as a Go Module: $ mkdir gqlgen-todos $ cd gqlgen-todos $ go mod init github.com/[username]/gqlgen-todos +$ go get github.com/99designs/gqlgen ``` ## Building the server -### Define the schema +### Create the project skeleton + +```bash +$ go run github.com/99designs/gqlgen init +``` + +This will create our suggested package layout. You can modify these paths in gqlgen.yml if you need to. +``` +├── go.mod +├── go.sum +├── gqlgen.yml - The gqlgen config file, knobs for controlling the generated code. +├── graph +│   ├── generated - A package that only contains the generated runtime +│   │   └── generated.go +│   ├── model - A package for all your graph models, generated or otherwise +│   │   └── models_gen.go +│   ├── resolver.go - The root graph resolver type. This file wont get regenerated +│   ├── schema.graphqls - Some schema. You can split the schema into as many graphql files as you like +│   └── schema.resolvers.go - the resolver implementation for schema.graphql +└── server.go - The entry point to your app. Customize it however you see fit +``` + +### Define your schema -gqlgen is a schema-first library — before writing code, you describe your API using the GraphQL -[Schema Definition Language](http://graphql.org/learn/schema/). This usually goes into a file called `schema.graphql`. A basic example as follows will be generated by the `init` command: +gqlgen is a schema-first library — before writing code, you describe your API using the GraphQL +[Schema Definition Language](http://graphql.org/learn/schema/). By default this goes into a file called +`schema.graphql` but you can break it up into as many different files as you want. +The schema that was generated for us was: ```graphql type Todo { id: ID! @@ -62,149 +83,49 @@ type Mutation { } ``` -### Create the project skeleton - -```bash -$ go run github.com/99designs/gqlgen init -``` - -This has created an empty skeleton with all files you need: +### Implement the resolvers - - `gqlgen.yml` — The gqlgen config file, knobs for controlling the generated code. - - `generated.go` — The GraphQL execution runtime, the bulk of the generated code. - - `models_gen.go` — Generated models required to build the graph. Often you will override these with your own models. Still very useful for input types. - - `resolver.go` — This is where your application code lives. `generated.go` will call into this to get the data the user has requested. - - `server/server.go` — This is a minimal entry point that sets up an `http.Handler` to the generated GraphQL server. - -### Create the database models +`gqlgen generate` compares the schema file (`graph/schema.graphqls`) with the models `graph/model/*` and wherever it +can it will bind directly to the model. -The generated model for Todo isn't right, it has a user embeded in it but we only want to fetch it if the user actually requested it. So instead lets make a new model in `todo.go`: +If we take a look in `graph/schema.resolvers.go` we will see all the times that gqlgen couldn't match them up. For us +it was twice: ```go -package gqlgen_todos - -type Todo struct { - ID string - Text string - Done bool - UserID string +func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (*model.Todo, error) { + panic(fmt.Errorf("not implemented")) } -``` -Next tell gqlgen to use this new struct by adding it to `gqlgen.yml`: - -```yaml -models: - Todo: - model: github.com/[username]/gqlgen-todos.Todo -``` - -Regenerate by running: - -```bash -$ go run github.com/99designs/gqlgen -v +func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) { + panic(fmt.Errorf("not implemented")) +} ``` -> Note -> -> The verbose flag `-v` is here to show what gqlgen is doing. It has looked at all the fields on the model and found matching methods for all of them, except user. For user it has added a resolver to the interface you need to implement. *This is the magic that makes gqlgen work so well!* - -### Implement the resolvers - -The generated runtime has defined an interface for all the missing resolvers that we need to provide. Lets take a look in `generated.go`: +We just need to implement these two methods to get our server working: +First we need somewhere to track our state, lets put it in `graph/resolver.go`: ```go -func NewExecutableSchema(cfg Config) graphql.ExecutableSchema {} - // ... +type Resolver struct{ + todos []*model.Todo } - -type Config struct { - Resolvers ResolverRoot - // ... -} - -type ResolverRoot interface { - Mutation() MutationResolver - Query() QueryResolver - Todo() TodoResolver -} - -type MutationResolver interface { - CreateTodo(ctx context.Context, input NewTodo) (*Todo, error) -} -type QueryResolver interface { - Todos(ctx context.Context) ([]Todo, error) -} -type TodoResolver interface { - User(ctx context.Context, obj *Todo) (*User, error) -} -``` - -Notice the `TodoResolver.User` method? Thats gqlgen saying "I dont know how to get a User from a Todo, you tell me.". -Its worked out how to build everything else for us. - -For any missing models (like `NewTodo`) gqlgen will generate a go struct. This is usually only used for input types and -one-off return values. Most of the time your types will be coming from the database, or an API client so binding is -better than generating. - -### Write the resolvers - -This is a work in progress, we have a way to generate resolver stubs, but it cannot currently update existing code. We can force it to run again by deleting `resolver.go` and re-running gqlgen: - -```bash -$ rm resolver.go -$ go run github.com/99designs/gqlgen ``` - -Now we just need to fill in the `not implemented` parts. Update `resolver.go` +This is where we declare any dependencies for our app like our database, it gets initialized once in `server.go` when +we create the graph. ```go -package gqlgen_todos - -import ( - context "context" - "fmt" - "math/rand" -) - -type Resolver struct { - todos []*Todo -} - -func (r *Resolver) Mutation() MutationResolver { - return &mutationResolver{r} -} -func (r *Resolver) Query() QueryResolver { - return &queryResolver{r} -} -func (r *Resolver) Todo() TodoResolver { - return &todoResolver{r} -} - -type mutationResolver struct{ *Resolver } - -func (r *mutationResolver) CreateTodo(ctx context.Context, input NewTodo) (*Todo, error) { - todo := &Todo{ +func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (*model.Todo, error) { + todo := &model.Todo{ Text: input.Text, ID: fmt.Sprintf("T%d", rand.Int()), - UserID: input.UserID, + User: &model.User{ID: input.UserID, Name: "user " + input.UserID}, } r.todos = append(r.todos, todo) return todo, nil } -type queryResolver struct{ *Resolver } - -func (r *queryResolver) Todos(ctx context.Context) ([]*Todo, error) { +func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) { return r.todos, nil } - -type todoResolver struct{ *Resolver } - -func (r *todoResolver) User(ctx context.Context, obj *Todo) (*User, error) { - return &User{ID: obj.UserID, Name: "user " + obj.UserID}, nil -} - ``` We now have a working server, to start it: @@ -235,6 +156,47 @@ query findTodos { } ``` +### Dont eagarly fetch the user + +This example is great, but in the real world fetching most objects is expensive. We dont want to load the User on the +todo unless the user actually asked for it. So lets replace the generated `Todo` model with something slightly more +realistic. + +Create a new file called `graph/model/todo.go` +```go +package model + +type Todo struct { + ID string `json:"id"` + Text string `json:"text"` + Done bool `json:"done"` + UserId int `json:"user"` +} +``` + +> Note +> +> By default gqlgen will use any models in the model directory that match on name, this can be configured in `gqlgen.yml`. + +And run `go run github.com/99designs/gqlgen generate`. + +Now if we look in `graph/schema.resolvers.go` we can see a new resolver, lets implement it and fix `CreateTodo`. +```go +func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (*model.Todo, error) { + todo := &model.Todo{ + Text: input.Text, + ID: fmt.Sprintf("T%d", rand.Int()), + UserID: input.UserID, // fix this line + } + r.todos = append(r.todos, todo) + return todo, nil +} + +func (r *todoResolver) User(ctx context.Context, obj *model.Todo) (*model.User, error) { + return &model.User{ID: obj.UserID, Name: "user " + obj.UserID}, nil +} +``` + ## Finishing touches At the top of our `resolver.go`, between `package` and `import`, add the following line: diff --git a/docs/content/reference/dataloaders.md b/docs/content/reference/dataloaders.md index f1423511b2..2cf4ac7fb1 100644 --- a/docs/content/reference/dataloaders.md +++ b/docs/content/reference/dataloaders.md @@ -1,13 +1,13 @@ --- title: "Optimizing N+1 database queries using Dataloaders" -description: Speeding up your GraphQL requests by reducing the number of round trips to the database. +description: Speeding up your GraphQL requests by reducing the number of round trips to the database. linkTitle: Dataloaders menu: { main: { parent: 'reference' } } --- -Have you noticed some GraphQL queries end can make hundreds of database -queries, often with mostly repeated data? Lets take a look why and how to -fix it. +Have you noticed some GraphQL queries end can make hundreds of database +queries, often with mostly repeated data? Lets take a look why and how to +fix it. ## Query Resolution @@ -19,14 +19,14 @@ query { todos { users { name } } } and our todo.user resolver looks like this: ```go -func (r *Resolver) Todo_user(ctx context.Context, obj *Todo) (*User, error) { - res := logAndQuery(r.db, "SELECT id, name FROM user WHERE id = ?", obj.UserID) +func (r *todoResolver) UserRaw(ctx context.Context, obj *model.Todo) (*model.User, error) { + res := db.LogAndQuery(r.Conn, "SELECT id, name FROM dataloader_example.user WHERE id = ?", obj.UserID) defer res.Close() if !res.Next() { return nil, nil } - var user User + var user model.User if err := res.Scan(&user.ID, &user.Name); err != nil { panic(err) } @@ -34,10 +34,10 @@ func (r *Resolver) Todo_user(ctx context.Context, obj *Todo) (*User, error) { } ``` -**Note**: I'm going to use go's low level `sql.DB` here. All of this will +**Note**: I'm going to use go's low level `sql.DB` here. All of this will work with whatever your favourite ORM is. -The query executor will call the Query_todos resolver which does a `select * from todo` and +The query executor will call the Query.Todos resolver which does a `select * from todo` and return N todos. Then for each of the todos, concurrently, call the Todo_user resolver, `SELECT from USER where id = todo.id`. @@ -71,79 +71,93 @@ Whats even worse? most of those todos are all owned by the same user! We can do ## Dataloader -What we need is a way to group up all of those concurrent requests, take out any duplicates, and -store them in case they are needed later on in request. The dataloader is just that, a request-scoped -batching and caching solution popularised by [facebook](https://github.com/facebook/dataloader). +What we need is a way to group up all of those concurrent requests, take out any duplicates, and +store them in case they are needed later on in request. The dataloader is just that, a request-scoped +batching and caching solution popularised by [facebook](https://github.com/facebook/dataloader). -We're going to use [dataloaden](https://github.com/vektah/dataloaden) to build our dataloaders. -In languages with generics, we could probably just create a DataLoader, but golang -doesnt have generics. Instead we generate the code manually for our instance. +We're going to use [dataloaden](https://github.com/vektah/dataloaden) to build our dataloaders. +In languages with generics, we could probably just create a DataLoader, but golang +doesnt have generics. Instead we generate the code manually for our instance. ```bash go get github.com/vektah/dataloaden - -dataloaden github.com/full/package/name.User +mkdir dataloader +cd dataloader +go run github.com/vektah/dataloaden UserLoader int *gqlgen-tutorials/dataloader/graph/model.User ``` -Next we need to create an instance of our new dataloader and tell how to fetch data. +Next we need to create an instance of our new dataloader and tell how to fetch data. Because dataloaders are request scoped, they are a good fit for `context`. ```go -const userLoaderKey = "userloader" +const loadersKey = "dataloaders" + +type Loaders struct { + UserById UserLoader +} -func DataloaderMiddleware(db *sql.DB, next http.Handler) http.Handler { +func Middleware(conn *sql.DB, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - userloader := UserLoader{ - maxBatch: 100, - wait: 1 * time.Millisecond, - fetch: func(ids []int) ([]*User, []error) { - placeholders := make([]string, len(ids)) - args := make([]interface{}, len(ids)) - for i := 0; i < len(ids); i++ { - placeholders[i] = "?" - args[i] = ids[i] - } - - res := logAndQuery(db, - "SELECT id, name from user WHERE id IN ("+ - strings.Join(placeholders, ",")+")", - args..., - ) - - defer res.Close() - - users := make(map[int]*User, len(ids)) - for res.Next() { - user := &User{} - err := res.Scan(&user.ID, &user.Name) - if err != nil { - panic(err) + ctx := context.WithValue(r.Context(), loadersKey, &Loaders{ + UserById: UserLoader{ + maxBatch: 100, + wait: 1 * time.Millisecond, + fetch: func(ids []int) ([]*model.User, []error) { + placeholders := make([]string, len(ids)) + args := make([]interface{}, len(ids)) + for i := 0; i < len(ids); i++ { + placeholders[i] = "?" + args[i] = i + } + + res := db.LogAndQuery(conn, + "SELECT id, name from dataloader_example.user WHERE id IN ("+strings.Join(placeholders, ",")+")", + args..., + ) + defer res.Close() + + userById := map[int]*model.User{} + for res.Next() { + user := model.User{} + err := res.Scan(&user.ID, &user.Name) + if err != nil { + panic(err) + } + userById[user.ID] = &user + } + + users := make([]*model.User, len(ids)) + for i, id := range ids { + users[i] = userById[id] + i++ } - users[user.ID] = user - } - - output := make([]*User, len(ids)) - for i, id := range ids { - output[i] = users[id] - } - return output, nil + + return users, nil + }, }, - } - ctx := context.WithValue(r.Context(), userLoaderKey, &userloader) + }) r = r.WithContext(ctx) next.ServeHTTP(w, r) }) } -func (r *Resolver) Todo_userLoader(ctx context.Context, obj *Todo) (*User, error) { - return ctx.Value(userLoaderKey).(*UserLoader).Load(obj.UserID) +func For(ctx context.Context) *Loaders { + return ctx.Value(loadersKey).(*Loaders) } -``` -This dataloader will wait for up to 1 millisecond to get 100 unique requests and then call +``` + +This dataloader will wait for up to 1 millisecond to get 100 unique requests and then call the fetch function. This function is a little ugly, but half of it is just building the SQL! +Now lets update our resolver to call the dataloader: +```go +func (r *todoResolver) UserLoader(ctx context.Context, obj *model.Todo) (*model.User, error) { + return dataloader.For(ctx).UserById.Load(obj.UserID) +} +``` + The end result? just 2 queries! ```sql SELECT id, todo, user_id FROM todo diff --git a/docs/static/main.css b/docs/static/main.css index 7bf7f5c494..efaa3d57c2 100644 --- a/docs/static/main.css +++ b/docs/static/main.css @@ -259,13 +259,21 @@ ul.menu a:hover { } code { - padding: 1px 5px; font-family: var(--font-code); font-weight: 500; color: var(--color-code-text); background-color: var(--color-code-background); border-radius: 3px; + display: inline-block; + padding: 0px 5px; font-size: 13px; + line-height: 1.5; +} + +pre > code { + overflow: auto; + display: block; + padding: 5px 10px; margin-bottom: var(--margin-default); } diff --git a/docs/static/syntax.css b/docs/static/syntax.css index 8e239fd5dd..4f561d4673 100644 --- a/docs/static/syntax.css +++ b/docs/static/syntax.css @@ -55,13 +55,3 @@ .chroma .vg { color: #aa0000 } /* Name.Variable.Global */ .chroma .vi { color: #aa0000 } /* Name.Variable.Instance */ .chroma .il { color: #009999 } /* Literal.Number.Integer.Long */ - -.chroma code { - overflow: auto; - display: block; - padding: 5px 10px; - font-family: var(--font-code); - color: var(--color-code-text); - background-color: var(--color-code-background); - font-size: 13px; -} diff --git a/example/config/schema_resolvers.go b/example/config/schema.resolvers.go similarity index 100% rename from example/config/schema_resolvers.go rename to example/config/schema.resolvers.go diff --git a/example/config/todo_resolvers.go b/example/config/todo.resolvers.go similarity index 100% rename from example/config/todo_resolvers.go rename to example/config/todo.resolvers.go diff --git a/plugin/resolvergen/resolver.go b/plugin/resolvergen/resolver.go index df403c828b..541d1a6470 100644 --- a/plugin/resolvergen/resolver.go +++ b/plugin/resolvergen/resolver.go @@ -143,8 +143,7 @@ func (m *Plugin) generatePerSchema(data *codegen.Data) error { } } - rootFilename := filepath.Join(data.Config.Resolver.Dir(), data.Config.Resolver.Filename) - if _, err := os.Stat(rootFilename); os.IsNotExist(errors.Cause(err)) { + if _, err := os.Stat(data.Config.Resolver.Filename); os.IsNotExist(errors.Cause(err)) { err := templates.Render(templates.Options{ PackageName: data.Config.Resolver.Package, PackageDoc: ` @@ -152,7 +151,7 @@ func (m *Plugin) generatePerSchema(data *codegen.Data) error { // // It serves as dependency injection for your app, add any dependencies you require here.`, Template: `type {{.}} struct {}`, - Filename: rootFilename, + Filename: data.Config.Resolver.Filename, Data: data.Config.Resolver.Type, }) if err != nil { @@ -199,5 +198,5 @@ func gqlToResolverName(base string, gqlname string) string { gqlname = filepath.Base(gqlname) ext := filepath.Ext(gqlname) - return filepath.Join(base, strings.TrimSuffix(gqlname, ext)+"_resolvers.go") + return filepath.Join(base, strings.TrimSuffix(gqlname, ext)+".resolvers.go") } diff --git a/plugin/resolvergen/resolver_test.go b/plugin/resolvergen/resolver_test.go index 6df6a266d4..07cc599538 100644 --- a/plugin/resolvergen/resolver_test.go +++ b/plugin/resolvergen/resolver_test.go @@ -43,7 +43,7 @@ func TestLayoutFollowSchema(t *testing.T) { require.NoError(t, p.GenerateCode(data)) assertNoErrors(t, "github.com/99designs/gqlgen/plugin/resolvergen/testdata/followschema/out") - b, err := ioutil.ReadFile("testdata/followschema/out/schema_resolvers.go") + b, err := ioutil.ReadFile("testdata/followschema/out/schema.resolvers.go") require.NoError(t, err) source := string(b) diff --git a/plugin/resolvergen/testdata/followschema/out/home/vektah/projects/99designs/gqlgen/plugin/resolvergen/testdata/followschema/out/resolver.go b/plugin/resolvergen/testdata/followschema/out/home/vektah/projects/99designs/gqlgen/plugin/resolvergen/testdata/followschema/out/resolver.go deleted file mode 100644 index 6129628811..0000000000 --- a/plugin/resolvergen/testdata/followschema/out/home/vektah/projects/99designs/gqlgen/plugin/resolvergen/testdata/followschema/out/resolver.go +++ /dev/null @@ -1,6 +0,0 @@ -// This file will not be regenerated automatically. -// -// It serves as dependency injection for your app, add any dependencies you require here. -package customresolver - -type CustomResolverType struct{} diff --git a/plugin/resolvergen/testdata/followschema/out/schema_resolvers.go b/plugin/resolvergen/testdata/followschema/out/schema.resolvers.go similarity index 100% rename from plugin/resolvergen/testdata/followschema/out/schema_resolvers.go rename to plugin/resolvergen/testdata/followschema/out/schema.resolvers.go diff --git a/plugin/servergen/server.gotpl b/plugin/servergen/server.gotpl index 85ec87fdbc..a3ae2a877a 100644 --- a/plugin/servergen/server.gotpl +++ b/plugin/servergen/server.gotpl @@ -2,6 +2,7 @@ {{ reserveImport "log" }} {{ reserveImport "net/http" }} {{ reserveImport "os" }} +{{ reserveImport "github.com/99designs/gqlgen/graphql/playground" }} {{ reserveImport "github.com/99designs/gqlgen/graphql/handler" }} const defaultPort = "8080" @@ -14,7 +15,7 @@ func main() { srv := handler.NewDefaultServer({{ lookupImport .ExecPackageName }}.NewExecutableSchema({{ lookupImport .ExecPackageName}}.Config{Resolvers: &{{ lookupImport .ResolverPackageName}}.Resolver{}})) - http.Handle("/", handler.Playground("GraphQL playground", "/query")) + http.Handle("/", playground.Handler("GraphQL playground", "/query")) http.Handle("/query", srv) log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)