diff --git a/.circleci/config.yml b/.circleci/config.yml index 9aae18f7534..61419caae94 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ jobs: build: docker: - image: golang:1.9 - working_directory: /go/src/github.com/vektah/gqlgen + working_directory: /go/src/github.com/99designs/gqlgen steps: &steps - checkout - run: > diff --git a/.gitignore b/.gitignore index ef6c1c9770e..c82d6fe033c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,7 @@ -/internal/tests/testdata/graphql-js /vendor /docs/public /example/chat/node_modules /example/chat/package-lock.json -/codegen/testdata/gen +/codegen/tests/gen .idea/ diff --git a/Gopkg.lock b/Gopkg.lock index f23744ee67e..70a1e151a3f 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1,6 +1,14 @@ # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. +[[projects]] + branch = "master" + digest = "1:99b2e09d42b4d0929e9e6ac496922f5221baa79b60968576d5ce310b44640c3b" + name = "github.com/agnivade/levenshtein" + packages = ["."] + pruneopts = "UT" + revision = "1787a73e302ce294513cfab1982e186f5cf8985f" + [[projects]] digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39" name = "github.com/davecgh/go-spew" @@ -52,6 +60,14 @@ revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b" version = "v1.2.0" +[[projects]] + digest = "1:870d441fe217b8e689d7949fef6e43efbc787e50f200cb1e70dbca9204a1d6be" + name = "github.com/inconshreveable/mousetrap" + packages = ["."] + pruneopts = "UT" + revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" + version = "v1.0" + [[projects]] branch = "master" digest = "1:ab9cfaf00fc5db5fd9d8e5f33da52e62bcc977d1976503dcc2a1492f391bd9ed" @@ -123,6 +139,22 @@ pruneopts = "UT" revision = "ffb13db8def02f545acc58bd288ec6057c2bbfb9" +[[projects]] + digest = "1:872fa275c31e1f9db31d66fa9b1d4a7bb9a080ff184e6977da01f36bfbe07f11" + name = "github.com/spf13/cobra" + packages = ["."] + pruneopts = "UT" + revision = "ef82de70bb3f60c65fb8eebacbb2d122ef517385" + version = "v0.0.3" + +[[projects]] + digest = "1:9424f440bba8f7508b69414634aef3b2b3a877e522d8a4624692412805407bb7" + name = "github.com/spf13/pflag" + packages = ["."] + pruneopts = "UT" + revision = "583c0c0531f06d5278b7d917446061adc344b5cd" + version = "v1.0.1" + [[projects]] digest = "1:73697231b93fb74a73ebd8384b68b9a60c57ea6b13c56d2425414566a72c8e6d" name = "github.com/stretchr/testify" @@ -142,6 +174,22 @@ pruneopts = "UT" revision = "314ac81052eedc03ac0a79bdc89d05a49a2a5814" +[[projects]] + branch = "master" + digest = "1:7acc3d2f02aed0095986646435472af4c2e2db42ad730aa78cae780aba5b59f9" + name = "github.com/vektah/gqlparser" + packages = [ + ".", + "ast", + "gqlerror", + "lexer", + "parser", + "validator", + "validator/rules", + ] + pruneopts = "UT" + revision = "6298a7d57d3de59879b323d6a8cb66280826906f" + [[projects]] branch = "master" digest = "1:76ee51c3f468493aff39dbacc401e8831fbb765104cbf613b89bef01cf4bad70" @@ -207,9 +255,14 @@ "github.com/opentracing/opentracing-go/ext", "github.com/opentracing/opentracing-go/log", "github.com/pkg/errors", + "github.com/spf13/cobra", "github.com/stretchr/testify/assert", "github.com/stretchr/testify/require", "github.com/vektah/dataloaden", + "github.com/vektah/gqlparser", + "github.com/vektah/gqlparser/ast", + "github.com/vektah/gqlparser/gqlerror", + "github.com/vektah/gqlparser/validator", "golang.org/x/tools/go/loader", "golang.org/x/tools/imports", "gopkg.in/yaml.v2", diff --git a/README.md b/README.md index a4ccac317b9..50948b88ba1 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,15 @@ See the [docs](https://gqlgen.com/) for a getting started guide. github.com/99designs/gqlgen soon. The next few weeks is going to see some heavy work pushing the new parser and directive support forward. Expect exciting things! -You can follow along on the [project board](https://github.com/vektah/gqlgen/projects/1). +You can follow along on the [project board](https://github.com/99designs/gqlgen/projects/1). ### Feature comparison -| | [gqlgen](https://github.com/vektah/gqlgen) | [gophers](https://github.com/graph-gophers/graphql-go) | [graphql-go](https://github.com/graphql-go/graphql) | [thunder](https://github.com/samsarahq/thunder) | +| | [gqlgen](https://github.com/99designs/gqlgen) | [gophers](https://github.com/graph-gophers/graphql-go) | [graphql-go](https://github.com/graphql-go/graphql) | [thunder](https://github.com/samsarahq/thunder) | | --------: | :-------- | :-------- | :-------- | :-------- | | Kind | schema first | schema first | run time types | struct first | | Boilerplate | less | more | more | some | -| Docs | [docs](https://gqlgen.com) & [examples](https://github.com/vektah/gqlgen/tree/master/example) | [examples](https://github.com/graph-gophers/graphql-go/tree/master/example/starwars) | [examples](https://github.com/graphql-go/graphql/tree/master/examples) | [examples](https://github.com/samsarahq/thunder/tree/master/example)| +| Docs | [docs](https://gqlgen.com) & [examples](https://github.com/99designs/gqlgen/tree/master/example) | [examples](https://github.com/graph-gophers/graphql-go/tree/master/example/starwars) | [examples](https://github.com/graphql-go/graphql/tree/master/examples) | [examples](https://github.com/samsarahq/thunder/tree/master/example)| | Query | :+1: | :+1: | :+1: | :+1: | | Mutation | :+1: | :construction: [pr](https://github.com/graph-gophers/graphql-go/pull/182) | :+1: | :+1: | | Subscription | :+1: | :construction: [pr](https://github.com/graph-gophers/graphql-go/pull/132) | :no_entry: [is](https://github.com/graphql-go/graphql/issues/207) | :+1: | @@ -28,7 +28,7 @@ You can follow along on the [project board](https://github.com/vektah/gqlgen/pro | Interfaces | :+1: | :+1: | :+1: | :no_entry: [is](https://github.com/samsarahq/thunder/issues/78) | | Generated Enums | :+1: | :no_entry: | :no_entry: | :no_entry: | | Generated Inputs | :+1: | :no_entry: | :no_entry: | :no_entry: | -| Stitching gql | :clock1: [is](https://github.com/vektah/gqlgen/issues/5) | :no_entry: | :no_entry: | :no_entry: | +| Stitching gql | :clock1: [is](https://github.com/99designs/gqlgen/issues/5) | :no_entry: | :no_entry: | :no_entry: | | Opentracing | :+1: | :+1: | :no_entry: | :scissors:[pr](https://github.com/samsarahq/thunder/pull/77) | | Hooks for error logging | :+1: | :no_entry: | :no_entry: | :no_entry: | | Dataloading | :+1: | :+1: | :no_entry: | :warning: | diff --git a/appveyor.yml b/appveyor.yml index fc40eb339eb..a07b3b78661 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,7 +3,7 @@ version: "{build}" # Source Config skip_branch_with_pr: true -clone_folder: c:\gopath\src\github.com\vektah\gqlgen +clone_folder: c:\gopath\src\github.com\99designs\gqlgen # Build host @@ -13,7 +13,7 @@ environment: PATH: '%PATH%;c:\gopath\bin' branches: - only: ["master"] + only: ["master", "next"] init: - git config --global core.autocrlf input diff --git a/client/client_test.go b/client/client_test.go index 333446993aa..af58feb140a 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -7,8 +7,8 @@ import ( "net/http/httptest" "testing" + "github.com/99designs/gqlgen/client" "github.com/stretchr/testify/require" - "github.com/vektah/gqlgen/client" ) func TestClient(t *testing.T) { diff --git a/client/websocket.go b/client/websocket.go index 555ddd5b9e6..e4e06051339 100644 --- a/client/websocket.go +++ b/client/websocket.go @@ -6,7 +6,7 @@ import ( "strings" "github.com/gorilla/websocket" - "github.com/vektah/gqlgen/neelance/errors" + "github.com/vektah/gqlparser/gqlerror" ) const ( @@ -83,7 +83,7 @@ func (p *Client) Websocket(query string, options ...Option) *Subscription { } if respDataRaw["errors"] != nil { - var errs []*errors.QueryError + var errs []*gqlerror.Error if err = unpack(respDataRaw["errors"], errs); err != nil { return err } diff --git a/cmd/gen.go b/cmd/gen.go new file mode 100644 index 00000000000..6a529ac5b5e --- /dev/null +++ b/cmd/gen.go @@ -0,0 +1,115 @@ +package cmd + +import ( + "fmt" + "io/ioutil" + "os" + + "github.com/99designs/gqlgen/codegen" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "gopkg.in/yaml.v2" +) + +func init() { + rootCmd.AddCommand(genCmd) +} + +var genCmd = &cobra.Command{ + Use: "gen", + Short: "Generate models & resolvers .go", + Long: "", + Run: func(cmd *cobra.Command, args []string) { + var config *codegen.Config + var err error + if configFilename != "" { + config, err = codegen.LoadConfig(configFilename) + if err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) + } + } else { + config, err = codegen.LoadConfigFromDefaultLocations() + if os.IsNotExist(errors.Cause(err)) { + config = codegen.DefaultConfig() + } else if err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) + } + } + + // overwrite by commandline options + var emitYamlGuidance bool + if schemaFilename != "" { + config.SchemaFilename = schemaFilename + } + if models != "" { + config.Model.Filename = models + } + if output != "" { + config.Exec.Filename = output + } + if packageName != "" { + config.Exec.Package = packageName + } + if modelPackageName != "" { + config.Model.Package = modelPackageName + } + if typemap != "" { + config.Models = loadModelMap() + emitYamlGuidance = true + } + + schemaRaw, err := ioutil.ReadFile(config.SchemaFilename) + if err != nil { + fmt.Fprintln(os.Stderr, "unable to open schema: "+err.Error()) + os.Exit(1) + } + config.SchemaStr = string(schemaRaw) + + if err = config.Check(); err != nil { + fmt.Fprintln(os.Stderr, "invalid config format: "+err.Error()) + os.Exit(1) + } + + if emitYamlGuidance { + var b []byte + b, err = yaml.Marshal(config) + if err != nil { + fmt.Fprintln(os.Stderr, "unable to marshal yaml: "+err.Error()) + os.Exit(1) + } + + fmt.Fprintf(os.Stderr, "DEPRECATION WARNING: we are moving away from the json typemap, instead create a gqlgen.yml with the following content:\n\n%s\n", string(b)) + } + + err = codegen.Generate(*config) + if err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(2) + } + }, +} + +func loadModelMap() codegen.TypeMap { + var goTypes map[string]string + b, err := ioutil.ReadFile(typemap) + if err != nil { + fmt.Fprintln(os.Stderr, "unable to open typemap: "+err.Error()) + return nil + } + + if err = yaml.Unmarshal(b, &goTypes); err != nil { + fmt.Fprintln(os.Stderr, "unable to parse typemap: "+err.Error()) + os.Exit(1) + } + + typeMap := make(codegen.TypeMap) + for typeName, entityPath := range goTypes { + typeMap[typeName] = codegen.TypeMapEntry{ + Model: entityPath, + } + } + + return typeMap +} diff --git a/cmd/init.go b/cmd/init.go new file mode 100644 index 00000000000..72b7c34f3e8 --- /dev/null +++ b/cmd/init.go @@ -0,0 +1,188 @@ +package cmd + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "strings" + + "github.com/99designs/gqlgen/codegen" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "gopkg.in/yaml.v2" +) + +func init() { + rootCmd.AddCommand(initCmd) +} + +var configComment = ` +# .gqlgen.yml example +# +# Refer to https://gqlgen.com/config/ +# for detailed .gqlgen.yml documentation. +` + +var schemaDefault = ` +# GraphQL schema example +# +# https://gqlgen.com/getting-started/ + +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! +} +` + +var initCmd = &cobra.Command{ + Use: "init", + Short: "Generate gqlgen skeleton", + Long: "", + Run: func(cmd *cobra.Command, args []string) { + initSchema() + config := initConfig() + + GenerateGraphServer(config) + }, +} + +func GenerateGraphServer(config *codegen.Config) { + schemaRaw, err := ioutil.ReadFile(config.SchemaFilename) + if err != nil { + fmt.Fprintln(os.Stderr, "unable to open schema: "+err.Error()) + os.Exit(1) + } + config.SchemaStr = string(schemaRaw) + + if err = config.Check(); err != nil { + fmt.Fprintln(os.Stderr, "invalid config format: "+err.Error()) + os.Exit(1) + } + + if serverFilename == "" { + serverFilename = "server/server.go" + } + + if err := codegen.Generate(*config); err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) + } + + if err := codegen.GenerateServer(*config, serverFilename); err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) + } + + fmt.Fprintln(os.Stdout, `Exec "go run ./server/server.go" to start GraphQL server`) +} + +func initConfig() *codegen.Config { + var config *codegen.Config + var err error + if configFilename != "" { + config, err = codegen.LoadConfig(configFilename) + if err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) + } else if config != nil { + fmt.Fprintln(os.Stderr, "config file already exists") + os.Exit(0) + } + } else { + config, err = codegen.LoadConfigFromDefaultLocations() + if os.IsNotExist(errors.Cause(err)) { + if configFilename == "" { + configFilename = "gqlgen.yml" + } + config = codegen.DefaultConfig() + config.Resolver = codegen.PackageConfig{ + Filename: "resolver.go", + Type: "Resolver", + } + } else if config != nil { + fmt.Fprintln(os.Stderr, "config file already exists") + os.Exit(0) + } else if err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) + } + } + + if schemaFilename != "" { + config.SchemaFilename = schemaFilename + } + if models != "" { + config.Model.Filename = models + } + if output != "" { + config.Exec.Filename = output + } + if packageName != "" { + config.Exec.Package = packageName + } + if modelPackageName != "" { + config.Model.Package = modelPackageName + } + if typemap != "" { + config.Models = loadModelMap() + } + + var buf bytes.Buffer + buf.WriteString(strings.TrimSpace(configComment)) + buf.WriteString("\n\n") + { + var b []byte + b, err = yaml.Marshal(config) + if err != nil { + fmt.Fprintln(os.Stderr, "unable to marshal yaml: "+err.Error()) + os.Exit(1) + } + buf.Write(b) + } + + err = ioutil.WriteFile(configFilename, buf.Bytes(), 0644) + if err != nil { + fmt.Fprintln(os.Stderr, "unable to write config file: "+err.Error()) + os.Exit(1) + } + + return config +} + +func initSchema() { + if schemaFilename == "" { + schemaFilename = "schema.graphql" + } + + _, err := os.Stat(schemaFilename) + if !os.IsNotExist(err) { + return + } + + err = ioutil.WriteFile(schemaFilename, []byte(strings.TrimSpace(schemaDefault)), 0644) + if err != nil { + fmt.Fprintln(os.Stderr, "unable to write schema file: "+err.Error()) + os.Exit(1) + } +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 00000000000..ddd02f18b40 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,56 @@ +package cmd + +import ( + "fmt" + "io/ioutil" + "log" + "os" + + "github.com/spf13/cobra" +) + +var configFilename string +var verbose bool + +var output string +var models string +var schemaFilename string +var typemap string +var packageName string +var modelPackageName string +var serverFilename string + +func init() { + rootCmd.PersistentFlags().StringVarP(&configFilename, "config", "c", "", "the file to configuration to") + rootCmd.PersistentFlags().StringVarP(&serverFilename, "server", "s", "", "the file to write server to") + rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "show logs") + + rootCmd.PersistentFlags().StringVar(&output, "out", "", "the file to write to") + rootCmd.PersistentFlags().StringVar(&models, "models", "", "the file to write the models to") + rootCmd.PersistentFlags().StringVar(&schemaFilename, "schema", "", "the graphql schema to generate types from") + rootCmd.PersistentFlags().StringVar(&typemap, "typemap", "", "a json map going from graphql to golang types") + rootCmd.PersistentFlags().StringVar(&packageName, "package", "", "the package name") + rootCmd.PersistentFlags().StringVar(&modelPackageName, "modelpackage", "", "the package name to use for models") +} + +var rootCmd = &cobra.Command{ + Use: "gqlgen", + Short: "go generate based graphql server library", + Long: `This is a library for quickly creating strictly typed graphql servers in golang. + See https://gqlgen.com/ for a getting started guide.`, + PersistentPreRun: func(cmd *cobra.Command, args []string) { + if verbose { + log.SetFlags(0) + } else { + log.SetOutput(ioutil.Discard) + } + }, + Run: genCmd.Run, // delegate to gen subcommand +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/codegen/build.go b/codegen/build.go index d56fc06f1f5..2f6f1e601e0 100644 --- a/codegen/build.go +++ b/codegen/build.go @@ -20,6 +20,8 @@ type Build struct { MutationRoot *Object SubscriptionRoot *Object SchemaRaw string + SchemaFilename string + Directives []*Directive } type ModelBuild struct { @@ -29,11 +31,27 @@ type ModelBuild struct { Enums []Enum } +type ResolverBuild struct { + PackageName string + Imports []*Import + ResolverType string + Objects Objects + ResolverFound bool +} + +type ServerBuild struct { + PackageName string + Imports []*Import + ExecPackageName string + ResolverPackageName string +} + // Create a list of models that need to be generated func (cfg *Config) models() (*ModelBuild, error) { namedTypes := cfg.buildNamedTypes() - prog, err := cfg.loadProgram(namedTypes, true) + progLoader := newLoader(namedTypes, true) + prog, err := progLoader.Load() if err != nil { return nil, errors.Wrap(err, "loading failed") } @@ -41,7 +59,7 @@ func (cfg *Config) models() (*ModelBuild, error) { cfg.bindTypes(imports, namedTypes, cfg.Model.Dir(), prog) - models, err := cfg.buildModels(namedTypes, prog) + models, err := cfg.buildModels(namedTypes, prog, imports) if err != nil { return nil, err } @@ -53,11 +71,60 @@ func (cfg *Config) models() (*ModelBuild, error) { }, nil } +// bind a schema together with some code to generate a Build +func (cfg *Config) resolver() (*ResolverBuild, error) { + progLoader := newLoader(cfg.buildNamedTypes(), true) + progLoader.Import(cfg.Resolver.ImportPath()) + + prog, err := progLoader.Load() + if err != nil { + return nil, err + } + + destDir := cfg.Resolver.Dir() + + namedTypes := cfg.buildNamedTypes() + imports := buildImports(namedTypes, destDir) + imports.add(cfg.Exec.ImportPath()) + + cfg.bindTypes(imports, namedTypes, destDir, prog) + + objects, err := cfg.buildObjects(namedTypes, prog, imports) + if err != nil { + return nil, err + } + + def, _ := findGoType(prog, cfg.Resolver.ImportPath(), cfg.Resolver.Type) + resolverFound := def != nil + + return &ResolverBuild{ + PackageName: cfg.Resolver.Package, + Imports: imports.finalize(), + Objects: objects, + ResolverType: cfg.Resolver.Type, + ResolverFound: resolverFound, + }, nil +} + +func (cfg *Config) server(destDir string) *ServerBuild { + imports := buildImports(NamedTypes{}, destDir) + imports.add(cfg.Exec.ImportPath()) + imports.add(cfg.Resolver.ImportPath()) + + return &ServerBuild{ + PackageName: cfg.Resolver.Package, + Imports: imports.finalize(), + ExecPackageName: cfg.Exec.Package, + ResolverPackageName: cfg.Resolver.Package, + } +} + // bind a schema together with some code to generate a Build func (cfg *Config) bind() (*Build, error) { namedTypes := cfg.buildNamedTypes() - prog, err := cfg.loadProgram(namedTypes, true) + progLoader := newLoader(namedTypes, true) + prog, err := progLoader.Load() if err != nil { return nil, errors.Wrap(err, "loading failed") } @@ -74,63 +141,45 @@ func (cfg *Config) bind() (*Build, error) { if err != nil { return nil, err } - - b := &Build{ - PackageName: cfg.Exec.Package, - Objects: objects, - Interfaces: cfg.buildInterfaces(namedTypes, prog), - Inputs: inputs, - Imports: imports.finalize(), - SchemaRaw: cfg.SchemaStr, + directives, err := cfg.buildDirectives(namedTypes) + if err != nil { + return nil, err } - if qr, ok := cfg.schema.EntryPoints["query"]; ok { - b.QueryRoot = b.Objects.ByName(qr.TypeName()) + b := &Build{ + PackageName: cfg.Exec.Package, + Objects: objects, + Interfaces: cfg.buildInterfaces(namedTypes, prog), + Inputs: inputs, + Imports: imports.finalize(), + SchemaRaw: cfg.SchemaStr, + SchemaFilename: cfg.SchemaFilename, + Directives: directives, } - if mr, ok := cfg.schema.EntryPoints["mutation"]; ok { - b.MutationRoot = b.Objects.ByName(mr.TypeName()) + if cfg.schema.Query != nil { + b.QueryRoot = b.Objects.ByName(cfg.schema.Query.Name) + } else { + return b, fmt.Errorf("query entry point missing") } - if sr, ok := cfg.schema.EntryPoints["subscription"]; ok { - b.SubscriptionRoot = b.Objects.ByName(sr.TypeName()) + if cfg.schema.Mutation != nil { + b.MutationRoot = b.Objects.ByName(cfg.schema.Mutation.Name) } - if b.QueryRoot == nil { - return b, fmt.Errorf("query entry point missing") + if cfg.schema.Subscription != nil { + b.SubscriptionRoot = b.Objects.ByName(cfg.schema.Subscription.Name) } - - // Poke a few magic methods into query - q := b.Objects.ByName(b.QueryRoot.GQLType) - q.Fields = append(q.Fields, Field{ - Type: &Type{namedTypes["__Schema"], []string{modPtr}, nil}, - GQLName: "__schema", - NoErr: true, - GoMethodName: "ec.introspectSchema", - Object: q, - }) - q.Fields = append(q.Fields, Field{ - Type: &Type{namedTypes["__Type"], []string{modPtr}, nil}, - GQLName: "__type", - NoErr: true, - GoMethodName: "ec.introspectType", - Args: []FieldArgument{ - {GQLName: "name", Type: &Type{namedTypes["String"], []string{}, nil}, Object: &Object{}}, - }, - Object: q, - }) - return b, nil } func (cfg *Config) validate() error { - namedTypes := cfg.buildNamedTypes() - - _, err := cfg.loadProgram(namedTypes, false) + progLoader := newLoader(cfg.buildNamedTypes(), false) + _, err := progLoader.Load() return err } -func (cfg *Config) loadProgram(namedTypes NamedTypes, allowErrors bool) (*loader.Program, error) { +func newLoader(namedTypes NamedTypes, allowErrors bool) loader.Config { conf := loader.Config{} if allowErrors { conf = loader.Config{ @@ -149,8 +198,7 @@ func (cfg *Config) loadProgram(namedTypes NamedTypes, allowErrors bool) (*loader conf.Import(imp.Package) } } - - return conf.Load() + return conf } func resolvePkg(pkgName string) (string, error) { diff --git a/codegen/codegen.go b/codegen/codegen.go index 789ef2ecb05..27873400857 100644 --- a/codegen/codegen.go +++ b/codegen/codegen.go @@ -1,18 +1,17 @@ package codegen import ( - "bytes" - "fmt" - "io/ioutil" + "log" "os" "path/filepath" "regexp" "syscall" + "github.com/99designs/gqlgen/codegen/templates" "github.com/pkg/errors" - "github.com/vektah/gqlgen/codegen/templates" - "github.com/vektah/gqlgen/neelance/schema" - "golang.org/x/tools/imports" + "github.com/vektah/gqlparser" + "github.com/vektah/gqlparser/ast" + "github.com/vektah/gqlparser/gqlerror" ) func Generate(cfg Config) error { @@ -28,15 +27,10 @@ func Generate(cfg Config) error { return errors.Wrap(err, "model plan failed") } if len(modelsBuild.Models) > 0 || len(modelsBuild.Enums) > 0 { - var buf *bytes.Buffer - buf, err = templates.Run("models.gotpl", modelsBuild) - if err != nil { - return errors.Wrap(err, "model generation failed") - } - - if err = write(cfg.Model.Filename, buf.Bytes()); err != nil { + if err = templates.RenderToFile("models.gotpl", cfg.Model.Filename, modelsBuild); err != nil { return err } + for _, model := range modelsBuild.Models { modelCfg := cfg.Models[model.GQLType] modelCfg.Model = cfg.Model.ImportPath() + "." + model.GoType @@ -55,23 +49,68 @@ func Generate(cfg Config) error { return errors.Wrap(err, "exec plan failed") } - var buf *bytes.Buffer - buf, err = templates.Run("generated.gotpl", build) - if err != nil { - return errors.Wrap(err, "exec codegen failed") + if err := templates.RenderToFile("generated.gotpl", cfg.Exec.Filename, build); err != nil { + return err } - if err = write(cfg.Exec.Filename, buf.Bytes()); err != nil { - return err + if cfg.Resolver.IsDefined() { + if err := generateResolver(cfg); err != nil { + return errors.Wrap(err, "generating resolver failed") + } } - if err = cfg.validate(); err != nil { + if err := cfg.validate(); err != nil { return errors.Wrap(err, "validation failed") } return nil } +func GenerateServer(cfg Config, filename string) error { + if err := cfg.Exec.normalize(); err != nil { + return errors.Wrap(err, "exec") + } + if err := cfg.Resolver.normalize(); err != nil { + return errors.Wrap(err, "resolver") + } + + serverFilename := abs(filename) + serverBuild := cfg.server(filepath.Dir(serverFilename)) + + if _, err := os.Stat(serverFilename); os.IsNotExist(errors.Cause(err)) { + err = templates.RenderToFile("server.gotpl", serverFilename, serverBuild) + if err != nil { + return errors.Wrap(err, "generate server failed") + } + } else { + log.Printf("Skipped server: %s already exists\n", serverFilename) + } + return nil +} + +func generateResolver(cfg Config) error { + resolverBuild, err := cfg.resolver() + if err != nil { + return errors.Wrap(err, "resolver build failed") + } + filename := cfg.Resolver.Filename + + if resolverBuild.ResolverFound { + log.Printf("Skipped resolver: %s.%s already exists\n", cfg.Resolver.ImportPath(), cfg.Resolver.Type) + return nil + } + + if _, err := os.Stat(filename); os.IsNotExist(errors.Cause(err)) { + if err := templates.RenderToFile("resolver.gotpl", filename, resolverBuild); err != nil { + return err + } + } else { + log.Printf("Skipped resolver: %s already exists\n", filename) + } + + return nil +} + func (cfg *Config) normalize() error { if err := cfg.Model.normalize(); err != nil { return errors.Wrap(err, "model") @@ -81,20 +120,26 @@ func (cfg *Config) normalize() error { return errors.Wrap(err, "exec") } + if cfg.Resolver.IsDefined() { + if err := cfg.Resolver.normalize(); err != nil { + return errors.Wrap(err, "resolver") + } + } + builtins := TypeMap{ - "__Directive": {Model: "github.com/vektah/gqlgen/neelance/introspection.Directive"}, - "__Type": {Model: "github.com/vektah/gqlgen/neelance/introspection.Type"}, - "__Field": {Model: "github.com/vektah/gqlgen/neelance/introspection.Field"}, - "__EnumValue": {Model: "github.com/vektah/gqlgen/neelance/introspection.EnumValue"}, - "__InputValue": {Model: "github.com/vektah/gqlgen/neelance/introspection.InputValue"}, - "__Schema": {Model: "github.com/vektah/gqlgen/neelance/introspection.Schema"}, - "Int": {Model: "github.com/vektah/gqlgen/graphql.Int"}, - "Float": {Model: "github.com/vektah/gqlgen/graphql.Float"}, - "String": {Model: "github.com/vektah/gqlgen/graphql.String"}, - "Boolean": {Model: "github.com/vektah/gqlgen/graphql.Boolean"}, - "ID": {Model: "github.com/vektah/gqlgen/graphql.ID"}, - "Time": {Model: "github.com/vektah/gqlgen/graphql.Time"}, - "Map": {Model: "github.com/vektah/gqlgen/graphql.Map"}, + "__Directive": {Model: "github.com/99designs/gqlgen/graphql/introspection.Directive"}, + "__Type": {Model: "github.com/99designs/gqlgen/graphql/introspection.Type"}, + "__Field": {Model: "github.com/99designs/gqlgen/graphql/introspection.Field"}, + "__EnumValue": {Model: "github.com/99designs/gqlgen/graphql/introspection.EnumValue"}, + "__InputValue": {Model: "github.com/99designs/gqlgen/graphql/introspection.InputValue"}, + "__Schema": {Model: "github.com/99designs/gqlgen/graphql/introspection.Schema"}, + "Int": {Model: "github.com/99designs/gqlgen/graphql.Int"}, + "Float": {Model: "github.com/99designs/gqlgen/graphql.Float"}, + "String": {Model: "github.com/99designs/gqlgen/graphql.String"}, + "Boolean": {Model: "github.com/99designs/gqlgen/graphql.Boolean"}, + "ID": {Model: "github.com/99designs/gqlgen/graphql.ID"}, + "Time": {Model: "github.com/99designs/gqlgen/graphql.Time"}, + "Map": {Model: "github.com/99designs/gqlgen/graphql.Map"}, } if cfg.Models == nil { @@ -106,8 +151,12 @@ func (cfg *Config) normalize() error { } } - cfg.schema = schema.New() - return cfg.schema.Parse(cfg.SchemaStr) + var err *gqlerror.Error + cfg.schema, err = gqlparser.LoadSchema(&ast.Source{Name: cfg.SchemaFilename, Input: cfg.SchemaStr}) + if err != nil { + return err + } + return nil } var invalidPackageNameChar = regexp.MustCompile(`[^\w]`) @@ -123,31 +172,3 @@ func abs(path string) string { } return filepath.ToSlash(absPath) } - -func gofmt(filename string, b []byte) ([]byte, error) { - out, err := imports.Process(filename, b, nil) - if err != nil { - return b, errors.Wrap(err, "unable to gofmt") - } - return out, nil -} - -func write(filename string, b []byte) error { - err := os.MkdirAll(filepath.Dir(filename), 0755) - if err != nil { - return errors.Wrap(err, "failed to create directory") - } - - formatted, err := gofmt(filename, b) - if err != nil { - fmt.Fprintf(os.Stderr, "gofmt failed: %s\n", err.Error()) - formatted = b - } - - err = ioutil.WriteFile(filename, formatted, 0644) - if err != nil { - return errors.Wrapf(err, "failed to write %s", filename) - } - - return nil -} diff --git a/codegen/codegen_test.go b/codegen/codegen_test.go new file mode 100644 index 00000000000..0c00888db95 --- /dev/null +++ b/codegen/codegen_test.go @@ -0,0 +1,43 @@ +package codegen + +import ( + "syscall" + "testing" + + "github.com/stretchr/testify/require" + "golang.org/x/tools/go/loader" +) + +func TestGenerateServer(t *testing.T) { + name := "graphserver" + schema := ` + type Query { + user(): User + } + type User { + id: Int + } +` + serverFilename := "tests/gen/" + name + "/server/server.go" + cfg := Config{ + SchemaStr: schema, + Exec: PackageConfig{Filename: "tests/gen/" + name + "/exec.go"}, + Model: PackageConfig{Filename: "tests/gen/" + name + "/model.go"}, + Resolver: PackageConfig{Filename: "tests/gen/" + name + "/resolver.go", Type: "Resolver"}, + } + + _ = syscall.Unlink(cfg.Resolver.Filename) + _ = syscall.Unlink(serverFilename) + + err := Generate(cfg) + require.NoError(t, err) + + err = GenerateServer(cfg, serverFilename) + require.NoError(t, err) + + conf := loader.Config{} + conf.CreateFromFilenames("tests/gen/"+name, serverFilename) + + _, err = conf.Load() + require.NoError(t, err) +} diff --git a/codegen/config.go b/codegen/config.go index cd42ae6bfc4..0b6f98da435 100644 --- a/codegen/config.go +++ b/codegen/config.go @@ -9,25 +9,27 @@ import ( "strings" "github.com/pkg/errors" - "github.com/vektah/gqlgen/neelance/schema" + "github.com/vektah/gqlparser/ast" "gopkg.in/yaml.v2" ) -var defaults = Config{ - SchemaFilename: "schema.graphql", - Model: PackageConfig{Filename: "models_gen.go"}, - Exec: PackageConfig{Filename: "generated.go"}, -} - var cfgFilenames = []string{".gqlgen.yml", "gqlgen.yml", "gqlgen.yaml"} -// LoadDefaultConfig looks for a config file in the current directory, and all parent directories +// DefaultConfig creates a copy of the default config +func DefaultConfig() *Config { + return &Config{ + SchemaFilename: "schema.graphql", + Model: PackageConfig{Filename: "models_gen.go"}, + Exec: PackageConfig{Filename: "generated.go"}, + } +} + +// LoadConfigFromDefaultLocations looks for a config file in the current directory, and all parent directories // walking up the tree. The closest config file will be returned. -func LoadDefaultConfig() (*Config, error) { +func LoadConfigFromDefaultLocations() (*Config, error) { cfgFile, err := findCfg() - if err != nil || cfgFile == "" { - cpy := defaults - return &cpy, err + if err != nil { + return nil, err } err = os.Chdir(filepath.Dir(cfgFile)) @@ -39,18 +41,18 @@ func LoadDefaultConfig() (*Config, error) { // LoadConfig reads the gqlgen.yml config file func LoadConfig(filename string) (*Config, error) { - config := defaults + config := DefaultConfig() b, err := ioutil.ReadFile(filename) if err != nil { return nil, errors.Wrap(err, "unable to read config") } - if err := yaml.UnmarshalStrict(b, &config); err != nil { + if err := yaml.UnmarshalStrict(b, config); err != nil { return nil, errors.Wrap(err, "unable to parse config") } - return &config, nil + return config, nil } type Config struct { @@ -58,14 +60,16 @@ type Config struct { SchemaStr string `yaml:"-"` Exec PackageConfig `yaml:"exec"` Model PackageConfig `yaml:"model"` + Resolver PackageConfig `yaml:"resolver,omitempty"` Models TypeMap `yaml:"models,omitempty"` - schema *schema.Schema `yaml:"-"` + schema *ast.Schema `yaml:"-"` } type PackageConfig struct { Filename string `yaml:"filename,omitempty"` Package string `yaml:"package,omitempty"` + Type string `yaml:"type,omitempty"` } type TypeMapEntry struct { @@ -74,7 +78,8 @@ type TypeMapEntry struct { } type TypeMapField struct { - Resolver bool `yaml:"resolver"` + Resolver bool `yaml:"resolver"` + FieldName string `yaml:"fieldName"` } func (c *PackageConfig) normalize() error { @@ -126,6 +131,10 @@ func (c *PackageConfig) Check() error { return nil } +func (c *PackageConfig) IsDefined() bool { + return c.Filename != "" +} + func (cfg *Config) Check() error { if err := cfg.Models.Check(); err != nil { return errors.Wrap(err, "config.models") @@ -136,6 +145,9 @@ func (cfg *Config) Check() error { if err := cfg.Model.Check(); err != nil { return errors.Wrap(err, "config.model") } + if err := cfg.Resolver.Check(); err != nil { + return errors.Wrap(err, "config.resolver") + } return nil } @@ -170,6 +182,10 @@ func findCfg() (string, error) { cfg = findCfgInDir(dir) } + if cfg == "" { + return "", os.ErrNotExist + } + return cfg, nil } diff --git a/codegen/config_test.go b/codegen/config_test.go index e4161d9c31d..ac82eecb01a 100644 --- a/codegen/config_test.go +++ b/codegen/config_test.go @@ -17,12 +17,12 @@ func TestLoadConfig(t *testing.T) { }) t.Run("malformed config", func(t *testing.T) { - _, err := LoadConfig("testdata/cfg/malformedconfig.yml") + _, err := LoadConfig("tests/cfg/malformedconfig.yml") require.EqualError(t, err, "unable to parse config: yaml: unmarshal errors:\n line 1: cannot unmarshal !!str `asdf` into codegen.Config") }) t.Run("unknown keys", func(t *testing.T) { - _, err := LoadConfig("testdata/cfg/unknownkeys.yml") + _, err := LoadConfig("tests/cfg/unknownkeys.yml") require.EqualError(t, err, "unable to parse config: yaml: unmarshal errors:\n line 2: field unknown not found in type codegen.Config") }) } @@ -33,30 +33,29 @@ func TestLoadDefaultConfig(t *testing.T) { var cfg *Config t.Run("will find closest match", func(t *testing.T) { - err = os.Chdir(filepath.Join(testDir, "testdata", "cfg", "subdir")) + err = os.Chdir(filepath.Join(testDir, "tests", "cfg", "subdir")) require.NoError(t, err) - cfg, err = LoadDefaultConfig() + cfg, err = LoadConfigFromDefaultLocations() require.NoError(t, err) require.Equal(t, cfg.SchemaFilename, "inner") }) t.Run("will find config in parent dirs", func(t *testing.T) { - err = os.Chdir(filepath.Join(testDir, "testdata", "cfg", "otherdir")) + err = os.Chdir(filepath.Join(testDir, "tests", "cfg", "otherdir")) require.NoError(t, err) - cfg, err = LoadDefaultConfig() + cfg, err = LoadConfigFromDefaultLocations() require.NoError(t, err) require.Equal(t, cfg.SchemaFilename, "outer") }) - t.Run("will fallback to defaults", func(t *testing.T) { + t.Run("will return error if config doesn't exist", func(t *testing.T) { err = os.Chdir(testDir) require.NoError(t, err) - cfg, err = LoadDefaultConfig() - require.NoError(t, err) - require.Equal(t, cfg.SchemaFilename, "schema.graphql") + cfg, err = LoadConfigFromDefaultLocations() + require.True(t, os.IsNotExist(err)) }) } diff --git a/codegen/directive.go b/codegen/directive.go new file mode 100644 index 00000000000..ee49788bc2f --- /dev/null +++ b/codegen/directive.go @@ -0,0 +1,33 @@ +package codegen + +import ( + "fmt" + "strconv" + "strings" +) + +type Directive struct { + Name string + Args []FieldArgument +} + +func (d *Directive) CallArgs() string { + args := []string{"ctx", "n"} + + for _, arg := range d.Args { + args = append(args, "args["+strconv.Quote(arg.GQLName)+"].("+arg.Signature()+")") + } + + return strings.Join(args, ", ") +} + +func (d *Directive) Declaration() string { + res := ucFirst(d.Name) + " func(ctx context.Context, next graphql.Resolver" + + for _, arg := range d.Args { + res += fmt.Sprintf(", %s %s", arg.GoVarName, arg.Signature()) + } + + res += ") (res interface{}, err error)" + return res +} diff --git a/codegen/directive_build.go b/codegen/directive_build.go new file mode 100644 index 00000000000..e95a0e6690e --- /dev/null +++ b/codegen/directive_build.go @@ -0,0 +1,42 @@ +package codegen + +import "github.com/pkg/errors" + +func (cfg *Config) buildDirectives(types NamedTypes) ([]*Directive, error) { + var directives []*Directive + + for name, dir := range cfg.schema.Directives { + if name == "skip" || name == "include" || name == "deprecated" { + continue + } + + var args []FieldArgument + for _, arg := range dir.Arguments { + newArg := FieldArgument{ + GQLName: arg.Name, + Type: types.getType(arg.Type), + GoVarName: sanitizeGoName(arg.Name), + } + + if !newArg.Type.IsInput && !newArg.Type.IsScalar { + return nil, errors.Errorf("%s cannot be used as argument of directive %s(%s) only input and scalar types are allowed", arg.Type, dir.Name, arg.Name) + } + + if arg.DefaultValue != nil { + var err error + newArg.Default, err = arg.DefaultValue.Value(nil) + if err != nil { + return nil, errors.Errorf("default value for directive argument %s(%s) is not valid: %s", dir.Name, arg.Name, err.Error()) + } + newArg.StripPtr() + } + args = append(args, newArg) + } + + directives = append(directives, &Directive{ + Name: name, + Args: args, + }) + } + return directives, nil +} diff --git a/codegen/enum_build.go b/codegen/enum_build.go index f2e6f63cb44..d5e82c24764 100644 --- a/codegen/enum_build.go +++ b/codegen/enum_build.go @@ -4,23 +4,22 @@ import ( "sort" "strings" - "github.com/vektah/gqlgen/codegen/templates" - "github.com/vektah/gqlgen/neelance/schema" + "github.com/99designs/gqlgen/codegen/templates" + "github.com/vektah/gqlparser/ast" ) func (cfg *Config) buildEnums(types NamedTypes) []Enum { var enums []Enum for _, typ := range cfg.schema.Types { - namedType := types[typ.TypeName()] - e, isEnum := typ.(*schema.Enum) - if !isEnum || strings.HasPrefix(typ.TypeName(), "__") || namedType.IsUserDefined { + namedType := types[typ.Name] + if typ.Kind != ast.Enum || strings.HasPrefix(typ.Name, "__") || namedType.IsUserDefined { continue } var values []EnumValue - for _, v := range e.Values { - values = append(values, EnumValue{v.Name, v.Desc}) + for _, v := range typ.EnumValues { + values = append(values, EnumValue{v.Name, v.Description}) } enum := Enum{ diff --git a/codegen/import_build.go b/codegen/import_build.go index f0877ed3d42..3bad3a26fd2 100644 --- a/codegen/import_build.go +++ b/codegen/import_build.go @@ -18,12 +18,12 @@ var ambientImports = []string{ "strconv", "time", "sync", - "github.com/vektah/gqlgen/neelance/introspection", - "github.com/vektah/gqlgen/neelance/errors", - "github.com/vektah/gqlgen/neelance/query", - "github.com/vektah/gqlgen/neelance/schema", - "github.com/vektah/gqlgen/neelance/validation", - "github.com/vektah/gqlgen/graphql", + "errors", + + "github.com/vektah/gqlparser", + "github.com/vektah/gqlparser/ast", + "github.com/99designs/gqlgen/graphql", + "github.com/99designs/gqlgen/graphql/introspection", } func buildImports(types NamedTypes, destDir string) *Imports { diff --git a/codegen/import_test.go b/codegen/import_test.go index 0e55887430e..133049dc55f 100644 --- a/codegen/import_test.go +++ b/codegen/import_test.go @@ -15,7 +15,7 @@ func TestInvalidPackagenames(t *testing.T) { id: Int! } `, TypeMap{ - "InvalidIdentifier": {Model: "github.com/vektah/gqlgen/codegen/testdata/invalid-packagename.InvalidIdentifier"}, + "InvalidIdentifier": {Model: "github.com/99designs/gqlgen/codegen/tests/invalid-packagename.InvalidIdentifier"}, }) require.NoError(t, err) @@ -31,7 +31,7 @@ func TestImportCollisions(t *testing.T) { } `, TypeMap{ - "It": {Model: "github.com/vektah/gqlgen/codegen/testdata/introspection.It"}, + "It": {Model: "github.com/99designs/gqlgen/codegen/tests/introspection.It"}, }) require.NoError(t, err) diff --git a/codegen/input_build.go b/codegen/input_build.go index 98b25b8b21d..c333201522d 100644 --- a/codegen/input_build.go +++ b/codegen/input_build.go @@ -6,7 +6,7 @@ import ( "strings" "github.com/pkg/errors" - "github.com/vektah/gqlgen/neelance/schema" + "github.com/vektah/gqlparser/ast" "golang.org/x/tools/go/loader" ) @@ -14,9 +14,9 @@ func (cfg *Config) buildInputs(namedTypes NamedTypes, prog *loader.Program, impo var inputs Objects for _, typ := range cfg.schema.Types { - switch typ := typ.(type) { - case *schema.InputObject: - input, err := buildInput(namedTypes, typ) + switch typ.Kind { + case ast.InputObject: + input, err := cfg.buildInput(namedTypes, typ) if err != nil { return nil, err } @@ -44,18 +44,29 @@ func (cfg *Config) buildInputs(namedTypes NamedTypes, prog *loader.Program, impo return inputs, nil } -func buildInput(types NamedTypes, typ *schema.InputObject) (*Object, error) { - obj := &Object{NamedType: types[typ.TypeName()]} +func (cfg *Config) buildInput(types NamedTypes, typ *ast.Definition) (*Object, error) { + obj := &Object{NamedType: types[typ.Name]} + typeEntry, entryExists := cfg.Models[typ.Name] - for _, field := range typ.Values { + for _, field := range typ.Fields { newField := Field{ - GQLName: field.Name.Name, + GQLName: field.Name, Type: types.getType(field.Type), Object: obj, } - if field.Default != nil { - newField.Default = field.Default.Value(nil) + if entryExists { + if typeField, ok := typeEntry.Fields[field.Name]; ok { + newField.GoFieldName = typeField.FieldName + } + } + + if field.DefaultValue != nil { + var err error + newField.Default, err = field.DefaultValue.Value(nil) + if err != nil { + return nil, errors.Errorf("default value for %s.%s is not valid: %s", typ.Name, field.Name, err.Error()) + } } if !newField.Type.IsInput && !newField.Type.IsScalar { @@ -70,7 +81,7 @@ func buildInput(types NamedTypes, typ *schema.InputObject) (*Object, error) { // if user has implemented an UnmarshalGQL method on the input type manually, use it // otherwise we will generate one. -func buildInputMarshaler(typ *schema.InputObject, def types.Object) *Ref { +func buildInputMarshaler(typ *ast.Definition, def types.Object) *Ref { switch def := def.(type) { case *types.TypeName: namedType := def.Type().(*types.Named) diff --git a/codegen/input_test.go b/codegen/input_test.go index 62e157b1a69..be2e7da2a6e 100644 --- a/codegen/input_test.go +++ b/codegen/input_test.go @@ -57,7 +57,7 @@ func TestRecursiveInputType(t *testing.T) { self: [RecursiveInputSlice!] } `, TypeMap{ - "RecursiveInputSlice": {Model: "github.com/vektah/gqlgen/codegen/testdata.RecursiveInputSlice"}, + "RecursiveInputSlice": {Model: "github.com/99designs/gqlgen/codegen/tests.RecursiveInputSlice"}, }) require.NoError(t, err) diff --git a/codegen/interface_build.go b/codegen/interface_build.go index cdf0f59724b..5ffcbcedab8 100644 --- a/codegen/interface_build.go +++ b/codegen/interface_build.go @@ -7,18 +7,15 @@ import ( "sort" "strings" - "github.com/vektah/gqlgen/neelance/schema" + "github.com/vektah/gqlparser/ast" "golang.org/x/tools/go/loader" ) func (cfg *Config) buildInterfaces(types NamedTypes, prog *loader.Program) []*Interface { var interfaces []*Interface for _, typ := range cfg.schema.Types { - switch typ := typ.(type) { - case *schema.Union, *schema.Interface: + if typ.Kind == ast.Union || typ.Kind == ast.Interface { interfaces = append(interfaces, cfg.buildInterface(types, typ, prog)) - default: - continue } } @@ -29,39 +26,19 @@ func (cfg *Config) buildInterfaces(types NamedTypes, prog *loader.Program) []*In return interfaces } -func (cfg *Config) buildInterface(types NamedTypes, typ schema.NamedType, prog *loader.Program) *Interface { - switch typ := typ.(type) { +func (cfg *Config) buildInterface(types NamedTypes, typ *ast.Definition, prog *loader.Program) *Interface { + i := &Interface{NamedType: types[typ.Name]} - case *schema.Union: - i := &Interface{NamedType: types[typ.TypeName()]} + for _, implementor := range cfg.schema.GetPossibleTypes(typ) { + t := types[implementor.Name] - for _, implementor := range typ.PossibleTypes { - t := types[implementor.TypeName()] - - i.Implementors = append(i.Implementors, InterfaceImplementor{ - NamedType: t, - ValueReceiver: cfg.isValueReceiver(types[typ.Name], t, prog), - }) - } - - return i - - case *schema.Interface: - i := &Interface{NamedType: types[typ.TypeName()]} - - for _, implementor := range typ.PossibleTypes { - t := types[implementor.TypeName()] - - i.Implementors = append(i.Implementors, InterfaceImplementor{ - NamedType: t, - ValueReceiver: cfg.isValueReceiver(types[typ.Name], t, prog), - }) - } - - return i - default: - panic(fmt.Errorf("unknown interface %#v", typ)) + i.Implementors = append(i.Implementors, InterfaceImplementor{ + NamedType: t, + ValueReceiver: cfg.isValueReceiver(types[typ.Name], t, prog), + }) } + + return i } func (cfg *Config) isValueReceiver(intf *NamedType, implementor *NamedType, prog *loader.Program) bool { diff --git a/codegen/interface_test.go b/codegen/interface_test.go index 175f53c6b2e..184186f502c 100644 --- a/codegen/interface_test.go +++ b/codegen/interface_test.go @@ -3,6 +3,8 @@ package codegen import ( "testing" + "syscall" + "github.com/stretchr/testify/require" "golang.org/x/tools/go/loader" ) @@ -26,10 +28,10 @@ func TestShapes(t *testing.T) { } union ShapeUnion = Circle | Rectangle `, TypeMap{ - "Shape": {Model: "github.com/vektah/gqlgen/codegen/testdata.Shape"}, - "ShapeUnion": {Model: "github.com/vektah/gqlgen/codegen/testdata.ShapeUnion"}, - "Circle": {Model: "github.com/vektah/gqlgen/codegen/testdata.Circle"}, - "Rectangle": {Model: "github.com/vektah/gqlgen/codegen/testdata.Rectangle"}, + "Shape": {Model: "github.com/99designs/gqlgen/codegen/tests.Shape"}, + "ShapeUnion": {Model: "github.com/99designs/gqlgen/codegen/tests.ShapeUnion"}, + "Circle": {Model: "github.com/99designs/gqlgen/codegen/tests.Circle"}, + "Rectangle": {Model: "github.com/99designs/gqlgen/codegen/tests.Rectangle"}, }) require.NoError(t, err) @@ -39,16 +41,20 @@ func TestShapes(t *testing.T) { func generate(name string, schema string, typemap ...TypeMap) error { cfg := Config{ SchemaStr: schema, - Exec: PackageConfig{Filename: "testdata/gen/" + name + "/exec.go"}, - Model: PackageConfig{Filename: "testdata/gen/" + name + "/model.go"}, + Exec: PackageConfig{Filename: "tests/gen/" + name + "/exec.go"}, + Model: PackageConfig{Filename: "tests/gen/" + name + "/model.go"}, + Resolver: PackageConfig{Filename: "tests/gen/" + name + "/resolver.go", Type: "Resolver"}, } + + _ = syscall.Unlink(cfg.Resolver.Filename) + if len(typemap) > 0 { cfg.Models = typemap[0] } err := Generate(cfg) if err == nil { conf := loader.Config{} - conf.Import("github.com/vektah/gqlgen/codegen/testdata/gen/" + name) + conf.Import("github.com/99designs/gqlgen/codegen/tests/gen/" + name) _, err = conf.Load() if err != nil { diff --git a/codegen/model.go b/codegen/model.go index 164a04d5b50..7b46a318c2b 100644 --- a/codegen/model.go +++ b/codegen/model.go @@ -8,8 +8,8 @@ type Model struct { type ModelField struct { *Type - GQLName string - GoVarName string - GoFKName string - GoFKType string + GQLName string + GoFieldName string + GoFKName string + GoFKType string } diff --git a/codegen/models_build.go b/codegen/models_build.go index 211d4bd4aa2..6b6ddb6fb61 100644 --- a/codegen/models_build.go +++ b/codegen/models_build.go @@ -4,18 +4,18 @@ import ( "sort" "strings" - "github.com/vektah/gqlgen/neelance/schema" + "github.com/vektah/gqlparser/ast" "golang.org/x/tools/go/loader" ) -func (cfg *Config) buildModels(types NamedTypes, prog *loader.Program) ([]Model, error) { +func (cfg *Config) buildModels(types NamedTypes, prog *loader.Program, imports *Imports) ([]Model, error) { var models []Model for _, typ := range cfg.schema.Types { var model Model - switch typ := typ.(type) { - case *schema.Object: - obj, err := cfg.buildObject(types, typ) + switch typ.Kind { + case ast.Object: + obj, err := cfg.buildObject(types, typ, imports) if err != nil { return nil, err } @@ -23,8 +23,8 @@ func (cfg *Config) buildModels(types NamedTypes, prog *loader.Program) ([]Model, continue } model = cfg.obj2Model(obj) - case *schema.InputObject: - obj, err := buildInput(types, typ) + case ast.InputObject: + obj, err := cfg.buildInput(types, typ) if err != nil { return nil, err } @@ -32,7 +32,7 @@ func (cfg *Config) buildModels(types NamedTypes, prog *loader.Program) ([]Model, continue } model = cfg.obj2Model(obj) - case *schema.Interface, *schema.Union: + case ast.Interface, ast.Union: intf := cfg.buildInterface(types, typ, prog) if intf.IsUserDefined { continue @@ -65,11 +65,10 @@ func (cfg *Config) obj2Model(obj *Object) Model { field := &obj.Fields[i] mf := ModelField{Type: field.Type, GQLName: field.GQLName} - mf.GoVarName = ucFirst(field.GQLName) - if mf.IsScalar { - if mf.GoVarName == "Id" { - mf.GoVarName = "ID" - } + if field.GoFieldName != "" { + mf.GoFieldName = field.GoFieldName + } else { + mf.GoFieldName = field.GoNameExported() } model.Fields = append(model.Fields, mf) diff --git a/codegen/object.go b/codegen/object.go index 1c03c0bae3d..c9cf52ddbe1 100644 --- a/codegen/object.go +++ b/codegen/object.go @@ -9,11 +9,20 @@ import ( "unicode" ) +type GoFieldType int + +const ( + GoFieldUndefined GoFieldType = iota + GoFieldMethod + GoFieldVariable +) + type Object struct { *NamedType Fields []Field Satisfies []string + ResolverInterface *Ref Root bool DisableConcurrency bool Stream bool @@ -22,14 +31,15 @@ type Object struct { type Field struct { *Type - GQLName string // The name of the field in graphql - GoMethodName string // The name of the method in go, if any - GoVarName string // The name of the var in go, if any - Args []FieldArgument // A list of arguments to be passed to this field - ForceResolver bool // Should be emit Resolver method - NoErr bool // If this is bound to a go method, does that method have an error as the second argument - Object *Object // A link back to the parent object - Default interface{} // The default value + GQLName string // The name of the field in graphql + GoFieldType GoFieldType // The field type in go, if any + GoReceiverName string // The name of method & var receiver in go, if any + GoFieldName string // The name of the method or var in go, if any + Args []FieldArgument // A list of arguments to be passed to this field + ForceResolver bool // Should be emit Resolver method + NoErr bool // If this is bound to a go method, does that method have an error as the second argument + Object *Object // A link back to the parent object + Default interface{} // The default value } type FieldArgument struct { @@ -61,40 +71,72 @@ func (o *Object) HasResolvers() bool { } func (f *Field) IsResolver() bool { - return f.ForceResolver || f.GoMethodName == "" && f.GoVarName == "" + return f.ForceResolver || f.GoFieldName == "" +} + +func (f *Field) IsMethod() bool { + return f.GoFieldType == GoFieldMethod +} + +func (f *Field) IsVariable() bool { + return f.GoFieldType == GoFieldVariable } func (f *Field) IsConcurrent() bool { return f.IsResolver() && !f.Object.DisableConcurrency } + +func (f *Field) GoNameExported() string { + return lintName(ucFirst(f.GQLName)) +} + +func (f *Field) GoNameUnexported() string { + return lintName(f.GQLName) +} + func (f *Field) ShortInvocation() string { if !f.IsResolver() { return "" } - shortName := strings.ToUpper(f.GQLName[:1]) + f.GQLName[1:] - res := fmt.Sprintf("%s().%s(ctx", f.Object.GQLType, shortName) - if !f.Object.Root { - res += fmt.Sprintf(", obj") - } - for _, arg := range f.Args { - res += fmt.Sprintf(", %s", arg.GoVarName) + + return fmt.Sprintf("%s().%s(%s)", f.Object.GQLType, f.GoNameExported(), f.CallArgs()) +} + +func (f *Field) ResolverType() string { + if !f.IsResolver() { + return "" } - res += ")" - return res + + return fmt.Sprintf("%s().%s(%s)", f.Object.GQLType, f.GoNameExported(), f.CallArgs()) } + func (f *Field) ShortResolverDeclaration() string { if !f.IsResolver() { return "" } - decl := strings.TrimPrefix(f.ResolverDeclaration(), f.Object.GQLType+"_") - return strings.ToUpper(decl[:1]) + decl[1:] + res := fmt.Sprintf("%s(ctx context.Context", f.GoNameExported()) + + if !f.Object.Root { + res += fmt.Sprintf(", obj *%s", f.Object.FullName()) + } + for _, arg := range f.Args { + res += fmt.Sprintf(", %s %s", arg.GoVarName, arg.Signature()) + } + + result := f.Signature() + if f.Object.Stream { + result = "<-chan " + result + } + + res += fmt.Sprintf(") (%s, error)", result) + return res } func (f *Field) ResolverDeclaration() string { if !f.IsResolver() { return "" } - res := fmt.Sprintf("%s_%s(ctx context.Context", f.Object.GQLType, f.GQLName) + res := fmt.Sprintf("%s_%s(ctx context.Context", f.Object.GQLType, f.GoNameUnexported()) if !f.Object.Root { res += fmt.Sprintf(", obj *%s", f.Object.FullName()) @@ -115,7 +157,7 @@ func (f *Field) ResolverDeclaration() string { func (f *Field) CallArgs() string { var args []string - if f.GoMethodName == "" { + if f.IsResolver() { args = append(args, "ctx") if !f.Object.Root { @@ -177,6 +219,10 @@ func (f *Field) doWriteJson(val string, remainingMods []string, isPtr bool, dept } } +func (f *FieldArgument) Stream() bool { + return f.Object != nil && f.Object.Stream +} + func (os Objects) ByName(name string) *Object { for i, o := range os { if strings.EqualFold(o.GQLType, name) { @@ -204,3 +250,117 @@ func ucFirst(s string) string { r[0] = unicode.ToUpper(r[0]) return string(r) } + +// copy from https://github.com/golang/lint/blob/06c8688daad7faa9da5a0c2f163a3d14aac986ca/lint.go#L679 + +// lintName returns a different name if it should be different. +func lintName(name string) (should string) { + // Fast path for simple cases: "_" and all lowercase. + if name == "_" { + return name + } + allLower := true + for _, r := range name { + if !unicode.IsLower(r) { + allLower = false + break + } + } + if allLower { + return name + } + + // Split camelCase at any lower->upper transition, and split on underscores. + // Check each word for common initialisms. + runes := []rune(name) + w, i := 0, 0 // index of start of word, scan + for i+1 <= len(runes) { + eow := false // whether we hit the end of a word + if i+1 == len(runes) { + eow = true + } else if runes[i+1] == '_' { + // underscore; shift the remainder forward over any run of underscores + eow = true + n := 1 + for i+n+1 < len(runes) && runes[i+n+1] == '_' { + n++ + } + + // Leave at most one underscore if the underscore is between two digits + if i+n+1 < len(runes) && unicode.IsDigit(runes[i]) && unicode.IsDigit(runes[i+n+1]) { + n-- + } + + copy(runes[i+1:], runes[i+n+1:]) + runes = runes[:len(runes)-n] + } else if unicode.IsLower(runes[i]) && !unicode.IsLower(runes[i+1]) { + // lower->non-lower + eow = true + } + i++ + if !eow { + continue + } + + // [w,i) is a word. + word := string(runes[w:i]) + if u := strings.ToUpper(word); commonInitialisms[u] { + // Keep consistent case, which is lowercase only at the start. + if w == 0 && unicode.IsLower(runes[w]) { + u = strings.ToLower(u) + } + // All the common initialisms are ASCII, + // so we can replace the bytes exactly. + copy(runes[w:], []rune(u)) + } else if w > 0 && strings.ToLower(word) == word { + // already all lowercase, and not the first word, so uppercase the first character. + runes[w] = unicode.ToUpper(runes[w]) + } + w = i + } + return string(runes) +} + +// commonInitialisms is a set of common initialisms. +// Only add entries that are highly unlikely to be non-initialisms. +// For instance, "ID" is fine (Freudian code is rare), but "AND" is not. +var commonInitialisms = map[string]bool{ + "ACL": true, + "API": true, + "ASCII": true, + "CPU": true, + "CSS": true, + "DNS": true, + "EOF": true, + "GUID": true, + "HTML": true, + "HTTP": true, + "HTTPS": true, + "ID": true, + "IP": true, + "JSON": true, + "LHS": true, + "QPS": true, + "RAM": true, + "RHS": true, + "RPC": true, + "SLA": true, + "SMTP": true, + "SQL": true, + "SSH": true, + "TCP": true, + "TLS": true, + "TTL": true, + "UDP": true, + "UI": true, + "UID": true, + "UUID": true, + "URI": true, + "URL": true, + "UTF8": true, + "VM": true, + "XML": true, + "XMPP": true, + "XSRF": true, + "XSS": true, +} diff --git a/codegen/object_build.go b/codegen/object_build.go index 0ef40feff21..3dfa7fd1567 100644 --- a/codegen/object_build.go +++ b/codegen/object_build.go @@ -6,7 +6,7 @@ import ( "strings" "github.com/pkg/errors" - "github.com/vektah/gqlgen/neelance/schema" + "github.com/vektah/gqlparser/ast" "golang.org/x/tools/go/loader" ) @@ -14,26 +14,27 @@ func (cfg *Config) buildObjects(types NamedTypes, prog *loader.Program, imports var objects Objects for _, typ := range cfg.schema.Types { - switch typ := typ.(type) { - case *schema.Object: - obj, err := cfg.buildObject(types, typ) - if err != nil { - return nil, err - } + if typ.Kind != ast.Object { + continue + } - def, err := findGoType(prog, obj.Package, obj.GoType) - if err != nil { - return nil, err - } - if def != nil { - for _, bindErr := range bindObject(def.Type(), obj, imports) { - log.Println(bindErr.Error()) - log.Println(" Adding resolver method") - } - } + obj, err := cfg.buildObject(types, typ, imports) + if err != nil { + return nil, err + } - objects = append(objects, obj) + def, err := findGoType(prog, obj.Package, obj.GoType) + if err != nil { + return nil, err + } + if def != nil { + for _, bindErr := range bindObject(def.Type(), obj, imports) { + log.Println(bindErr.Error()) + log.Println(" Adding resolver method") + } } + + objects = append(objects, obj) } sort.Slice(objects, func(i, j int) bool { @@ -80,38 +81,86 @@ func sanitizeGoName(name string) string { return name } -func (cfg *Config) buildObject(types NamedTypes, typ *schema.Object) (*Object, error) { - obj := &Object{NamedType: types[typ.TypeName()]} - typeEntry, entryExists := cfg.Models[typ.TypeName()] +func (cfg *Config) buildObject(types NamedTypes, typ *ast.Definition, imports *Imports) (*Object, error) { + obj := &Object{NamedType: types[typ.Name]} + typeEntry, entryExists := cfg.Models[typ.Name] + + imp := imports.findByPath(cfg.Exec.ImportPath()) + obj.ResolverInterface = &Ref{GoType: obj.GQLType + "Resolver", Import: imp} + + if typ == cfg.schema.Query { + obj.Root = true + } - for _, i := range typ.Interfaces { - obj.Satisfies = append(obj.Satisfies, i.Name) + if typ == cfg.schema.Mutation { + obj.Root = true + obj.DisableConcurrency = true } + if typ == cfg.schema.Subscription { + obj.Root = true + obj.Stream = true + } + + obj.Satisfies = append(obj.Satisfies, typ.Interfaces...) + for _, field := range typ.Fields { + if typ == cfg.schema.Query && field.Name == "__type" { + obj.Fields = append(obj.Fields, Field{ + Type: &Type{types["__Schema"], []string{modPtr}, nil}, + GQLName: "__schema", + NoErr: true, + GoFieldType: GoFieldMethod, + GoReceiverName: "ec", + GoFieldName: "introspectSchema", + Object: obj, + }) + continue + } + if typ == cfg.schema.Query && field.Name == "__schema" { + obj.Fields = append(obj.Fields, Field{ + Type: &Type{types["__Type"], []string{modPtr}, nil}, + GQLName: "__type", + NoErr: true, + GoFieldType: GoFieldMethod, + GoReceiverName: "ec", + GoFieldName: "introspectType", + Args: []FieldArgument{ + {GQLName: "name", Type: &Type{types["String"], []string{}, nil}, Object: &Object{}}, + }, + Object: obj, + }) + continue + } var forceResolver bool + var goName string if entryExists { if typeField, ok := typeEntry.Fields[field.Name]; ok { + goName = typeField.FieldName forceResolver = typeField.Resolver } } var args []FieldArgument - for _, arg := range field.Args { + for _, arg := range field.Arguments { newArg := FieldArgument{ - GQLName: arg.Name.Name, + GQLName: arg.Name, Type: types.getType(arg.Type), Object: obj, - GoVarName: sanitizeGoName(arg.Name.Name), + GoVarName: sanitizeGoName(arg.Name), } if !newArg.Type.IsInput && !newArg.Type.IsScalar { return nil, errors.Errorf("%s cannot be used as argument of %s.%s. only input and scalar types are allowed", arg.Type, obj.GQLType, field.Name) } - if arg.Default != nil { - newArg.Default = arg.Default.Value(nil) + if arg.DefaultValue != nil { + var err error + newArg.Default, err = arg.DefaultValue.Value(nil) + if err != nil { + return nil, errors.Errorf("default value for %s.%s is not valid: %s", typ.Name, field.Name, err.Error()) + } newArg.StripPtr() } args = append(args, newArg) @@ -122,23 +171,10 @@ func (cfg *Config) buildObject(types NamedTypes, typ *schema.Object) (*Object, e Type: types.getType(field.Type), Args: args, Object: obj, + GoFieldName: goName, ForceResolver: forceResolver, }) } - for name, typ := range cfg.schema.EntryPoints { - schemaObj := typ.(*schema.Object) - if schemaObj.TypeName() != obj.GQLType { - continue - } - - obj.Root = true - if name == "mutation" { - obj.DisableConcurrency = true - } - if name == "subscription" { - obj.Stream = true - } - } return obj, nil } diff --git a/codegen/templates/args.gotpl b/codegen/templates/args.gotpl index f53aceec1e5..db6a0b6bd9e 100644 --- a/codegen/templates/args.gotpl +++ b/codegen/templates/args.gotpl @@ -1,30 +1,17 @@ {{- if . }}args := map[string]interface{}{} {{end}} {{- range $i, $arg := . }} var arg{{$i}} {{$arg.Signature }} - if tmp, ok := field.Args[{{$arg.GQLName|quote}}]; ok { + if tmp, ok := rawArgs[{{$arg.GQLName|quote}}]; ok { var err error {{$arg.Unmarshal (print "arg" $i) "tmp" }} if err != nil { ec.Error(ctx, err) - {{- if $arg.Object.Stream }} - return nil - {{- else }} - return graphql.Null - {{- end }} - } - } {{ if $arg.Default }} else { - var tmp interface{} = {{ $arg.Default | dump }} - var err error - {{$arg.Unmarshal (print "arg" $i) "tmp" }} - if err != nil { - ec.Error(ctx, err) - {{- if $arg.Object.Stream }} + {{- if $arg.Stream }} return nil {{- else }} return graphql.Null {{- end }} } } - {{end }} args[{{$arg.GQLName|quote}}] = arg{{$i}} {{- end -}} diff --git a/codegen/templates/data.go b/codegen/templates/data.go index d6da480770b..45b5f6d0adb 100644 --- a/codegen/templates/data.go +++ b/codegen/templates/data.go @@ -1,11 +1,13 @@ package templates var data = map[string]string{ - "args.gotpl": "\t{{- if . }}args := map[string]interface{}{} {{end}}\n\t{{- range $i, $arg := . }}\n\t\tvar arg{{$i}} {{$arg.Signature }}\n\t\tif tmp, ok := field.Args[{{$arg.GQLName|quote}}]; ok {\n\t\t\tvar err error\n\t\t\t{{$arg.Unmarshal (print \"arg\" $i) \"tmp\" }}\n\t\t\tif err != nil {\n\t\t\t\tec.Error(ctx, err)\n\t\t\t\t{{- if $arg.Object.Stream }}\n\t\t\t\t\treturn nil\n\t\t\t\t{{- else }}\n\t\t\t\t\treturn graphql.Null\n\t\t\t\t{{- end }}\n\t\t\t}\n\t\t} {{ if $arg.Default }} else {\n\t\t\tvar tmp interface{} = {{ $arg.Default | dump }}\n\t\t\tvar err error\n\t\t\t{{$arg.Unmarshal (print \"arg\" $i) \"tmp\" }}\n\t\t\tif err != nil {\n\t\t\t\tec.Error(ctx, err)\n\t\t\t\t{{- if $arg.Object.Stream }}\n\t\t\t\t\treturn nil\n\t\t\t\t{{- else }}\n\t\t\t\t\treturn graphql.Null\n\t\t\t\t{{- end }}\n\t\t\t}\n\t\t}\n\t\t{{end }}\n\t\targs[{{$arg.GQLName|quote}}] = arg{{$i}}\n\t{{- end -}}\n", - "field.gotpl": "{{ $field := . }}\n{{ $object := $field.Object }}\n\n{{- if $object.Stream }}\n\tfunc (ec *executionContext) _{{$object.GQLType}}_{{$field.GQLName}}(ctx context.Context, field graphql.CollectedField) func() graphql.Marshaler {\n\t\t{{- template \"args.gotpl\" $field.Args }}\n\t\tctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{Field: field})\n\t\tresults, err := ec.resolvers.{{ $object.GQLType }}_{{ $field.GQLName }}({{ $field.CallArgs }})\n\t\tif err != nil {\n\t\t\tec.Error(ctx, err)\n\t\t\treturn nil\n\t\t}\n\t\treturn func() graphql.Marshaler {\n\t\t\tres, ok := <-results\n\t\t\tif !ok {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tvar out graphql.OrderedMap\n\t\t\tout.Add(field.Alias, func() graphql.Marshaler { {{ $field.WriteJson }} }())\n\t\t\treturn &out\n\t\t}\n\t}\n{{ else }}\n\tfunc (ec *executionContext) _{{$object.GQLType}}_{{$field.GQLName}}(ctx context.Context, field graphql.CollectedField, {{if not $object.Root}}obj *{{$object.FullName}}{{end}}) graphql.Marshaler {\n\t\t{{- template \"args.gotpl\" $field.Args }}\n\n\t\t{{- if $field.IsConcurrent }}\n\t\t\tctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{\n\t\t\t\tObject: {{$object.GQLType|quote}},\n\t\t\t\tArgs: {{if $field.Args }}args{{else}}nil{{end}},\n\t\t\t\tField: field,\n\t\t\t})\n\t\t\treturn graphql.Defer(func() (ret graphql.Marshaler) {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t\tuserErr := ec.Recover(ctx, r)\n\t\t\t\t\t\tec.Error(ctx, userErr)\n\t\t\t\t\t\tret = graphql.Null\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t{{ else }}\n\t\t\trctx := graphql.GetResolverContext(ctx)\n\t\t\trctx.Object = {{$object.GQLType|quote}}\n\t\t\trctx.Args = {{if $field.Args }}args{{else}}nil{{end}}\n\t\t\trctx.Field = field\n\t\t\trctx.PushField(field.Alias)\n\t\t\tdefer rctx.Pop()\n\t\t{{- end }}\n\n\t\t\t{{- if $field.IsResolver }}\n\t\t\t\tresTmp, err := ec.ResolverMiddleware(ctx, func(ctx context.Context) (interface{}, error) {\n\t\t\t\t\treturn ec.resolvers.{{ $object.GQLType }}_{{ $field.GQLName }}({{ $field.CallArgs }})\n\t\t\t\t})\n\t\t\t\tif err != nil {\n\t\t\t\t\tec.Error(ctx, err)\n\t\t\t\t\treturn graphql.Null\n\t\t\t\t}\n\t\t\t\tif resTmp == nil {\n\t\t\t\t\treturn graphql.Null\n\t\t\t\t}\n\t\t\t\tres := resTmp.({{$field.Signature}})\n\t\t\t{{- else if $field.GoVarName }}\n\t\t\t\tres := obj.{{$field.GoVarName}}\n\t\t\t{{- else if $field.GoMethodName }}\n\t\t\t\t{{- if $field.NoErr }}\n\t\t\t\t\tres := {{$field.GoMethodName}}({{ $field.CallArgs }})\n\t\t\t\t{{- else }}\n\t\t\t\t\tres, err := {{$field.GoMethodName}}({{ $field.CallArgs }})\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tec.Error(ctx, err)\n\t\t\t\t\t\treturn graphql.Null\n\t\t\t\t\t}\n\t\t\t\t{{- end }}\n\t\t\t{{- end }}\n\t\t\t{{ $field.WriteJson }}\n\t\t{{- if $field.IsConcurrent }}\n\t\t\t})\n\t\t{{- end }}\n\t}\n{{ end }}\n", - "generated.gotpl": "// Code generated by github.com/vektah/gqlgen, DO NOT EDIT.\n\npackage {{ .PackageName }}\n\nimport (\n{{- range $import := .Imports }}\n\t{{- $import.Write }}\n{{ end }}\n)\n\n// MakeExecutableSchema creates an ExecutableSchema from the Resolvers interface.\nfunc MakeExecutableSchema(resolvers Resolvers) graphql.ExecutableSchema {\n\treturn &executableSchema{resolvers: resolvers}\n}\n\n// NewExecutableSchema creates an ExecutableSchema from the ResolverRoot interface.\nfunc NewExecutableSchema(resolvers ResolverRoot) graphql.ExecutableSchema {\n\treturn MakeExecutableSchema(shortMapper{r: resolvers})\n}\n\ntype Resolvers interface {\n{{- range $object := .Objects -}}\n\t{{ range $field := $object.Fields -}}\n\t\t{{ $field.ResolverDeclaration }}\n\t{{ end }}\n{{- end }}\n}\n\ntype ResolverRoot interface {\n{{- range $object := .Objects -}}\n\t{{ if $object.HasResolvers -}}\n\t\t{{$object.GQLType}}() {{$object.GQLType}}Resolver\n\t{{ end }}\n{{- end }}\n}\n\n{{- range $object := .Objects -}}\n\t{{ if $object.HasResolvers }}\n\t\ttype {{$object.GQLType}}Resolver interface {\n\t\t{{ range $field := $object.Fields -}}\n\t\t\t{{ $field.ShortResolverDeclaration }}\n\t\t{{ end }}\n\t\t}\n\t{{- end }}\n{{- end }}\n\ntype shortMapper struct {\n\tr ResolverRoot\n}\n\n{{- range $object := .Objects -}}\n\t{{ range $field := $object.Fields -}}\n\t\t{{- if $field.IsResolver }}\n\t\t\tfunc (s shortMapper) {{ $field.ResolverDeclaration }} {\n\t\t\t\treturn s.r.{{$field.ShortInvocation}}\n\t\t\t}\n\t\t{{- end }}\n\t{{ end }}\n{{- end }}\n\ntype executableSchema struct {\n\tresolvers Resolvers\n}\n\nfunc (e *executableSchema) Schema() *schema.Schema {\n\treturn parsedSchema\n}\n\nfunc (e *executableSchema) Query(ctx context.Context, op *query.Operation) *graphql.Response {\n\t{{- if .QueryRoot }}\n\t\tec := executionContext{graphql.GetRequestContext(ctx), e.resolvers}\n\n\t\tbuf := ec.RequestMiddleware(ctx, func(ctx context.Context) []byte {\n\t\t\tdata := ec._{{.QueryRoot.GQLType}}(ctx, op.Selections)\n\t\t\tvar buf bytes.Buffer\n\t\t\tdata.MarshalGQL(&buf)\n\t\t\treturn buf.Bytes()\n\t\t})\n\n\t\treturn &graphql.Response{\n\t\t\tData: buf,\n\t\t\tErrors: ec.Errors,\n\t\t}\n\t{{- else }}\n\t\treturn graphql.ErrorResponse(ctx, \"queries are not supported\")\n\t{{- end }}\n}\n\nfunc (e *executableSchema) Mutation(ctx context.Context, op *query.Operation) *graphql.Response {\n\t{{- if .MutationRoot }}\n\t\tec := executionContext{graphql.GetRequestContext(ctx), e.resolvers}\n\n\t\tbuf := ec.RequestMiddleware(ctx, func(ctx context.Context) []byte {\n\t\t\tdata := ec._{{.MutationRoot.GQLType}}(ctx, op.Selections)\n\t\t\tvar buf bytes.Buffer\n\t\t\tdata.MarshalGQL(&buf)\n\t\t\treturn buf.Bytes()\n\t\t})\n\n\t\treturn &graphql.Response{\n\t\t\tData: buf,\n\t\t\tErrors: ec.Errors,\n\t\t}\n\t{{- else }}\n\t\treturn graphql.ErrorResponse(ctx, \"mutations are not supported\")\n\t{{- end }}\n}\n\nfunc (e *executableSchema) Subscription(ctx context.Context, op *query.Operation) func() *graphql.Response {\n\t{{- if .SubscriptionRoot }}\n\t\tec := executionContext{graphql.GetRequestContext(ctx), e.resolvers}\n\n\t\tnext := ec._{{.SubscriptionRoot.GQLType}}(ctx, op.Selections)\n\t\tif ec.Errors != nil {\n\t\t\treturn graphql.OneShot(&graphql.Response{Data: []byte(\"null\"), Errors: ec.Errors})\n\t\t}\n\n\t\tvar buf bytes.Buffer\n\t\treturn func() *graphql.Response {\n\t\t\tbuf := ec.RequestMiddleware(ctx, func(ctx context.Context) []byte {\n\t\t\t\tbuf.Reset()\n\t\t\t\tdata := next()\n\n\t\t\t\tif data == nil {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tdata.MarshalGQL(&buf)\n\t\t\t\treturn buf.Bytes()\n\t\t\t})\n\n\t\t\treturn &graphql.Response{\n\t\t\t\tData: buf,\n\t\t\t\tErrors: ec.Errors,\n\t\t\t}\n\t\t}\n\t{{- else }}\n\t\treturn graphql.OneShot(graphql.ErrorResponse(ctx, \"subscriptions are not supported\"))\n\t{{- end }}\n}\n\ntype executionContext struct {\n\t*graphql.RequestContext\n\n\tresolvers Resolvers\n}\n\n{{- range $object := .Objects }}\n\t{{ template \"object.gotpl\" $object }}\n\n\t{{- range $field := $object.Fields }}\n\t\t{{ template \"field.gotpl\" $field }}\n\t{{ end }}\n{{- end}}\n\n{{- range $interface := .Interfaces }}\n\t{{ template \"interface.gotpl\" $interface }}\n{{- end }}\n\n{{- range $input := .Inputs }}\n\t{{ template \"input.gotpl\" $input }}\n{{- end }}\n\nfunc (ec *executionContext) introspectSchema() *introspection.Schema {\n\treturn introspection.WrapSchema(parsedSchema)\n}\n\nfunc (ec *executionContext) introspectType(name string) *introspection.Type {\n\tt := parsedSchema.Resolve(name)\n\tif t == nil {\n\t\treturn nil\n\t}\n\treturn introspection.WrapType(t)\n}\n\nvar parsedSchema = schema.MustParse({{.SchemaRaw|rawQuote}})\n", - "input.gotpl": "\t{{- if .IsMarshaled }}\n\tfunc Unmarshal{{ .GQLType }}(v interface{}) ({{.FullName}}, error) {\n\t\tvar it {{.FullName}}\n\t\tvar asMap = v.(map[string]interface{})\n\t\t{{ range $field := .Fields}}\n\t\t\t{{- if $field.Default}}\n\t\t\t\tif _, present := asMap[{{$field.GQLName|quote}}] ; !present {\n\t\t\t\t\tasMap[{{$field.GQLName|quote}}] = {{ $field.Default | dump }}\n\t\t\t\t}\n\t\t\t{{- end}}\n\t\t{{- end }}\n\n\t\tfor k, v := range asMap {\n\t\t\tswitch k {\n\t\t\t{{- range $field := .Fields }}\n\t\t\tcase {{$field.GQLName|quote}}:\n\t\t\t\tvar err error\n\t\t\t\t{{ $field.Unmarshal (print \"it.\" $field.GoVarName) \"v\" }}\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn it, err\n\t\t\t\t}\n\t\t\t{{- end }}\n\t\t\t}\n\t\t}\n\n\t\treturn it, nil\n\t}\n\t{{- end }}\n", - "interface.gotpl": "{{- $interface := . }}\n\nfunc (ec *executionContext) _{{$interface.GQLType}}(ctx context.Context, sel []query.Selection, obj *{{$interface.FullName}}) graphql.Marshaler {\n\tswitch obj := (*obj).(type) {\n\tcase nil:\n\t\treturn graphql.Null\n\t{{- range $implementor := $interface.Implementors }}\n\t\t{{- if $implementor.ValueReceiver }}\n\t\t\tcase {{$implementor.FullName}}:\n\t\t\t\treturn ec._{{$implementor.GQLType}}(ctx, sel, &obj)\n\t\t{{- end}}\n\t\tcase *{{$implementor.FullName}}:\n\t\t\treturn ec._{{$implementor.GQLType}}(ctx, sel, obj)\n\t{{- end }}\n\tdefault:\n\t\tpanic(fmt.Errorf(\"unexpected type %T\", obj))\n\t}\n}\n", - "models.gotpl": "// Code generated by github.com/vektah/gqlgen, DO NOT EDIT.\n\npackage {{ .PackageName }}\n\nimport (\n{{- range $import := .Imports }}\n\t{{- $import.Write }}\n{{ end }}\n)\n\n{{ range $model := .Models }}\n\t{{- if .IsInterface }}\n\t\ttype {{.GoType}} interface {}\n\t{{- else }}\n\t\ttype {{.GoType}} struct {\n\t\t\t{{- range $field := .Fields }}\n\t\t\t\t{{- if $field.GoVarName }}\n\t\t\t\t\t{{ $field.GoVarName }} {{$field.Signature}} `json:\"{{$field.GQLName}}\"`\n\t\t\t\t{{- else }}\n\t\t\t\t\t{{ $field.GoFKName }} {{$field.GoFKType}}\n\t\t\t\t{{- end }}\n\t\t\t{{- end }}\n\t\t}\n\t{{- end }}\n{{- end}}\n\n{{ range $enum := .Enums }}\n\ttype {{.GoType}} string\n\tconst (\n\t{{ range $value := .Values -}}\n\t\t{{with .Description}} {{.|prefixLines \"// \"}} {{end}}\n\t\t{{$enum.GoType}}{{ .Name|toCamel }} {{$enum.GoType}} = {{.Name|quote}}\n\t{{- end }}\n\t)\n\n\tfunc (e {{.GoType}}) IsValid() bool {\n\t\tswitch e {\n\t\tcase {{ range $index, $element := .Values}}{{if $index}},{{end}}{{ $enum.GoType }}{{ $element.Name|toCamel }}{{end}}:\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t}\n\n\tfunc (e {{.GoType}}) String() string {\n\t\treturn string(e)\n\t}\n\n\tfunc (e *{{.GoType}}) UnmarshalGQL(v interface{}) error {\n\t\tstr, ok := v.(string)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"enums must be strings\")\n\t\t}\n\n\t\t*e = {{.GoType}}(str)\n\t\tif !e.IsValid() {\n\t\t\treturn fmt.Errorf(\"%s is not a valid {{.GQLType}}\", str)\n\t\t}\n\t\treturn nil\n\t}\n\n\tfunc (e {{.GoType}}) MarshalGQL(w io.Writer) {\n\t\tfmt.Fprint(w, strconv.Quote(e.String()))\n\t}\n\n{{- end }}\n", - "object.gotpl": "{{ $object := . }}\n\nvar {{ $object.GQLType|lcFirst}}Implementors = {{$object.Implementors}}\n\n// nolint: gocyclo, errcheck, gas, goconst\n{{- if .Stream }}\nfunc (ec *executionContext) _{{$object.GQLType}}(ctx context.Context, sel []query.Selection) func() graphql.Marshaler {\n\tfields := graphql.CollectFields(ec.Doc, sel, {{$object.GQLType|lcFirst}}Implementors, ec.Variables)\n\tctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{\n\t\tObject: {{$object.GQLType|quote}},\n\t})\n\tif len(fields) != 1 {\n\t\tec.Errorf(ctx, \"must subscribe to exactly one stream\")\n\t\treturn nil\n\t}\n\n\tswitch fields[0].Name {\n\t{{- range $field := $object.Fields }}\n\tcase \"{{$field.GQLName}}\":\n\t\treturn ec._{{$object.GQLType}}_{{$field.GQLName}}(ctx, fields[0])\n\t{{- end }}\n\tdefault:\n\t\tpanic(\"unknown field \" + strconv.Quote(fields[0].Name))\n\t}\n}\n{{- else }}\nfunc (ec *executionContext) _{{$object.GQLType}}(ctx context.Context, sel []query.Selection{{if not $object.Root}}, obj *{{$object.FullName}} {{end}}) graphql.Marshaler {\n\tfields := graphql.CollectFields(ec.Doc, sel, {{$object.GQLType|lcFirst}}Implementors, ec.Variables)\n\t{{if $object.Root}}\n\t\tctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{\n\t\t\tObject: {{$object.GQLType|quote}},\n\t\t})\n\t{{end}}\n\tout := graphql.NewOrderedMap(len(fields))\n\tfor i, field := range fields {\n\t\tout.Keys[i] = field.Alias\n\n\t\tswitch field.Name {\n\t\tcase \"__typename\":\n\t\t\tout.Values[i] = graphql.MarshalString({{$object.GQLType|quote}})\n\t\t{{- range $field := $object.Fields }}\n\t\tcase \"{{$field.GQLName}}\":\n\t\t\tout.Values[i] = ec._{{$object.GQLType}}_{{$field.GQLName}}(ctx, field{{if not $object.Root}}, obj{{end}})\n\t\t{{- end }}\n\t\tdefault:\n\t\t\tpanic(\"unknown field \" + strconv.Quote(field.Name))\n\t\t}\n\t}\n\n\treturn out\n}\n{{- end }}\n", + "args.gotpl": "\t{{- if . }}args := map[string]interface{}{} {{end}}\n\t{{- range $i, $arg := . }}\n\t\tvar arg{{$i}} {{$arg.Signature }}\n\t\tif tmp, ok := rawArgs[{{$arg.GQLName|quote}}]; ok {\n\t\t\tvar err error\n\t\t\t{{$arg.Unmarshal (print \"arg\" $i) \"tmp\" }}\n\t\t\tif err != nil {\n\t\t\t\tec.Error(ctx, err)\n\t\t\t\t{{- if $arg.Stream }}\n\t\t\t\t\treturn nil\n\t\t\t\t{{- else }}\n\t\t\t\t\treturn graphql.Null\n\t\t\t\t{{- end }}\n\t\t\t}\n\t\t}\n\t\targs[{{$arg.GQLName|quote}}] = arg{{$i}}\n\t{{- end -}}\n", + "field.gotpl": "{{ $field := . }}\n{{ $object := $field.Object }}\n\n{{- if $object.Stream }}\n\tfunc (ec *executionContext) _{{$object.GQLType}}_{{$field.GQLName}}(ctx context.Context, field graphql.CollectedField) func() graphql.Marshaler {\n\t\t{{- if $field.Args }}\n\t\t\trawArgs := field.ArgumentMap(ec.Variables)\n\t\t\t{{ template \"args.gotpl\" $field.Args }}\n\t\t{{- end }}\n\t\tctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{Field: field})\n\t\tresults, err := ec.resolvers.{{ $field.ShortInvocation }}\n\t\tif err != nil {\n\t\t\tec.Error(ctx, err)\n\t\t\treturn nil\n\t\t}\n\t\treturn func() graphql.Marshaler {\n\t\t\tres, ok := <-results\n\t\t\tif !ok {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tvar out graphql.OrderedMap\n\t\t\tout.Add(field.Alias, func() graphql.Marshaler { {{ $field.WriteJson }} }())\n\t\t\treturn &out\n\t\t}\n\t}\n{{ else }}\n\tfunc (ec *executionContext) _{{$object.GQLType}}_{{$field.GQLName}}(ctx context.Context, field graphql.CollectedField, {{if not $object.Root}}obj *{{$object.FullName}}{{end}}) graphql.Marshaler {\n\t\t{{- if $field.Args }}\n\t\t\trawArgs := field.ArgumentMap(ec.Variables)\n\t\t\t{{ template \"args.gotpl\" $field.Args }}\n\t\t{{- end }}\n\n\t\t{{- if $field.IsConcurrent }}\n\t\t\tctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{\n\t\t\t\tObject: {{$object.GQLType|quote}},\n\t\t\t\tArgs: {{if $field.Args }}args{{else}}nil{{end}},\n\t\t\t\tField: field,\n\t\t\t})\n\t\t\treturn graphql.Defer(func() (ret graphql.Marshaler) {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t\tuserErr := ec.Recover(ctx, r)\n\t\t\t\t\t\tec.Error(ctx, userErr)\n\t\t\t\t\t\tret = graphql.Null\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t{{ else }}\n\t\t\trctx := graphql.GetResolverContext(ctx)\n\t\t\trctx.Object = {{$object.GQLType|quote}}\n\t\t\trctx.Args = {{if $field.Args }}args{{else}}nil{{end}}\n\t\t\trctx.Field = field\n\t\t\trctx.PushField(field.Alias)\n\t\t\tdefer rctx.Pop()\n\t\t{{- end }}\n\t\t\tresTmp := ec.FieldMiddleware(ctx, func(ctx context.Context) (interface{}, error) {\n\t\t\t\t{{- if $field.IsResolver }}\n\t\t\t\t\treturn ec.resolvers.{{ $field.ShortInvocation }}\n\t\t\t\t{{- else if $field.IsMethod }}\n\t\t\t\t\t{{- if $field.NoErr }}\n\t\t\t\t\t\treturn {{$field.GoReceiverName}}.{{$field.GoFieldName}}({{ $field.CallArgs }}), nil\n\t\t\t\t\t{{- else }}\n\t\t\t\t\t\treturn {{$field.GoReceiverName}}.{{$field.GoFieldName}}({{ $field.CallArgs }})\n\t\t\t\t\t{{- end }}\n\t\t\t\t{{- else if $field.IsVariable }}\n\t\t\t\t\treturn {{$field.GoReceiverName}}.{{$field.GoFieldName}}, nil\n\t\t\t\t{{- end }}\n\t\t\t})\n\t\t\tif resTmp == nil {\n\t\t\t\treturn graphql.Null\n\t\t\t}\n\t\t\tres := resTmp.({{$field.Signature}})\n\t\t\t{{ $field.WriteJson }}\n\t\t{{- if $field.IsConcurrent }}\n\t\t\t})\n\t\t{{- end }}\n\t}\n{{ end }}\n", + "generated.gotpl": "// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.\n\npackage {{ .PackageName }}\n\nimport (\n{{- range $import := .Imports }}\n\t{{- $import.Write }}\n{{ end }}\n)\n\n// NewExecutableSchema creates an ExecutableSchema from the ResolverRoot interface.\nfunc NewExecutableSchema(cfg Config) graphql.ExecutableSchema {\n\treturn &executableSchema{\n\t\tresolvers: cfg.Resolvers,\n\t\tdirectives: cfg.Directives,\n\t}\n}\n\ntype Config struct {\n\tResolvers ResolverRoot\n\tDirectives DirectiveRoot\n}\n\ntype ResolverRoot interface {\n{{- range $object := .Objects -}}\n\t{{ if $object.HasResolvers -}}\n\t\t{{$object.GQLType}}() {{$object.GQLType}}Resolver\n\t{{ end }}\n{{- end }}\n}\n\ntype DirectiveRoot struct {\n{{ range $directive := .Directives }}\n\t{{ $directive.Declaration }}\n{{ end }}\n}\n\n{{- range $object := .Objects -}}\n\t{{ if $object.HasResolvers }}\n\t\ttype {{$object.GQLType}}Resolver interface {\n\t\t{{ range $field := $object.Fields -}}\n\t\t\t{{ $field.ShortResolverDeclaration }}\n\t\t{{ end }}\n\t\t}\n\t{{- end }}\n{{- end }}\n\ntype executableSchema struct {\n\tresolvers ResolverRoot\n\tdirectives DirectiveRoot\n}\n\nfunc (e *executableSchema) Schema() *ast.Schema {\n\treturn parsedSchema\n}\n\nfunc (e *executableSchema) Query(ctx context.Context, op *ast.OperationDefinition) *graphql.Response {\n\t{{- if .QueryRoot }}\n\t\tec := executionContext{graphql.GetRequestContext(ctx), e}\n\n\t\tbuf := ec.RequestMiddleware(ctx, func(ctx context.Context) []byte {\n\t\t\tdata := ec._{{.QueryRoot.GQLType}}(ctx, op.SelectionSet)\n\t\t\tvar buf bytes.Buffer\n\t\t\tdata.MarshalGQL(&buf)\n\t\t\treturn buf.Bytes()\n\t\t})\n\n\t\treturn &graphql.Response{\n\t\t\tData: buf,\n\t\t\tErrors: ec.Errors,\n\t\t}\n\t{{- else }}\n\t\treturn graphql.ErrorResponse(ctx, \"queries are not supported\")\n\t{{- end }}\n}\n\nfunc (e *executableSchema) Mutation(ctx context.Context, op *ast.OperationDefinition) *graphql.Response {\n\t{{- if .MutationRoot }}\n\t\tec := executionContext{graphql.GetRequestContext(ctx), e}\n\n\t\tbuf := ec.RequestMiddleware(ctx, func(ctx context.Context) []byte {\n\t\t\tdata := ec._{{.MutationRoot.GQLType}}(ctx, op.SelectionSet)\n\t\t\tvar buf bytes.Buffer\n\t\t\tdata.MarshalGQL(&buf)\n\t\t\treturn buf.Bytes()\n\t\t})\n\n\t\treturn &graphql.Response{\n\t\t\tData: buf,\n\t\t\tErrors: ec.Errors,\n\t\t}\n\t{{- else }}\n\t\treturn graphql.ErrorResponse(ctx, \"mutations are not supported\")\n\t{{- end }}\n}\n\nfunc (e *executableSchema) Subscription(ctx context.Context, op *ast.OperationDefinition) func() *graphql.Response {\n\t{{- if .SubscriptionRoot }}\n\t\tec := executionContext{graphql.GetRequestContext(ctx), e}\n\n\t\tnext := ec._{{.SubscriptionRoot.GQLType}}(ctx, op.SelectionSet)\n\t\tif ec.Errors != nil {\n\t\t\treturn graphql.OneShot(&graphql.Response{Data: []byte(\"null\"), Errors: ec.Errors})\n\t\t}\n\n\t\tvar buf bytes.Buffer\n\t\treturn func() *graphql.Response {\n\t\t\tbuf := ec.RequestMiddleware(ctx, func(ctx context.Context) []byte {\n\t\t\t\tbuf.Reset()\n\t\t\t\tdata := next()\n\n\t\t\t\tif data == nil {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tdata.MarshalGQL(&buf)\n\t\t\t\treturn buf.Bytes()\n\t\t\t})\n\n\t\t\treturn &graphql.Response{\n\t\t\t\tData: buf,\n\t\t\t\tErrors: ec.Errors,\n\t\t\t}\n\t\t}\n\t{{- else }}\n\t\treturn graphql.OneShot(graphql.ErrorResponse(ctx, \"subscriptions are not supported\"))\n\t{{- end }}\n}\n\ntype executionContext struct {\n\t*graphql.RequestContext\n\t*executableSchema\n}\n\n{{- range $object := .Objects }}\n\t{{ template \"object.gotpl\" $object }}\n\n\t{{- range $field := $object.Fields }}\n\t\t{{ template \"field.gotpl\" $field }}\n\t{{ end }}\n{{- end}}\n\n{{- range $interface := .Interfaces }}\n\t{{ template \"interface.gotpl\" $interface }}\n{{- end }}\n\n{{- range $input := .Inputs }}\n\t{{ template \"input.gotpl\" $input }}\n{{- end }}\n\nfunc (ec *executionContext) FieldMiddleware(ctx context.Context, next graphql.Resolver) interface{} {\n\t{{- if .Directives }}\n\trctx := graphql.GetResolverContext(ctx)\n\tfor _, d := range rctx.Field.Definition.Directives {\n\t\tswitch d.Name {\n\t\t{{- range $directive := .Directives }}\n\t\tcase \"{{$directive.Name}}\":\n\t\t\tif ec.directives.{{$directive.Name|ucFirst}} != nil {\n\t\t\t\t{{- if $directive.Args }}\n\t\t\t\t\trawArgs := d.ArgumentMap(ec.Variables)\n\t\t\t\t\t{{ template \"args.gotpl\" $directive.Args }}\n\t\t\t\t{{- end }}\n\t\t\t\tn := next\n\t\t\t\tnext = func(ctx context.Context) (interface{}, error) {\n\t\t\t\t\treturn ec.directives.{{$directive.Name|ucFirst}}({{$directive.CallArgs}})\n\t\t\t\t}\n\t\t\t}\n\t\t{{- end }}\n\t\t}\n\t}\n\t{{- end }}\n\tres, err := ec.ResolverMiddleware(ctx, next)\n\tif err != nil {\n\t\tec.Error(ctx, err)\n\t\treturn nil\n\t}\n\treturn res\n}\n\nfunc (ec *executionContext) introspectSchema() *introspection.Schema {\n\treturn introspection.WrapSchema(parsedSchema)\n}\n\nfunc (ec *executionContext) introspectType(name string) *introspection.Type {\n\treturn introspection.WrapTypeFromDef(parsedSchema, parsedSchema.Types[name])\n}\n\nvar parsedSchema = gqlparser.MustLoadSchema(\n\t&ast.Source{Name: {{.SchemaFilename|quote}}, Input: {{.SchemaRaw|rawQuote}}},\n)\n", + "input.gotpl": "\t{{- if .IsMarshaled }}\n\tfunc Unmarshal{{ .GQLType }}(v interface{}) ({{.FullName}}, error) {\n\t\tvar it {{.FullName}}\n\t\tvar asMap = v.(map[string]interface{})\n\t\t{{ range $field := .Fields}}\n\t\t\t{{- if $field.Default}}\n\t\t\t\tif _, present := asMap[{{$field.GQLName|quote}}] ; !present {\n\t\t\t\t\tasMap[{{$field.GQLName|quote}}] = {{ $field.Default | dump }}\n\t\t\t\t}\n\t\t\t{{- end}}\n\t\t{{- end }}\n\n\t\tfor k, v := range asMap {\n\t\t\tswitch k {\n\t\t\t{{- range $field := .Fields }}\n\t\t\tcase {{$field.GQLName|quote}}:\n\t\t\t\tvar err error\n\t\t\t\t{{ $field.Unmarshal (print \"it.\" $field.GoFieldName) \"v\" }}\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn it, err\n\t\t\t\t}\n\t\t\t{{- end }}\n\t\t\t}\n\t\t}\n\n\t\treturn it, nil\n\t}\n\t{{- end }}\n", + "interface.gotpl": "{{- $interface := . }}\n\nfunc (ec *executionContext) _{{$interface.GQLType}}(ctx context.Context, sel ast.SelectionSet, obj *{{$interface.FullName}}) graphql.Marshaler {\n\tswitch obj := (*obj).(type) {\n\tcase nil:\n\t\treturn graphql.Null\n\t{{- range $implementor := $interface.Implementors }}\n\t\t{{- if $implementor.ValueReceiver }}\n\t\t\tcase {{$implementor.FullName}}:\n\t\t\t\treturn ec._{{$implementor.GQLType}}(ctx, sel, &obj)\n\t\t{{- end}}\n\t\tcase *{{$implementor.FullName}}:\n\t\t\treturn ec._{{$implementor.GQLType}}(ctx, sel, obj)\n\t{{- end }}\n\tdefault:\n\t\tpanic(fmt.Errorf(\"unexpected type %T\", obj))\n\t}\n}\n", + "models.gotpl": "// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.\n\npackage {{ .PackageName }}\n\nimport (\n{{- range $import := .Imports }}\n\t{{- $import.Write }}\n{{ end }}\n)\n\n{{ range $model := .Models }}\n\t{{- if .IsInterface }}\n\t\ttype {{.GoType}} interface {}\n\t{{- else }}\n\t\ttype {{.GoType}} struct {\n\t\t\t{{- range $field := .Fields }}\n\t\t\t\t{{- if $field.GoFieldName }}\n\t\t\t\t\t{{ $field.GoFieldName }} {{$field.Signature}} `json:\"{{$field.GQLName}}\"`\n\t\t\t\t{{- else }}\n\t\t\t\t\t{{ $field.GoFKName }} {{$field.GoFKType}}\n\t\t\t\t{{- end }}\n\t\t\t{{- end }}\n\t\t}\n\t{{- end }}\n{{- end}}\n\n{{ range $enum := .Enums }}\n\ttype {{.GoType}} string\n\tconst (\n\t{{ range $value := .Values -}}\n\t\t{{with .Description}} {{.|prefixLines \"// \"}} {{end}}\n\t\t{{$enum.GoType}}{{ .Name|toCamel }} {{$enum.GoType}} = {{.Name|quote}}\n\t{{- end }}\n\t)\n\n\tfunc (e {{.GoType}}) IsValid() bool {\n\t\tswitch e {\n\t\tcase {{ range $index, $element := .Values}}{{if $index}},{{end}}{{ $enum.GoType }}{{ $element.Name|toCamel }}{{end}}:\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t}\n\n\tfunc (e {{.GoType}}) String() string {\n\t\treturn string(e)\n\t}\n\n\tfunc (e *{{.GoType}}) UnmarshalGQL(v interface{}) error {\n\t\tstr, ok := v.(string)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"enums must be strings\")\n\t\t}\n\n\t\t*e = {{.GoType}}(str)\n\t\tif !e.IsValid() {\n\t\t\treturn fmt.Errorf(\"%s is not a valid {{.GQLType}}\", str)\n\t\t}\n\t\treturn nil\n\t}\n\n\tfunc (e {{.GoType}}) MarshalGQL(w io.Writer) {\n\t\tfmt.Fprint(w, strconv.Quote(e.String()))\n\t}\n\n{{- end }}\n", + "object.gotpl": "{{ $object := . }}\n\nvar {{ $object.GQLType|lcFirst}}Implementors = {{$object.Implementors}}\n\n// nolint: gocyclo, errcheck, gas, goconst\n{{- if .Stream }}\nfunc (ec *executionContext) _{{$object.GQLType}}(ctx context.Context, sel ast.SelectionSet) func() graphql.Marshaler {\n\tfields := graphql.CollectFields(ctx, sel, {{$object.GQLType|lcFirst}}Implementors)\n\tctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{\n\t\tObject: {{$object.GQLType|quote}},\n\t})\n\tif len(fields) != 1 {\n\t\tec.Errorf(ctx, \"must subscribe to exactly one stream\")\n\t\treturn nil\n\t}\n\n\tswitch fields[0].Name {\n\t{{- range $field := $object.Fields }}\n\tcase \"{{$field.GQLName}}\":\n\t\treturn ec._{{$object.GQLType}}_{{$field.GQLName}}(ctx, fields[0])\n\t{{- end }}\n\tdefault:\n\t\tpanic(\"unknown field \" + strconv.Quote(fields[0].Name))\n\t}\n}\n{{- else }}\nfunc (ec *executionContext) _{{$object.GQLType}}(ctx context.Context, sel ast.SelectionSet{{if not $object.Root}}, obj *{{$object.FullName}} {{end}}) graphql.Marshaler {\n\tfields := graphql.CollectFields(ctx, sel, {{$object.GQLType|lcFirst}}Implementors)\n\t{{if $object.Root}}\n\t\tctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{\n\t\t\tObject: {{$object.GQLType|quote}},\n\t\t})\n\t{{end}}\n\tout := graphql.NewOrderedMap(len(fields))\n\tfor i, field := range fields {\n\t\tout.Keys[i] = field.Alias\n\n\t\tswitch field.Name {\n\t\tcase \"__typename\":\n\t\t\tout.Values[i] = graphql.MarshalString({{$object.GQLType|quote}})\n\t\t{{- range $field := $object.Fields }}\n\t\tcase \"{{$field.GQLName}}\":\n\t\t\tout.Values[i] = ec._{{$object.GQLType}}_{{$field.GQLName}}(ctx, field{{if not $object.Root}}, obj{{end}})\n\t\t{{- end }}\n\t\tdefault:\n\t\t\tpanic(\"unknown field \" + strconv.Quote(field.Name))\n\t\t}\n\t}\n\n\treturn out\n}\n{{- end }}\n", + "resolver.gotpl": "//go:generate gorunpkg github.com/99designs/gqlgen\n\npackage {{ .PackageName }}\n\nimport (\n{{- range $import := .Imports }}\n\t{{- $import.Write }}\n{{ end }}\n)\n\ntype {{.ResolverType}} struct {}\n\n{{ range $object := .Objects -}}\n\t{{- if $object.HasResolvers -}}\n\t\tfunc (r *{{$.ResolverType}}) {{$object.GQLType}}() {{ $object.ResolverInterface.FullName }} {\n\t\t\treturn &{{lcFirst $object.GQLType}}Resolver{r}\n\t\t}\n\t{{ end -}}\n{{ end }}\n\n{{ range $object := .Objects -}}\n\t{{- if $object.HasResolvers -}}\n\t\ttype {{lcFirst $object.GQLType}}Resolver struct { *Resolver }\n\n\t\t{{ range $field := $object.Fields -}}\n\t\t\t{{- if $field.IsResolver -}}\n\t\t\tfunc (r *{{lcFirst $object.GQLType}}Resolver) {{ $field.ShortResolverDeclaration }} {\n\t\t\t\tpanic(\"not implemented\")\n\t\t\t}\n\t\t\t{{ end -}}\n\t\t{{ end -}}\n\t{{ end -}}\n{{ end }}\n", + "server.gotpl": "package main\n\nimport (\n{{- range $import := .Imports }}\n\t{{- $import.Write }}\n{{ end }}\n)\n\nconst defaultPort = \"8080\"\n\nfunc main() {\n\tport := os.Getenv(\"PORT\")\n\tif port == \"\" {\n\t\tport = defaultPort\n\t}\n\n\thttp.Handle(\"/\", handler.Playground(\"GraphQL playground\", \"/query\"))\n\thttp.Handle(\"/query\", handler.GraphQL({{.ExecPackageName}}.NewExecutableSchema({{.ExecPackageName}}.Config{Resolvers: &{{.ResolverPackageName}}.Resolver{}})))\n\n\tlog.Printf(\"connect to http://localhost:%s/ for GraphQL playground\", port)\n\tlog.Fatal(http.ListenAndServe(\":\" + port, nil))\n}\n", } diff --git a/codegen/templates/field.gotpl b/codegen/templates/field.gotpl index 4279ad8eaea..c324a4ecddf 100644 --- a/codegen/templates/field.gotpl +++ b/codegen/templates/field.gotpl @@ -3,9 +3,12 @@ {{- if $object.Stream }} func (ec *executionContext) _{{$object.GQLType}}_{{$field.GQLName}}(ctx context.Context, field graphql.CollectedField) func() graphql.Marshaler { - {{- template "args.gotpl" $field.Args }} + {{- if $field.Args }} + rawArgs := field.ArgumentMap(ec.Variables) + {{ template "args.gotpl" $field.Args }} + {{- end }} ctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{Field: field}) - results, err := ec.resolvers.{{ $object.GQLType }}_{{ $field.GQLName }}({{ $field.CallArgs }}) + results, err := ec.resolvers.{{ $field.ShortInvocation }} if err != nil { ec.Error(ctx, err) return nil @@ -22,7 +25,10 @@ } {{ else }} func (ec *executionContext) _{{$object.GQLType}}_{{$field.GQLName}}(ctx context.Context, field graphql.CollectedField, {{if not $object.Root}}obj *{{$object.FullName}}{{end}}) graphql.Marshaler { - {{- template "args.gotpl" $field.Args }} + {{- if $field.Args }} + rawArgs := field.ArgumentMap(ec.Variables) + {{ template "args.gotpl" $field.Args }} + {{- end }} {{- if $field.IsConcurrent }} ctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{ @@ -46,32 +52,23 @@ rctx.PushField(field.Alias) defer rctx.Pop() {{- end }} - - {{- if $field.IsResolver }} - resTmp, err := ec.ResolverMiddleware(ctx, func(ctx context.Context) (interface{}, error) { - return ec.resolvers.{{ $object.GQLType }}_{{ $field.GQLName }}({{ $field.CallArgs }}) - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.({{$field.Signature}}) - {{- else if $field.GoVarName }} - res := obj.{{$field.GoVarName}} - {{- else if $field.GoMethodName }} - {{- if $field.NoErr }} - res := {{$field.GoMethodName}}({{ $field.CallArgs }}) - {{- else }} - res, err := {{$field.GoMethodName}}({{ $field.CallArgs }}) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } + resTmp := ec.FieldMiddleware(ctx, func(ctx context.Context) (interface{}, error) { + {{- if $field.IsResolver }} + return ec.resolvers.{{ $field.ShortInvocation }} + {{- else if $field.IsMethod }} + {{- if $field.NoErr }} + return {{$field.GoReceiverName}}.{{$field.GoFieldName}}({{ $field.CallArgs }}), nil + {{- else }} + return {{$field.GoReceiverName}}.{{$field.GoFieldName}}({{ $field.CallArgs }}) + {{- end }} + {{- else if $field.IsVariable }} + return {{$field.GoReceiverName}}.{{$field.GoFieldName}}, nil {{- end }} - {{- end }} + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.({{$field.Signature}}) {{ $field.WriteJson }} {{- if $field.IsConcurrent }} }) diff --git a/codegen/templates/generated.gotpl b/codegen/templates/generated.gotpl index cc1dc459b07..309185dd8fe 100644 --- a/codegen/templates/generated.gotpl +++ b/codegen/templates/generated.gotpl @@ -1,4 +1,4 @@ -// Code generated by github.com/vektah/gqlgen, DO NOT EDIT. +// Code generated by github.com/99designs/gqlgen, DO NOT EDIT. package {{ .PackageName }} @@ -8,22 +8,17 @@ import ( {{ end }} ) -// MakeExecutableSchema creates an ExecutableSchema from the Resolvers interface. -func MakeExecutableSchema(resolvers Resolvers) graphql.ExecutableSchema { - return &executableSchema{resolvers: resolvers} -} - // NewExecutableSchema creates an ExecutableSchema from the ResolverRoot interface. -func NewExecutableSchema(resolvers ResolverRoot) graphql.ExecutableSchema { - return MakeExecutableSchema(shortMapper{r: resolvers}) +func NewExecutableSchema(cfg Config) graphql.ExecutableSchema { + return &executableSchema{ + resolvers: cfg.Resolvers, + directives: cfg.Directives, + } } -type Resolvers interface { -{{- range $object := .Objects -}} - {{ range $field := $object.Fields -}} - {{ $field.ResolverDeclaration }} - {{ end }} -{{- end }} +type Config struct { + Resolvers ResolverRoot + Directives DirectiveRoot } type ResolverRoot interface { @@ -34,6 +29,12 @@ type ResolverRoot interface { {{- end }} } +type DirectiveRoot struct { +{{ range $directive := .Directives }} + {{ $directive.Declaration }} +{{ end }} +} + {{- range $object := .Objects -}} {{ if $object.HasResolvers }} type {{$object.GQLType}}Resolver interface { @@ -44,34 +45,21 @@ type ResolverRoot interface { {{- end }} {{- end }} -type shortMapper struct { - r ResolverRoot -} - -{{- range $object := .Objects -}} - {{ range $field := $object.Fields -}} - {{- if $field.IsResolver }} - func (s shortMapper) {{ $field.ResolverDeclaration }} { - return s.r.{{$field.ShortInvocation}} - } - {{- end }} - {{ end }} -{{- end }} - type executableSchema struct { - resolvers Resolvers + resolvers ResolverRoot + directives DirectiveRoot } -func (e *executableSchema) Schema() *schema.Schema { +func (e *executableSchema) Schema() *ast.Schema { return parsedSchema } -func (e *executableSchema) Query(ctx context.Context, op *query.Operation) *graphql.Response { +func (e *executableSchema) Query(ctx context.Context, op *ast.OperationDefinition) *graphql.Response { {{- if .QueryRoot }} - ec := executionContext{graphql.GetRequestContext(ctx), e.resolvers} + ec := executionContext{graphql.GetRequestContext(ctx), e} buf := ec.RequestMiddleware(ctx, func(ctx context.Context) []byte { - data := ec._{{.QueryRoot.GQLType}}(ctx, op.Selections) + data := ec._{{.QueryRoot.GQLType}}(ctx, op.SelectionSet) var buf bytes.Buffer data.MarshalGQL(&buf) return buf.Bytes() @@ -86,12 +74,12 @@ func (e *executableSchema) Query(ctx context.Context, op *query.Operation) *grap {{- end }} } -func (e *executableSchema) Mutation(ctx context.Context, op *query.Operation) *graphql.Response { +func (e *executableSchema) Mutation(ctx context.Context, op *ast.OperationDefinition) *graphql.Response { {{- if .MutationRoot }} - ec := executionContext{graphql.GetRequestContext(ctx), e.resolvers} + ec := executionContext{graphql.GetRequestContext(ctx), e} buf := ec.RequestMiddleware(ctx, func(ctx context.Context) []byte { - data := ec._{{.MutationRoot.GQLType}}(ctx, op.Selections) + data := ec._{{.MutationRoot.GQLType}}(ctx, op.SelectionSet) var buf bytes.Buffer data.MarshalGQL(&buf) return buf.Bytes() @@ -106,11 +94,11 @@ func (e *executableSchema) Mutation(ctx context.Context, op *query.Operation) *g {{- end }} } -func (e *executableSchema) Subscription(ctx context.Context, op *query.Operation) func() *graphql.Response { +func (e *executableSchema) Subscription(ctx context.Context, op *ast.OperationDefinition) func() *graphql.Response { {{- if .SubscriptionRoot }} - ec := executionContext{graphql.GetRequestContext(ctx), e.resolvers} + ec := executionContext{graphql.GetRequestContext(ctx), e} - next := ec._{{.SubscriptionRoot.GQLType}}(ctx, op.Selections) + next := ec._{{.SubscriptionRoot.GQLType}}(ctx, op.SelectionSet) if ec.Errors != nil { return graphql.OneShot(&graphql.Response{Data: []byte("null"), Errors: ec.Errors}) } @@ -140,8 +128,7 @@ func (e *executableSchema) Subscription(ctx context.Context, op *query.Operation type executionContext struct { *graphql.RequestContext - - resolvers Resolvers + *executableSchema } {{- range $object := .Objects }} @@ -160,16 +147,43 @@ type executionContext struct { {{ template "input.gotpl" $input }} {{- end }} +func (ec *executionContext) FieldMiddleware(ctx context.Context, next graphql.Resolver) interface{} { + {{- if .Directives }} + rctx := graphql.GetResolverContext(ctx) + for _, d := range rctx.Field.Definition.Directives { + switch d.Name { + {{- range $directive := .Directives }} + case "{{$directive.Name}}": + if ec.directives.{{$directive.Name|ucFirst}} != nil { + {{- if $directive.Args }} + rawArgs := d.ArgumentMap(ec.Variables) + {{ template "args.gotpl" $directive.Args }} + {{- end }} + n := next + next = func(ctx context.Context) (interface{}, error) { + return ec.directives.{{$directive.Name|ucFirst}}({{$directive.CallArgs}}) + } + } + {{- end }} + } + } + {{- end }} + res, err := ec.ResolverMiddleware(ctx, next) + if err != nil { + ec.Error(ctx, err) + return nil + } + return res +} + func (ec *executionContext) introspectSchema() *introspection.Schema { return introspection.WrapSchema(parsedSchema) } func (ec *executionContext) introspectType(name string) *introspection.Type { - t := parsedSchema.Resolve(name) - if t == nil { - return nil - } - return introspection.WrapType(t) + return introspection.WrapTypeFromDef(parsedSchema, parsedSchema.Types[name]) } -var parsedSchema = schema.MustParse({{.SchemaRaw|rawQuote}}) +var parsedSchema = gqlparser.MustLoadSchema( + &ast.Source{Name: {{.SchemaFilename|quote}}, Input: {{.SchemaRaw|rawQuote}}}, +) diff --git a/codegen/templates/input.gotpl b/codegen/templates/input.gotpl index 6073daf4ee6..f543608df0c 100644 --- a/codegen/templates/input.gotpl +++ b/codegen/templates/input.gotpl @@ -15,7 +15,7 @@ {{- range $field := .Fields }} case {{$field.GQLName|quote}}: var err error - {{ $field.Unmarshal (print "it." $field.GoVarName) "v" }} + {{ $field.Unmarshal (print "it." $field.GoFieldName) "v" }} if err != nil { return it, err } diff --git a/codegen/templates/interface.gotpl b/codegen/templates/interface.gotpl index 817d0abe540..84cbe5002c5 100644 --- a/codegen/templates/interface.gotpl +++ b/codegen/templates/interface.gotpl @@ -1,6 +1,6 @@ {{- $interface := . }} -func (ec *executionContext) _{{$interface.GQLType}}(ctx context.Context, sel []query.Selection, obj *{{$interface.FullName}}) graphql.Marshaler { +func (ec *executionContext) _{{$interface.GQLType}}(ctx context.Context, sel ast.SelectionSet, obj *{{$interface.FullName}}) graphql.Marshaler { switch obj := (*obj).(type) { case nil: return graphql.Null diff --git a/codegen/templates/models.gotpl b/codegen/templates/models.gotpl index e66266a520b..c9599333565 100644 --- a/codegen/templates/models.gotpl +++ b/codegen/templates/models.gotpl @@ -1,4 +1,4 @@ -// Code generated by github.com/vektah/gqlgen, DO NOT EDIT. +// Code generated by github.com/99designs/gqlgen, DO NOT EDIT. package {{ .PackageName }} @@ -14,8 +14,8 @@ import ( {{- else }} type {{.GoType}} struct { {{- range $field := .Fields }} - {{- if $field.GoVarName }} - {{ $field.GoVarName }} {{$field.Signature}} `json:"{{$field.GQLName}}"` + {{- if $field.GoFieldName }} + {{ $field.GoFieldName }} {{$field.Signature}} `json:"{{$field.GQLName}}"` {{- else }} {{ $field.GoFKName }} {{$field.GoFKType}} {{- end }} diff --git a/codegen/templates/object.gotpl b/codegen/templates/object.gotpl index b531d5fe6d0..9535a65a6d6 100644 --- a/codegen/templates/object.gotpl +++ b/codegen/templates/object.gotpl @@ -4,8 +4,8 @@ var {{ $object.GQLType|lcFirst}}Implementors = {{$object.Implementors}} // nolint: gocyclo, errcheck, gas, goconst {{- if .Stream }} -func (ec *executionContext) _{{$object.GQLType}}(ctx context.Context, sel []query.Selection) func() graphql.Marshaler { - fields := graphql.CollectFields(ec.Doc, sel, {{$object.GQLType|lcFirst}}Implementors, ec.Variables) +func (ec *executionContext) _{{$object.GQLType}}(ctx context.Context, sel ast.SelectionSet) func() graphql.Marshaler { + fields := graphql.CollectFields(ctx, sel, {{$object.GQLType|lcFirst}}Implementors) ctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{ Object: {{$object.GQLType|quote}}, }) @@ -24,8 +24,8 @@ func (ec *executionContext) _{{$object.GQLType}}(ctx context.Context, sel []quer } } {{- else }} -func (ec *executionContext) _{{$object.GQLType}}(ctx context.Context, sel []query.Selection{{if not $object.Root}}, obj *{{$object.FullName}} {{end}}) graphql.Marshaler { - fields := graphql.CollectFields(ec.Doc, sel, {{$object.GQLType|lcFirst}}Implementors, ec.Variables) +func (ec *executionContext) _{{$object.GQLType}}(ctx context.Context, sel ast.SelectionSet{{if not $object.Root}}, obj *{{$object.FullName}} {{end}}) graphql.Marshaler { + fields := graphql.CollectFields(ctx, sel, {{$object.GQLType|lcFirst}}Implementors) {{if $object.Root}} ctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{ Object: {{$object.GQLType|quote}}, diff --git a/codegen/templates/resolver.gotpl b/codegen/templates/resolver.gotpl new file mode 100644 index 00000000000..dd8acf24d86 --- /dev/null +++ b/codegen/templates/resolver.gotpl @@ -0,0 +1,33 @@ +//go:generate gorunpkg github.com/99designs/gqlgen + +package {{ .PackageName }} + +import ( +{{- range $import := .Imports }} + {{- $import.Write }} +{{ end }} +) + +type {{.ResolverType}} struct {} + +{{ range $object := .Objects -}} + {{- if $object.HasResolvers -}} + func (r *{{$.ResolverType}}) {{$object.GQLType}}() {{ $object.ResolverInterface.FullName }} { + return &{{lcFirst $object.GQLType}}Resolver{r} + } + {{ end -}} +{{ end }} + +{{ range $object := .Objects -}} + {{- if $object.HasResolvers -}} + type {{lcFirst $object.GQLType}}Resolver struct { *Resolver } + + {{ range $field := $object.Fields -}} + {{- if $field.IsResolver -}} + func (r *{{lcFirst $object.GQLType}}Resolver) {{ $field.ShortResolverDeclaration }} { + panic("not implemented") + } + {{ end -}} + {{ end -}} + {{ end -}} +{{ end }} diff --git a/codegen/templates/server.gotpl b/codegen/templates/server.gotpl new file mode 100644 index 00000000000..f23b30e1166 --- /dev/null +++ b/codegen/templates/server.gotpl @@ -0,0 +1,22 @@ +package main + +import ( +{{- range $import := .Imports }} + {{- $import.Write }} +{{ end }} +) + +const defaultPort = "8080" + +func main() { + port := os.Getenv("PORT") + if port == "" { + port = defaultPort + } + + http.Handle("/", handler.Playground("GraphQL playground", "/query")) + http.Handle("/query", handler.GraphQL({{.ExecPackageName}}.NewExecutableSchema({{.ExecPackageName}}.Config{Resolvers: &{{.ResolverPackageName}}.Resolver{}}))) + + log.Printf("connect to http://localhost:%s/ for GraphQL playground", port) + log.Fatal(http.ListenAndServe(":" + port, nil)) +} diff --git a/codegen/templates/templates.go b/codegen/templates/templates.go index 3d29b403aac..df909cb5747 100644 --- a/codegen/templates/templates.go +++ b/codegen/templates/templates.go @@ -5,11 +5,19 @@ package templates import ( "bytes" "fmt" + "io/ioutil" + "os" + "path/filepath" "sort" "strconv" "strings" "text/template" "unicode" + + "log" + + "github.com/pkg/errors" + "golang.org/x/tools/imports" ) func Run(name string, tpldata interface{}) (*bytes.Buffer, error) { @@ -96,6 +104,8 @@ func dump(val interface{}) string { switch val := val.(type) { case int: return strconv.Itoa(val) + case int64: + return fmt.Sprintf("%d", val) case float64: return fmt.Sprintf("%f", val) case string: @@ -137,3 +147,47 @@ func dump(val interface{}) string { func prefixLines(prefix, s string) string { return prefix + strings.Replace(s, "\n", "\n"+prefix, -1) } + +func RenderToFile(tpl string, filename string, data interface{}) error { + var buf *bytes.Buffer + buf, err := Run(tpl, data) + if err != nil { + return errors.Wrap(err, filename+" generation failed") + } + + if err := write(filename, buf.Bytes()); err != nil { + return err + } + + log.Println(filename) + + return nil +} + +func gofmt(filename string, b []byte) ([]byte, error) { + out, err := imports.Process(filename, b, nil) + if err != nil { + return b, errors.Wrap(err, "unable to gofmt") + } + return out, nil +} + +func write(filename string, b []byte) error { + err := os.MkdirAll(filepath.Dir(filename), 0755) + if err != nil { + return errors.Wrap(err, "failed to create directory") + } + + formatted, err := gofmt(filename, b) + if err != nil { + fmt.Fprintf(os.Stderr, "gofmt failed: %s\n", err.Error()) + formatted = b + } + + err = ioutil.WriteFile(filename, formatted, 0644) + if err != nil { + return errors.Wrapf(err, "failed to write %s", filename) + } + + return nil +} diff --git a/codegen/testdata/cfg/gqlgen.yml b/codegen/tests/cfg/gqlgen.yml similarity index 100% rename from codegen/testdata/cfg/gqlgen.yml rename to codegen/tests/cfg/gqlgen.yml diff --git a/codegen/testdata/cfg/malformedconfig.yml b/codegen/tests/cfg/malformedconfig.yml similarity index 100% rename from codegen/testdata/cfg/malformedconfig.yml rename to codegen/tests/cfg/malformedconfig.yml diff --git a/codegen/testdata/cfg/otherdir/.gitkeep b/codegen/tests/cfg/otherdir/.gitkeep similarity index 100% rename from codegen/testdata/cfg/otherdir/.gitkeep rename to codegen/tests/cfg/otherdir/.gitkeep diff --git a/codegen/testdata/cfg/subdir/gqlgen.yaml b/codegen/tests/cfg/subdir/gqlgen.yaml similarity index 100% rename from codegen/testdata/cfg/subdir/gqlgen.yaml rename to codegen/tests/cfg/subdir/gqlgen.yaml diff --git a/codegen/testdata/cfg/unknownkeys.yml b/codegen/tests/cfg/unknownkeys.yml similarity index 100% rename from codegen/testdata/cfg/unknownkeys.yml rename to codegen/tests/cfg/unknownkeys.yml diff --git a/codegen/testdata/element.go b/codegen/tests/element.go similarity index 97% rename from codegen/testdata/element.go rename to codegen/tests/element.go index feaed69f41f..5ba273dcf99 100644 --- a/codegen/testdata/element.go +++ b/codegen/tests/element.go @@ -1,4 +1,4 @@ -package testdata +package tests import ( "context" diff --git a/codegen/testdata/interfaces.go b/codegen/tests/interfaces.go similarity index 95% rename from codegen/testdata/interfaces.go rename to codegen/tests/interfaces.go index ead09b16084..c8daa473045 100644 --- a/codegen/testdata/interfaces.go +++ b/codegen/tests/interfaces.go @@ -1,4 +1,4 @@ -package testdata +package tests import "math" diff --git a/codegen/testdata/introspection/it.go b/codegen/tests/introspection/it.go similarity index 100% rename from codegen/testdata/introspection/it.go rename to codegen/tests/introspection/it.go diff --git a/codegen/testdata/invalid-packagename/invalid-identifier.go b/codegen/tests/invalid-packagename/invalid-identifier.go similarity index 100% rename from codegen/testdata/invalid-packagename/invalid-identifier.go rename to codegen/tests/invalid-packagename/invalid-identifier.go diff --git a/codegen/testdata/recursive.go b/codegen/tests/recursive.go similarity index 79% rename from codegen/testdata/recursive.go rename to codegen/tests/recursive.go index 65854f7721b..c3fccea6ba5 100644 --- a/codegen/testdata/recursive.go +++ b/codegen/tests/recursive.go @@ -1,4 +1,4 @@ -package testdata +package tests type RecursiveInputSlice struct { Self []RecursiveInputSlice diff --git a/codegen/type.go b/codegen/type.go index 7af24b3c83c..04bb0954315 100644 --- a/codegen/type.go +++ b/codegen/type.go @@ -26,8 +26,8 @@ type Ref struct { type Type struct { *NamedType - Modifiers []string - CastType *Ref // the type to cast to when unmarshalling + Modifiers []string + AliasedType *Ref } const ( @@ -47,6 +47,9 @@ func (t Ref) PkgDot() string { } func (t Type) Signature() string { + if t.AliasedType != nil { + return strings.Join(t.Modifiers, "") + t.AliasedType.FullName() + } return strings.Join(t.Modifiers, "") + t.FullName() } @@ -109,6 +112,8 @@ func (t Type) unmarshal(result, raw string, remainingMods []string, depth int) s if {{.raw}} != nil { if tmp1, ok := {{.raw}}.([]interface{}); ok { {{.rawSlice}} = tmp1 + } else { + {{.rawSlice}} = []interface{}{ {{.raw}} } } } {{.result}} = make({{.type}}, len({{.rawSlice}})) @@ -125,11 +130,11 @@ func (t Type) unmarshal(result, raw string, remainingMods []string, depth int) s } realResult := result - if t.CastType != nil { + if t.AliasedType != nil { result = "castTmp" } - return tpl(`{{- if .t.CastType }} + return tpl(`{{- if .t.AliasedType }} var castTmp {{.t.FullName}} {{ end }} {{- if eq .t.GoType "map[string]interface{}" }} @@ -139,8 +144,8 @@ func (t Type) unmarshal(result, raw string, remainingMods []string, depth int) s {{- else -}} err = (&{{.result}}).UnmarshalGQL({{.raw}}) {{- end }} - {{- if .t.CastType }} - {{ .realResult }} = {{.t.CastType.FullName}}(castTmp) + {{- if .t.AliasedType }} + {{ .realResult }} = {{.t.AliasedType.FullName}}(castTmp) {{- end }}`, map[string]interface{}{ "realResult": realResult, "result": result, @@ -150,7 +155,7 @@ func (t Type) unmarshal(result, raw string, remainingMods []string, depth int) s } func (t Type) Marshal(val string) string { - if t.CastType != nil { + if t.AliasedType != nil { val = t.GoType + "(" + val + ")" } diff --git a/codegen/type_build.go b/codegen/type_build.go index ba2874b0572..127791c0245 100644 --- a/codegen/type_build.go +++ b/codegen/type_build.go @@ -1,12 +1,10 @@ package codegen import ( - "fmt" "go/types" "strings" - "github.com/vektah/gqlgen/neelance/common" - "github.com/vektah/gqlgen/neelance/schema" + "github.com/vektah/gqlparser/ast" "golang.org/x/tools/go/loader" ) @@ -20,7 +18,7 @@ func (cfg *Config) buildNamedTypes() NamedTypes { t.IsUserDefined = true t.Package, t.GoType = pkgAndType(userEntry.Model) } else if t.IsScalar { - t.Package = "github.com/vektah/gqlgen/graphql" + t.Package = "github.com/99designs/gqlgen/graphql" t.GoType = "String" } @@ -50,16 +48,16 @@ func (cfg *Config) bindTypes(imports *Imports, namedTypes NamedTypes, destDir st // namedTypeFromSchema objects for every graphql type, including primitives. // don't recurse into object fields or interfaces yet, lets make sure we have collected everything first. -func namedTypeFromSchema(schemaType schema.NamedType) *NamedType { - switch val := schemaType.(type) { - case *schema.Scalar, *schema.Enum: - return &NamedType{GQLType: val.TypeName(), IsScalar: true} - case *schema.Interface, *schema.Union: - return &NamedType{GQLType: val.TypeName(), IsInterface: true} - case *schema.InputObject: - return &NamedType{GQLType: val.TypeName(), IsInput: true} +func namedTypeFromSchema(schemaType *ast.Definition) *NamedType { + switch schemaType.Kind { + case ast.Scalar, ast.Enum: + return &NamedType{GQLType: schemaType.Name, IsScalar: true} + case ast.Interface, ast.Union: + return &NamedType{GQLType: schemaType.Name, IsInterface: true} + case ast.InputObject: + return &NamedType{GQLType: schemaType.Name, IsInput: true} default: - return &NamedType{GQLType: val.TypeName()} + return &NamedType{GQLType: schemaType.Name} } } @@ -73,40 +71,29 @@ func pkgAndType(name string) (string, string) { return normalizeVendor(strings.Join(parts[:len(parts)-1], ".")), parts[len(parts)-1] } -func (n NamedTypes) getType(t common.Type) *Type { +func (n NamedTypes) getType(t *ast.Type) *Type { var modifiers []string - usePtr := true for { - if _, nonNull := t.(*common.NonNull); nonNull { - usePtr = false - } else if _, nonNull := t.(*common.List); nonNull { - usePtr = true + if t.Elem != nil { + modifiers = append(modifiers, modList) + t = t.Elem } else { - if usePtr { + if !t.NonNull { modifiers = append(modifiers, modPtr) } - usePtr = true - } - - switch val := t.(type) { - case *common.NonNull: - t = val.OfType - case *common.List: - modifiers = append(modifiers, modList) - t = val.OfType - case schema.NamedType: - t := &Type{ - NamedType: n[val.TypeName()], + if n[t.NamedType] == nil { + panic("missing type " + t.NamedType) + } + res := &Type{ + NamedType: n[t.NamedType], Modifiers: modifiers, } - if t.IsInterface { - t.StripPtr() + if res.IsInterface { + res.StripPtr() } - return t - default: - panic(fmt.Errorf("unknown type %T", t)) + return res } } } diff --git a/codegen/util.go b/codegen/util.go index 5ff41074324..f1732ad1125 100644 --- a/codegen/util.go +++ b/codegen/util.go @@ -194,7 +194,11 @@ func bindMethod(imports *Imports, t types.Type, field *Field) error { return fmt.Errorf("not a named type") } - method := findMethod(namedType, field.GQLName) + goName := field.GQLName + if field.GoFieldName != "" { + goName = field.GoFieldName + } + method := findMethod(namedType, goName) if method == nil { return fmt.Errorf("no method named %s", field.GQLName) } @@ -216,7 +220,9 @@ func bindMethod(imports *Imports, t types.Type, field *Field) error { } // success, args and return type match. Bind to method - field.GoMethodName = "obj." + method.Name() + field.GoFieldType = GoFieldMethod + field.GoReceiverName = "obj" + field.GoFieldName = method.Name() field.Args = newArgs return nil } @@ -227,7 +233,11 @@ func bindVar(imports *Imports, t types.Type, field *Field) error { return fmt.Errorf("not a struct") } - structField := findField(underlying, field.GQLName) + goName := field.GQLName + if field.GoFieldName != "" { + goName = field.GoFieldName + } + structField := findField(underlying, goName) if structField == nil { return fmt.Errorf("no field named %s", field.GQLName) } @@ -237,7 +247,9 @@ func bindVar(imports *Imports, t types.Type, field *Field) error { } // success, bind to var - field.GoVarName = structField.Name() + field.GoFieldType = GoFieldVariable + field.GoReceiverName = "obj" + field.GoFieldName = structField.Name() return nil } @@ -276,7 +288,7 @@ func validateTypeBinding(imports *Imports, field *Field, goType types.Type) erro field.Type.Modifiers = modifiersFromGoType(goType) pkg, typ := pkgAndType(goType.String()) imp := imports.findByPath(pkg) - field.CastType = &Ref{GoType: typ, Import: imp} + field.AliasedType = &Ref{GoType: typ, Import: imp} return nil } diff --git a/docs/content/config.md b/docs/content/config.md index 2e7bb8825f4..8415fa37684 100644 --- a/docs/content/config.md +++ b/docs/content/config.md @@ -22,6 +22,11 @@ model: filename: models/generated.go package: models +# Optional, turns on resolver stub generation +resolver: + filename: resolver.go # where to write them + type: Resolver # whats the resolver root implementation type called? + # Tell gqlgen about any existing models you want to reuse for # graphql. These normally come from the db or a remote api. models: @@ -32,6 +37,7 @@ models: fields: id: resolver: true # force a resolver to be generated + fieldName: todoId # bind to a different go field name ``` Everything has defaults, so add things as you need. diff --git a/docs/content/directives.md b/docs/content/directives.md new file mode 100644 index 00000000000..9db95711ac7 --- /dev/null +++ b/docs/content/directives.md @@ -0,0 +1,63 @@ +--- +title: Using schema directives to implement permission checks +description: Implementing graphql schema directives in golang for permission checks. +linkTitle: Schema Directives +menu: main +--- + +Directives are a bit like annotations in any other language. They give you a way to specify some behaviour without directly binding to the implementation. This can be really useful for cross cutting concerns like permission checks. + +**Note**: The current directives implementation is still fairly limited, and is designed to cover the most common "field middleware" case. + +## Declare it in the schema + +Directives are declared in your schema, along with all your other types. Lets define a @hasRole directive: + +```graphql +directive @hasRole(role: Role!) on FIELD_DEFINITION + +enum Role { + ADMIN + USER +} +``` + +When we next run go generate, gqlgen will add this directive to the DirectiveRoot +```go +type DirectiveRoot struct { + HasRole func(ctx context.Context, next graphql.Resolver, role Role) (res interface{}, err error) +} +``` + + +## Use it in the schema + +We can call this on any field definition now: +```graphql +type Mutation { + deleteUser(userID: ID!): Bool @hasRole(role: ADMIN) +} +``` + +## Implement the directive + +Finally, we need to implement the directive, and pass it in when startin gthe server: +```go +package main + +func main() { + c := Config{ Resolvers: &resolvers{} } + c.Directives.HasRole = func(ctx context.Context, next graphql.Resolver, role Role) (interface{}, error) { + if !getCurrentUser(ctx).HasRole(role) { + // block calling the next resolver + return nil, fmt.Errorf("Access denied") + } + + // or let it pass through + return next(ctx) + } + + http.Handle("/query", handler.GraphQL(todo.NewExecutableSchema(c), )) + log.Fatal(http.ListenAndServe(":8081", nil)) +} +``` diff --git a/docs/content/getting-started.md b/docs/content/getting-started.md index 292ac856e5c..15b24d0da54 100644 --- a/docs/content/getting-started.md +++ b/docs/content/getting-started.md @@ -14,24 +14,24 @@ The aim for this tutorial is to build a "todo" graphql server that can: - 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) +You can find the finished code for this tutorial [here](https://github.com/99designs/gqlgen-tutorials/tree/master/gettingstarted) ## Install gqlgen Assuming you already have a working [go environment](https://golang.org/doc/install) you can simply go get: ```sh -go get -u github.com/vektah/gqlgen +go get -u github.com/99designs/gqlgen github.com/vektah/gorunpkg ``` - ## Building the server -### Define the schema first +### Define the schema gqlgen is a schema-first library, so before touching any code we write out the API we want using the graphql [Schema Definition Language](http://graphql.org/learn/schema/). This usually goes into a file called schema.graphql +`schema.graphql` ```graphql type Todo { id: ID! @@ -59,24 +59,25 @@ type Mutation { } ``` +### Create the project skeleton +```bash +$ gqlgen init +Exec "go run ./server/server.go" to start GraphQL server +``` + +This has created an empty skeleton with all files we 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 models you write yourself. 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. + ### Create the database models -Now we define some types, these are stand-ins for your database layer. Feel free to use whatever ORM you are familiar -with, or roll it yourself. For this example we are just going to use some structs and keep them in memory. +The generated model for Todo isnt quite right, it has a user embeded in it but we only want to fetch it if the user actually requested it. So lets make our own. -`model/user.go` +`todo.go` ```go -package model - -type User struct { - ID string - Name string -} -``` - -`model/todo.go` -```go -package model +package gettingstarted type Todo struct { ID string @@ -84,55 +85,41 @@ type Todo struct { Done bool UserID string } - ``` -### Generate the graphql runtime - -So we have our schema and our models, now we need to link them up: - -`gqlgen.yml` - [Read more about the config]({{< ref "config.md" >}}) +And then tell gqlgen to use this new struct by adding this to the gqlgen.yml: ```yaml -schema: schema.graphql -exec: - filename: graph/generated.go -model: - filename: model/generated.go - models: Todo: - model: github.com/vektah/gqlgen-tutorials/gettingstarted/model.Todo - User: - model: github.com/vektah/gqlgen-tutorials/gettingstarted/model.User + model: github.com/99designs/gqlgen-tutorials/gettingstarted.Todo ``` -This simply says, `User` in schema is backed by `graph.User` in go. - -gqlgen is going to look at all the models in the schema and see if they are in this map, if they arent -it will create a struct for us. For the models that are there its going to match up each field in the -struct with fields in the schema: - - 1. If there is a property that matches, use it - 2. If there is a method that matches, use it - 3. Otherwise, add it to the Resolvers interface. This is the magic. - -### Generate the bindings - - -Lets generate the server now: - +and regenerate by running ```bash -$ gqlgen +$ gqlgen -v +Unable to bind Todo.user to github.com/99designs/gqlgen-tutorials/gettingstarted.Todo + no method named user + no field named user + Adding resolver method ``` +*note* we've used the verbose flag here to show what gqlgen is doing. Its looked at all the fields on our model and found matching methods for all of them, except user. For user it added a resolver to the interface we need to implement. This is the magic that makes gqlgen work so well. -gqlgen should have created two new files `graph/generated.go` and `models/generated.go`. If we take a peek in both -we can see what the server has generated: +### 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 -// graph/generated.go // NewExecutableSchema creates an ExecutableSchema from the ResolverRoot interface. -func NewExecutableSchema(resolvers ResolverRoot) graphql.ExecutableSchema { - return MakeExecutableSchema(shortMapper{r: resolvers}) +func NewExecutableSchema(cfg Config) graphql.ExecutableSchema { + return &executableSchema{ + resolvers: cfg.Resolvers, + directives: cfg.Directives, + } +} + +type Config struct { + Resolvers ResolverRoot + Directives DirectiveRoot } type ResolverRoot interface { @@ -140,25 +127,22 @@ type ResolverRoot interface { Query() QueryResolver Todo() TodoResolver } + +type DirectiveRoot struct { +} type MutationResolver interface { - CreateTodo(ctx context.Context, input models.NewTodo) (model.Todo, error) + CreateTodo(ctx context.Context, input NewTodo) (Todo, error) } type QueryResolver interface { - Todos(ctx context.Context) ([]model.Todo, error) + Todos(ctx context.Context) ([]Todo, error) } type TodoResolver interface { - User(ctx context.Context, obj *model.Todo) (model.User, error) -} - -// graph/models_gen.go -type NewTodo struct { - Text string `json:"text"` - User string `json:"user"` + 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 everything else for us. +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 @@ -166,92 +150,76 @@ better than generating. ### Write the resolvers -All thats left for us to do now is fill in the blanks in that interface: +This is a work in progress, we have a way to generate resolver stubs, but it only cant currently update existing code. We can force it to run again by deleting `resolvers.go` and re-running gqlgen: +```bash +rm resolvers.go +gqlgen +``` + +Now we just need to fill in the `not implemented` parts + `graph/graph.go` ```go -package graph +//go:generate gorunpkg github.com/99designs/gqlgen + +package gettingstarted import ( - "context" + context "context" "fmt" "math/rand" - - "github.com/vektah/gqlgen-tutorials/gettingstarted/model" ) -type App struct { - todos []model.Todo -} - -func (a *App) Mutation() MutationResolver { - return &mutationResolver{a} +type Resolver struct{ + todos []Todo } -func (a *App) Query() QueryResolver { - return &queryResolver{a} +func (r *Resolver) Mutation() MutationResolver { + return &mutationResolver{r} } - -func (a *App) Todo() TodoResolver { - return &todoResolver{a} +func (r *Resolver) Query() QueryResolver { + return &queryResolver{r} } - -type queryResolver struct{ *App } - -func (a *queryResolver) Todos(ctx context.Context) ([]model.Todo, error) { - return a.todos, nil +func (r *Resolver) Todo() TodoResolver { + return &todoResolver{r} } -type mutationResolver struct{ *App } +type mutationResolver struct{ *Resolver } -func (a *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (model.Todo, error) { - todo := model.Todo{ +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, + UserID: input.UserID, } - a.todos = append(a.todos, todo) + r.todos = append(r.todos, todo) return todo, nil } -type todoResolver struct{ *App } +type queryResolver struct{ *Resolver } -func (a *todoResolver) User(ctx context.Context, it *model.Todo) (model.User, error) { - return model.User{ID: it.UserID, Name: "user " + it.UserID}, nil +func (r *queryResolver) Todos(ctx context.Context) ([]Todo, error) { + return r.todos, nil } -``` - -`main.go` -```go -package main - -import ( - "fmt" - "log" - "net/http" - - "github.com/vektah/gqlgen-tutorials/gettingstarted/graph" - "github.com/vektah/gqlgen/handler" -) -func main() { - http.Handle("/", handler.Playground("Todo", "/query")) - http.Handle("/query", handler.GraphQL(graph.NewExecutableSchema(&graph.App{}))) +type todoResolver struct{ *Resolver } - fmt.Println("Listening on :8080") - log.Fatal(http.ListenAndServe(":8080", nil)) +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 main.go +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", user:"1"}) { + createTodo(input:{text:"todo", userId:"1"}) { user { id } @@ -282,7 +250,7 @@ First uninstall the global version we grabbed earlier. This is a good way to pre ```bash rm ~/go/bin/gqlgen -rm -rf ~/go/src/github.com/vektah/gqlgen +rm -rf ~/go/src/github.com/99designs/gqlgen ``` Next install gorunpkg, its kind of like npx but only searches vendor. @@ -290,24 +258,17 @@ Next install gorunpkg, its kind of like npx but only searches vendor. ```bash dep init dep ensure -go get github.com/vektah/gorunpkg ``` -Now at the top of our graph.go: +At the top of our resolvers.go a go generate command was added that looks like this: ```go -//go:generate gorunpkg github.com/vektah/gqlgen - -package graph +//go:generate gorunpkg github.com/99designs/gqlgen ``` -**Note:** be careful formatting this, there must no space between the `//` and `go:generate`, and one empty line -between it and the `package main`. - This magic comment tells `go generate` what command to run when we want to regenerate our code. to do so run: ```go go generate ./... ``` -*gorunpkg* will build and run the version of gqlgen we just installed into vendor with dep. This makes sure -that everyone working on your project generates code the same way regardless which binaries are installed in their gopath. +*gorunpkg* will build and run the version of gqlgen we just installed into vendor with dep. This makes sure that everyone working on your project generates code the same way regardless which binaries are installed in their gopath. diff --git a/docs/content/introduction.md b/docs/content/introduction.md index 45e1a04b21e..67267f9336a 100644 --- a/docs/content/introduction.md +++ b/docs/content/introduction.md @@ -7,7 +7,7 @@ date: 2018-03-17T13:06:47+11:00 ## What is gqlgen? -[gqlgen](https://github.com/vektah/gqlgen) is golang library for building graphql servers without any fuss. gqlgen is: +[gqlgen](https://github.com/99designs/gqlgen) is golang library for building graphql servers without any fuss. gqlgen is: - Schema first: You define your API using the graphql [Schema Definition Language](http://graphql.org/learn/schema/) - Type safe: You should never see `map[string]interface{}` here. @@ -18,9 +18,9 @@ date: 2018-03-17T13:06:47+11:00 First take a look at the [Getting Started]({{< ref "getting-started.md" >}}) tutorial. -If you cant find what your looking for, maybe the [examples](https://github.com/vektah/gqlgen/tree/master/example) will help. +If you cant find what your looking for, maybe the [examples](https://github.com/99designs/gqlgen/tree/master/example) will help. ## Getting help -If you think you've found bug, or something isnt behaving the way you think it should please raise an [issue](https://github.com/vektah/gqlgen/issues) on github. +If you think you've found bug, or something isnt behaving the way you think it should please raise an [issue](https://github.com/99designs/gqlgen/issues) on github. diff --git a/docs/content/reference/dataloaders.md b/docs/content/reference/dataloaders.md index ad6c5506871..862d1f7166e 100644 --- a/docs/content/reference/dataloaders.md +++ b/docs/content/reference/dataloaders.md @@ -152,4 +152,4 @@ The generated UserLoader has a few other useful methods on it: - `LoadAll(keys)`: If you know up front you want a bunch users - `Prime(key, user)`: Used to sync state between similar loaders (usersById, usersByNote) -You can see the full working example [here](https://github.com/vektah/gqlgen-tutorials/tree/master/dataloader) +You can see the full working example [here](https://github.com/99designs/gqlgen-tutorials/tree/master/dataloader) diff --git a/docs/content/reference/scalars.md b/docs/content/reference/scalars.md index 775ed748555..5a126daae00 100644 --- a/docs/content/reference/scalars.md +++ b/docs/content/reference/scalars.md @@ -68,7 +68,7 @@ import ( "io" "strings" - "github.com/vektah/gqlgen/graphql" + "github.com/99designs/gqlgen/graphql" ) @@ -103,4 +103,4 @@ models: model: github.com/me/mypkg.MyCustomBooleanScalar ``` -see the [example/scalars](https://github.com/vektah/gqlgen/tree/master/example/scalars) package for more examples. +see the [example/scalars](https://github.com/99designs/gqlgen/tree/master/example/scalars) package for more examples. diff --git a/docs/layouts/_default/baseof.html b/docs/layouts/_default/baseof.html index 4d02b77d79a..3f274ce85ae 100644 --- a/docs/layouts/_default/baseof.html +++ b/docs/layouts/_default/baseof.html @@ -11,7 +11,7 @@