diff --git a/README.md b/README.md index 5655ffcb718..0e02d724bff 100644 --- a/README.md +++ b/README.md @@ -2,32 +2,16 @@ This is a library for quickly creating strictly typed graphql servers in golang. -`dep ensure -add github.com/vektah/gqlgen` +### Getting started -Please use [dep](https://github.com/golang/dep) to pin your versions, the apis here should be considered unstable. - -Ideally you should version the binary used to generate the code, as well as the library itself. Version mismatches -between the generated code and the runtime will be ugly. [gorunpkg](https://github.com/vektah/gorunpkg) makes this -as easy as: - -Gopkg.toml -```toml -required = ["github.com/vektah/gqlgen"] -``` - -then -```go -//go:generate gorunpkg github.com/vektah/gqlgen -out generated.go +#### install gqlgen +```bash +go get github.com/vektah/gqlgen ``` -#### Todo - - [ ] opentracing - - [ ] subscriptions - -### Try it - -Define your schema first: +#### define a schema +schema.graphql ```graphql schema schema { query: Query @@ -55,57 +39,207 @@ type User { } ``` -Then define your models: + +#### generate the bindings + + +gqlgen can then take the schema and generate all the code needed to execute incoming graphql queries in a safe, +strictly typed manner: +```bash +gqlgen -out generated.go -package main +``` + +If you look at the top of `generated.go` it has created an interface and some temporary models: + ```go -package yourapp +func MakeExecutableSchema(resolvers Resolvers) graphql.ExecutableSchema { + return &executableSchema{resolvers} +} + +type Resolvers interface { + Mutation_createTodo(ctx context.Context, text string) (Todo, error) + Query_todos(ctx context.Context) ([]Todo, error) + Todo_user(ctx context.Context, it *Todo) (User, error) +} type Todo struct { ID string Text string Done bool - UserID int + UserID string } type User struct { - ID string - Name string + ID string + Name string +} + +type executableSchema struct { + resolvers Resolvers +} + +func (e *executableSchema) Schema() *schema.Schema { + return parsedSchema } ``` -Tell the generator how to map between the two in `types.json` +Notice that only the scalar types were added to the model? Todo.user doesnt exist on the struct, instead a resolver +method has been added. Resolver methods have a simple naming convention of {Type}_{field}. + +You're probably thinking why not just have a method on the user struct? Well, you can. But its assumed it will be a +getter method and wont be hitting the database, so parallel execution is disabled and you dont have access to any +database context. Plus, your models probably shouldn't be responsible for fetching more data. To define methods on the +model you will need to copy it out of the generated code and define it in types.json. + + +**Note**: ctx here is the golang context.Context, its used to pass per-request context like url params, tracing +information, cancellation, and also the current selection set. This makes it more like the `info` argument in +`graphql-js`. Because the caller will create an object to satisfy the interface, they can inject any dependencies in +directly. + +#### write our resolvers +Now we need to join the edges of the graph up. + +main.go: +```go +package main + +import ( + "context" + "fmt" + "log" + "math/rand" + "net/http" + + "github.com/vektah/gqlgen/handler" +) + +type MyApp struct { + todos []Todo +} + +func (a *MyApp) Mutation_createTodo(ctx context.Context, text string) (Todo, error) { + todo := Todo{ + Text: text, + ID: fmt.Sprintf("T%d", rand.Int()), + UserID: fmt.Sprintf("U%d", rand.Int()), + } + a.todos = append(a.todos, todo) + return todo, nil +} + +func (a *MyApp) Query_todos(ctx context.Context) ([]Todo, error) { + return a.todos, nil +} + +func (a *MyApp) Todo_user(ctx context.Context, it *Todo) (User, error) { + return User{ID: it.UserID, Name: "user " + it.UserID}, nil +} + +func main() { + app := &MyApp{ + todos: []Todo{}, // this would normally be a reference to the db + } + http.Handle("/", handler.Playground("Dataloader", "/query")) + http.Handle("/query", handler.GraphQL(MakeExecutableSchema(app))) + + fmt.Println("Listening on :8080") + log.Fatal(http.ListenAndServe(":8080", nil)) +} +``` + +We now have a working server, to start it: +```bash +go run *.go +``` + +then open http://localhost:8080 in a browser. here are some queries to try: +```graphql +mutation createTodo { + createTodo(text:"test") { + user { + id + } + text + done + } +} + +query findTodos { + todos { + text + done + user { + name + } + } +} +``` + +#### customizing the models or reusing your existing ones + +Generated models are nice to get moving quickly, but you probably want control over them at some point. To do that +create a types.json, eg: ```json { - "Todo": "github.com/you/yourapp.Todo", - "User": "github.com/you/yourapp.User" + "Todo": "github.com/vektah/gettingstarted.Todo" +} +``` + +and create the model yourself: +```go +type Todo struct { + ID string + Text string + done bool + userID string // I've made userID private now. } + +// lets define a getter too. it could also return an error if we needed. +func (t Todo) Done() bool { + return t.done +} + ``` -Then generate the runtime from it: +then regenerate, this time specifying the type map: + ```bash -gqlgen -out generated.go +gqlgen -out generated.go -package main -typemap types.json ``` -At the top of the generated file will be an interface with the resolvers that are required to complete the graph: -```go -package yourapp +gqlgen will look at the user defined types and match the fields up finding fields and functions by matching names. -type Resolvers interface { - Mutation_createTodo(ctx context.Context, text string) (Todo, error) - Query_todos(ctx context.Context) ([]Todo, error) +#### Finishing touches - Todo_user(ctx context.Context, it *Todo) (User, error) -} +gqlgen is still unstable, and the APIs may change at any time. To prevent changes from ruining your day make sure +to lock your dependencies: + +```bash +dep init +dep ensure +go get github.com/vektah/gorunpkg ``` -implement this interface, then create a server with by passing it into the generated code: -```go -func main() { - http.Handle("/query", graphql.Handler(gen.NewResolver(yourResolvers{}))) +at the top of our main.go: +```go +//go:generate gorunpkg github.com/vektah/gqlgen -typemap types.json -out generated.go -package main - log.Fatal(http.ListenAndServe(":8080", nil)) -} +package main ``` +**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. + ### Prior art diff --git a/codegen/build.go b/codegen/build.go index eb5fb11b170..ea0e98d37b4 100644 --- a/codegen/build.go +++ b/codegen/build.go @@ -2,6 +2,7 @@ package codegen import ( "go/build" + "go/types" "os" "path/filepath" @@ -12,6 +13,7 @@ import ( type Build struct { PackageName string Objects Objects + Models Objects Inputs Objects Interfaces []*Interface Imports Imports @@ -33,9 +35,12 @@ func Bind(schema *schema.Schema, userTypes map[string]string, destDir string) (* bindTypes(imports, namedTypes, prog) + objects := buildObjects(namedTypes, schema, prog, imports) + b := &Build{ PackageName: filepath.Base(destDir), - Objects: buildObjects(namedTypes, schema, prog, imports), + Objects: objects, + Models: findMissingObjects(objects, schema), Interfaces: buildInterfaces(namedTypes, schema), Inputs: buildInputs(namedTypes, schema, prog, imports), Imports: imports, @@ -77,7 +82,12 @@ func Bind(schema *schema.Schema, userTypes map[string]string, destDir string) (* } func loadProgram(imports Imports) (*loader.Program, error) { - var conf loader.Config + conf := loader.Config{ + AllowErrors: true, + TypeChecker: types.Config{ + Error: func(e error) {}, + }, + } for _, imp := range imports { if imp.Package != "" { conf.Import(imp.Package) diff --git a/codegen/object.go b/codegen/object.go index 518ff7a5334..ae42688a567 100644 --- a/codegen/object.go +++ b/codegen/object.go @@ -25,6 +25,8 @@ type Field struct { 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 + GoFKName string // The name of the FK used when generating models + GoFKType string // The type of the FK used when generating models Args []FieldArgument // A list of arguments to be passed to this field 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 @@ -175,3 +177,13 @@ func lcFirst(s string) string { r[0] = unicode.ToLower(r[0]) return string(r) } + +func ucFirst(s string) string { + if s == "" { + return "" + } + + r := []rune(s) + r[0] = unicode.ToUpper(r[0]) + return string(r) +} diff --git a/codegen/object_build.go b/codegen/object_build.go index 7ea1e3d0fd1..367ffbc7503 100644 --- a/codegen/object_build.go +++ b/codegen/object_build.go @@ -48,6 +48,45 @@ func buildObjects(types NamedTypes, s *schema.Schema, prog *loader.Program, impo return objects } +func findMissingObjects(objects Objects, s *schema.Schema) Objects { + var missingObjects Objects + + for _, object := range objects { + if !object.Generated || object.Root { + continue + } + object.GoType = ucFirst(object.GQLType) + + for i := range object.Fields { + field := &object.Fields[i] + + if field.IsScalar { + field.GoVarName = ucFirst(field.GQLName) + if field.GoVarName == "Id" { + field.GoVarName = "ID" + } + } else { + field.GoFKName = ucFirst(field.GQLName) + "ID" + field.GoFKType = "int" + + for _, f := range objects.ByName(field.Type.GQLType).Fields { + if strings.EqualFold(f.GQLName, "id") { + field.GoFKType = f.GoType + } + } + } + } + + missingObjects = append(missingObjects, object) + } + + sort.Slice(missingObjects, func(i, j int) bool { + return strings.Compare(missingObjects[i].GQLType, missingObjects[j].GQLType) == -1 + }) + + return missingObjects +} + func buildObject(types NamedTypes, typ *schema.Object) *Object { obj := &Object{NamedType: types[typ.TypeName()]} diff --git a/codegen/type.go b/codegen/type.go index 19fa5e870cc..dc032e9e077 100644 --- a/codegen/type.go +++ b/codegen/type.go @@ -12,6 +12,7 @@ type NamedType struct { IsInterface bool GQLType string // Name of the graphql type Marshaler *Ref // If this type has an external marshaler this will be set + Generated bool // will it be autogenerated? } type Ref struct { diff --git a/codegen/type_build.go b/codegen/type_build.go index 93e817cec28..86be1265a57 100644 --- a/codegen/type_build.go +++ b/codegen/type_build.go @@ -17,14 +17,15 @@ func buildNamedTypes(s *schema.Schema, userTypes map[string]string) NamedTypes { t := namedTypeFromSchema(schemaType) userType := userTypes[t.GQLType] - if userType == "" { - if t.IsScalar { - userType = "github.com/vektah/gqlgen/graphql.String" - } else { - userType = "interface{}" - } + if userType == "" && t.IsScalar { + userType = "github.com/vektah/gqlgen/graphql.String" + } + + if userType != "" { + t.Package, t.GoType = pkgAndType(userType) + } else { + t.Generated = true } - t.Package, t.GoType = pkgAndType(userType) types[t.GQLType] = t } diff --git a/codegen/util.go b/codegen/util.go index baed7a5e0e5..fd2d50d10df 100644 --- a/codegen/util.go +++ b/codegen/util.go @@ -121,7 +121,7 @@ func bindObject(t types.Type, object *Object, imports Imports) { if structField := findField(underlying, field.GQLName); structField != nil { field.Type.Modifiers = modifiersFromGoType(structField.Type()) - field.GoVarName = "it." + structField.Name() + field.GoVarName = structField.Name() switch field.Type.FullSignature() { case structField.Type().String(): @@ -136,7 +136,9 @@ func bindObject(t types.Type, object *Object, imports Imports) { } default: - fmt.Fprintf(os.Stderr, "type mismatch on %s.%s, expected %s got %s\n", object.GQLType, field.GQLName, field.Type.FullSignature(), structField.Type()) + if !field.Generated { + fmt.Fprintf(os.Stderr, "type mismatch on %s.%s, expected %s got %s\n", object.GQLType, field.GQLName, field.Type.FullSignature(), structField.Type()) + } } continue } diff --git a/example/chat/generated.go b/example/chat/generated.go index f89b74d95df..e155d3c887a 100644 --- a/example/chat/generated.go +++ b/example/chat/generated.go @@ -15,6 +15,10 @@ import ( schema "github.com/vektah/gqlgen/neelance/schema" ) +func MakeExecutableSchema(resolvers Resolvers) graphql.ExecutableSchema { + return &executableSchema{resolvers} +} + type Resolvers interface { Mutation_post(ctx context.Context, text string, username string, roomName string) (Message, error) Query_room(ctx context.Context, name string) (*Chatroom, error) @@ -22,8 +26,11 @@ type Resolvers interface { Subscription_messageAdded(ctx context.Context, roomName string) (<-chan Message, error) } -func MakeExecutableSchema(resolvers Resolvers) graphql.ExecutableSchema { - return &executableSchema{resolvers} +type Message struct { + ID string + Text string + CreatedBy string + CreatedAt time.Time } type executableSchema struct { @@ -37,7 +44,7 @@ func (e *executableSchema) Schema() *schema.Schema { func (e *executableSchema) Query(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation) *graphql.Response { ec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx} - data := ec._query(op.Selections, nil) + data := ec._query(op.Selections) ec.wg.Wait() return &graphql.Response{ @@ -49,7 +56,7 @@ func (e *executableSchema) Query(ctx context.Context, doc *query.Document, varia func (e *executableSchema) Mutation(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation) *graphql.Response { ec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx} - data := ec._mutation(op.Selections, nil) + data := ec._mutation(op.Selections) ec.wg.Wait() return &graphql.Response{ @@ -63,7 +70,7 @@ func (e *executableSchema) Subscription(ctx context.Context, doc *query.Document ec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx} - eventData := ec._subscription(op.Selections, nil) + eventData := ec._subscription(op.Selections) if ec.Errors != nil { events <- &graphql.Response{ Data: graphql.Null, @@ -107,14 +114,6 @@ func (ec *executionContext) _chatroom(sel []query.Selection, it *Chatroom) graph switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("Chatroom") - case "id": - badArgs := false - if badArgs { - continue - } - res := it.ID() - - out.Values[i] = graphql.MarshalID(res) case "name": badArgs := false if badArgs { @@ -201,7 +200,7 @@ func (ec *executionContext) _message(sel []query.Selection, it *Message) graphql var mutationImplementors = []string{"Mutation"} // nolint: gocyclo, errcheck, gas, goconst -func (ec *executionContext) _mutation(sel []query.Selection, it *interface{}) graphql.Marshaler { +func (ec *executionContext) _mutation(sel []query.Selection) graphql.Marshaler { fields := graphql.CollectFields(ec.doc, sel, mutationImplementors, ec.variables) out := graphql.NewOrderedMap(len(fields)) for i, field := range fields { @@ -258,7 +257,7 @@ func (ec *executionContext) _mutation(sel []query.Selection, it *interface{}) gr var queryImplementors = []string{"Query"} // nolint: gocyclo, errcheck, gas, goconst -func (ec *executionContext) _query(sel []query.Selection, it *interface{}) graphql.Marshaler { +func (ec *executionContext) _query(sel []query.Selection) graphql.Marshaler { fields := graphql.CollectFields(ec.doc, sel, queryImplementors, ec.variables) out := graphql.NewOrderedMap(len(fields)) for i, field := range fields { @@ -339,7 +338,7 @@ func (ec *executionContext) _query(sel []query.Selection, it *interface{}) graph var subscriptionImplementors = []string{"Subscription"} // nolint: gocyclo, errcheck, gas, goconst -func (ec *executionContext) _subscription(sel []query.Selection, it *interface{}) <-chan graphql.Marshaler { +func (ec *executionContext) _subscription(sel []query.Selection) <-chan graphql.Marshaler { fields := graphql.CollectFields(ec.doc, sel, subscriptionImplementors, ec.variables) if len(fields) != 1 { @@ -948,7 +947,7 @@ func (ec *executionContext) ___Type(sel []query.Selection, it *introspection.Typ return out } -var parsedSchema = schema.MustParse("type Chatroom {\n id: ID!\n name: String!\n messages: [Message!]!\n}\n\ntype Message {\n id: ID!\n text: String!\n createdBy: String!\n createdAt: Time!\n}\n\ntype Query {\n room(name:String!): Chatroom\n}\n\ntype Mutation {\n post(text: String!, username: String!, roomName: String!): Message!\n}\n\ntype Subscription {\n messageAdded(roomName: String!): Message!\n}\n\nschema {\n query: Query\n mutation: Mutation\n subscription: Subscription\n}\n\nscalar Time\n") +var parsedSchema = schema.MustParse("type Chatroom {\n name: String!\n messages: [Message!]!\n}\n\ntype Message {\n id: ID!\n text: String!\n createdBy: String!\n createdAt: Time!\n}\n\ntype Query {\n room(name:String!): Chatroom\n}\n\ntype Mutation {\n post(text: String!, username: String!, roomName: String!): Message!\n}\n\ntype Subscription {\n messageAdded(roomName: String!): Message!\n}\n\nschema {\n query: Query\n mutation: Mutation\n subscription: Subscription\n}\n\nscalar Time\n") func (ec *executionContext) introspectSchema() *introspection.Schema { return introspection.WrapSchema(parsedSchema) diff --git a/example/chat/models.go b/example/chat/models.go deleted file mode 100644 index 50a9cc092f1..00000000000 --- a/example/chat/models.go +++ /dev/null @@ -1,20 +0,0 @@ -package chat - -import ( - "time" -) - -type Chatroom struct { - Name string - Messages []Message - Observers map[string]chan Message -} - -func (c *Chatroom) ID() string { return "C" + c.Name } - -type Message struct { - ID string - Text string - CreatedBy string - CreatedAt time.Time -} diff --git a/example/chat/resolvers.go b/example/chat/resolvers.go index 9b3dfd4463a..979835cc25d 100644 --- a/example/chat/resolvers.go +++ b/example/chat/resolvers.go @@ -1,4 +1,4 @@ -//go:generate gorunpkg github.com/vektah/gqlgen -out generated.go +//go:generate gorunpkg github.com/vektah/gqlgen -typemap types.json -out generated.go package chat @@ -20,6 +20,12 @@ func New() *resolvers { } } +type Chatroom struct { + Name string + Messages []Message + Observers map[string]chan Message +} + func (r *resolvers) Mutation_post(ctx context.Context, text string, userName string, roomName string) (Message, error) { r.mu.Lock() room := r.Rooms[roomName] diff --git a/example/chat/schema.graphql b/example/chat/schema.graphql index 3507aa503af..9cb79ed9124 100644 --- a/example/chat/schema.graphql +++ b/example/chat/schema.graphql @@ -1,5 +1,4 @@ type Chatroom { - id: ID! name: String! messages: [Message!]! } diff --git a/example/chat/types.json b/example/chat/types.json index c523c6ba5c6..7d13de6dfea 100644 --- a/example/chat/types.json +++ b/example/chat/types.json @@ -1,5 +1,3 @@ { - "Chatroom": "github.com/vektah/gqlgen/example/chat.Chatroom", - "User": "github.com/vektah/gqlgen/example/chat.User", - "Message": "github.com/vektah/gqlgen/example/chat.Message" + "Chatroom": "github.com/vektah/gqlgen/example/chat.Chatroom" } diff --git a/example/dataloader/generated.go b/example/dataloader/generated.go index 5a05efbfe93..0297f3bc877 100644 --- a/example/dataloader/generated.go +++ b/example/dataloader/generated.go @@ -6,6 +6,7 @@ import ( context "context" strconv "strconv" sync "sync" + time "time" graphql "github.com/vektah/gqlgen/graphql" errors "github.com/vektah/gqlgen/neelance/errors" @@ -14,6 +15,10 @@ import ( schema "github.com/vektah/gqlgen/neelance/schema" ) +func MakeExecutableSchema(resolvers Resolvers) graphql.ExecutableSchema { + return &executableSchema{resolvers} +} + type Resolvers interface { Customer_address(ctx context.Context, it *Customer) (*Address, error) Customer_orders(ctx context.Context, it *Customer) ([]Order, error) @@ -22,8 +27,28 @@ type Resolvers interface { Query_customers(ctx context.Context) ([]Customer, error) } -func MakeExecutableSchema(resolvers Resolvers) graphql.ExecutableSchema { - return &executableSchema{resolvers} +type Address struct { + ID int + Street string + Country string +} + +type Customer struct { + ID int + Name string + AddressID int + OrdersID int +} + +type Item struct { + Name string +} + +type Order struct { + ID int + Date time.Time + Amount float64 + ItemsID int } type executableSchema struct { @@ -37,7 +62,7 @@ func (e *executableSchema) Schema() *schema.Schema { func (e *executableSchema) Query(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation) *graphql.Response { ec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx} - data := ec._query(op.Selections, nil) + data := ec._query(op.Selections) ec.wg.Wait() return &graphql.Response{ @@ -288,7 +313,7 @@ func (ec *executionContext) _order(sel []query.Selection, it *Order) graphql.Mar var queryImplementors = []string{"Query"} // nolint: gocyclo, errcheck, gas, goconst -func (ec *executionContext) _query(sel []query.Selection, it *interface{}) graphql.Marshaler { +func (ec *executionContext) _query(sel []query.Selection) graphql.Marshaler { fields := graphql.CollectFields(ec.doc, sel, queryImplementors, ec.variables) out := graphql.NewOrderedMap(len(fields)) for i, field := range fields { @@ -923,7 +948,7 @@ func (ec *executionContext) ___Type(sel []query.Selection, it *introspection.Typ return out } -var parsedSchema = schema.MustParse("schema {\n query: Query\n}\n\ntype Query {\n customers: [Customer!]\n}\n\ntype Customer {\n id: Int!\n name: String!\n address: Address\n orders: [Order!]\n}\n\ntype Address {\n id: Int!\n street: String\n country: String\n}\n\ntype Order {\n id: Int!\n date: Time\n amount: Float!\n items: [Item!]\n}\n\ntype Item {\n name: String\n}\nscalar Time\n") +var parsedSchema = schema.MustParse("schema {\n query: Query\n}\n\ntype Query {\n customers: [Customer!]\n}\n\ntype Customer {\n id: Int!\n name: String!\n address: Address\n orders: [Order!]\n}\n\ntype Address {\n id: Int!\n street: String!\n country: String!\n}\n\ntype Order {\n id: Int!\n date: Time!\n amount: Float!\n items: [Item!]\n}\n\ntype Item {\n name: String!\n}\nscalar Time\n") func (ec *executionContext) introspectSchema() *introspection.Schema { return introspection.WrapSchema(parsedSchema) diff --git a/example/dataloader/models.go b/example/dataloader/models.go deleted file mode 100644 index 8671cfb8d5b..00000000000 --- a/example/dataloader/models.go +++ /dev/null @@ -1,25 +0,0 @@ -package dataloader - -import "time" - -type Customer struct { - ID int - Name string - addressID int -} - -type Address struct { - ID int - Street string - Country string -} - -type Order struct { - ID int - Date time.Time - Amount float64 -} - -type Item struct { - Name string -} diff --git a/example/dataloader/resolvers.go b/example/dataloader/resolvers.go index 37c5943a8ab..09b6430a853 100644 --- a/example/dataloader/resolvers.go +++ b/example/dataloader/resolvers.go @@ -11,7 +11,7 @@ import ( type Resolver struct{} func (r *Resolver) Customer_address(ctx context.Context, it *Customer) (*Address, error) { - return ctxLoaders(ctx).addressByID.Load(it.addressID) + return ctxLoaders(ctx).addressByID.Load(it.AddressID) } func (r *Resolver) Customer_orders(ctx context.Context, it *Customer) ([]Order, error) { @@ -28,8 +28,8 @@ func (r *Resolver) Query_customers(ctx context.Context) ([]Customer, error) { time.Sleep(5 * time.Millisecond) return []Customer{ - {ID: 1, Name: "Bob", addressID: 1}, - {ID: 2, Name: "Alice", addressID: 3}, - {ID: 3, Name: "Eve", addressID: 4}, + {ID: 1, Name: "Bob", AddressID: 1}, + {ID: 2, Name: "Alice", AddressID: 3}, + {ID: 3, Name: "Eve", AddressID: 4}, }, nil } diff --git a/example/dataloader/schema.graphql b/example/dataloader/schema.graphql index 9bbf970d0ff..953b6d4d12a 100644 --- a/example/dataloader/schema.graphql +++ b/example/dataloader/schema.graphql @@ -15,18 +15,18 @@ type Customer { type Address { id: Int! - street: String - country: String + street: String! + country: String! } type Order { id: Int! - date: Time + date: Time! amount: Float! items: [Item!] } type Item { - name: String + name: String! } scalar Time diff --git a/example/dataloader/types.json b/example/dataloader/types.json deleted file mode 100644 index a0fb5a15193..00000000000 --- a/example/dataloader/types.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "Customer": "github.com/vektah/gqlgen/example/dataloader.Customer", - "Address": "github.com/vektah/gqlgen/example/dataloader.Address", - "Item": "github.com/vektah/gqlgen/example/dataloader.Item", - "Order": "github.com/vektah/gqlgen/example/dataloader.Order" -} diff --git a/example/scalars/generated.go b/example/scalars/generated.go index 5a810eeb753..946a9c9df41 100644 --- a/example/scalars/generated.go +++ b/example/scalars/generated.go @@ -14,15 +14,15 @@ import ( schema "github.com/vektah/gqlgen/neelance/schema" ) +func MakeExecutableSchema(resolvers Resolvers) graphql.ExecutableSchema { + return &executableSchema{resolvers} +} + type Resolvers interface { Query_user(ctx context.Context, id string) (*User, error) Query_search(ctx context.Context, input SearchArgs) ([]User, error) } -func MakeExecutableSchema(resolvers Resolvers) graphql.ExecutableSchema { - return &executableSchema{resolvers} -} - type executableSchema struct { resolvers Resolvers } @@ -34,7 +34,7 @@ func (e *executableSchema) Schema() *schema.Schema { func (e *executableSchema) Query(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation) *graphql.Response { ec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx} - data := ec._query(op.Selections, nil) + data := ec._query(op.Selections) ec.wg.Wait() return &graphql.Response{ @@ -65,7 +65,7 @@ type executionContext struct { var queryImplementors = []string{"Query"} // nolint: gocyclo, errcheck, gas, goconst -func (ec *executionContext) _query(sel []query.Selection, it *interface{}) graphql.Marshaler { +func (ec *executionContext) _query(sel []query.Selection) graphql.Marshaler { fields := graphql.CollectFields(ec.doc, sel, queryImplementors, ec.variables) out := graphql.NewOrderedMap(len(fields)) for i, field := range fields { diff --git a/example/scalars/resolvers.go b/example/scalars/resolvers.go index 8ccc12a3857..21a72555f63 100644 --- a/example/scalars/resolvers.go +++ b/example/scalars/resolvers.go @@ -1,4 +1,4 @@ -//go:generate gorunpkg github.com/vektah/gqlgen -out generated.go +//go:generate gorunpkg github.com/vektah/gqlgen -typemap types.json -out generated.go package scalars diff --git a/example/starwars/generated.go b/example/starwars/generated.go index 095a2efb051..d9f81d09357 100644 --- a/example/starwars/generated.go +++ b/example/starwars/generated.go @@ -16,6 +16,10 @@ import ( schema "github.com/vektah/gqlgen/neelance/schema" ) +func MakeExecutableSchema(resolvers Resolvers) graphql.ExecutableSchema { + return &executableSchema{resolvers} +} + type Resolvers interface { Droid_friends(ctx context.Context, it *Droid) ([]Character, error) Droid_friendsConnection(ctx context.Context, it *Droid, first *int, after *string) (FriendsConnection, error) @@ -38,10 +42,6 @@ type Resolvers interface { Query_starship(ctx context.Context, id string) (*Starship, error) } -func MakeExecutableSchema(resolvers Resolvers) graphql.ExecutableSchema { - return &executableSchema{resolvers} -} - type executableSchema struct { resolvers Resolvers } @@ -53,7 +53,7 @@ func (e *executableSchema) Schema() *schema.Schema { func (e *executableSchema) Query(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation) *graphql.Response { ec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx} - data := ec._query(op.Selections, nil) + data := ec._query(op.Selections) ec.wg.Wait() return &graphql.Response{ @@ -65,7 +65,7 @@ func (e *executableSchema) Query(ctx context.Context, doc *query.Document, varia func (e *executableSchema) Mutation(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation) *graphql.Response { ec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx} - data := ec._mutation(op.Selections, nil) + data := ec._mutation(op.Selections) ec.wg.Wait() return &graphql.Response{ @@ -474,7 +474,7 @@ func (ec *executionContext) _human(sel []query.Selection, it *Human) graphql.Mar var mutationImplementors = []string{"Mutation"} // nolint: gocyclo, errcheck, gas, goconst -func (ec *executionContext) _mutation(sel []query.Selection, it *interface{}) graphql.Marshaler { +func (ec *executionContext) _mutation(sel []query.Selection) graphql.Marshaler { fields := graphql.CollectFields(ec.doc, sel, mutationImplementors, ec.variables) out := graphql.NewOrderedMap(len(fields)) for i, field := range fields { @@ -572,7 +572,7 @@ func (ec *executionContext) _pageInfo(sel []query.Selection, it *PageInfo) graph var queryImplementors = []string{"Query"} // nolint: gocyclo, errcheck, gas, goconst -func (ec *executionContext) _query(sel []query.Selection, it *interface{}) graphql.Marshaler { +func (ec *executionContext) _query(sel []query.Selection) graphql.Marshaler { fields := graphql.CollectFields(ec.doc, sel, queryImplementors, ec.variables) out := graphql.NewOrderedMap(len(fields)) for i, field := range fields { diff --git a/example/starwars/resolvers.go b/example/starwars/resolvers.go index c8e808ef696..04c97068e6e 100644 --- a/example/starwars/resolvers.go +++ b/example/starwars/resolvers.go @@ -1,4 +1,4 @@ -//go:generate gorunpkg github.com/vektah/gqlgen -out generated.go +//go:generate gorunpkg github.com/vektah/gqlgen -typemap types.json -out generated.go package starwars diff --git a/example/todo/generated.go b/example/todo/generated.go index 281dc501d78..22d02bf8d68 100644 --- a/example/todo/generated.go +++ b/example/todo/generated.go @@ -14,6 +14,10 @@ import ( schema "github.com/vektah/gqlgen/neelance/schema" ) +func MakeExecutableSchema(resolvers Resolvers) graphql.ExecutableSchema { + return &executableSchema{resolvers} +} + type Resolvers interface { MyMutation_createTodo(ctx context.Context, text string) (Todo, error) MyMutation_updateTodo(ctx context.Context, id int, changes map[string]interface{}) (*Todo, error) @@ -22,8 +26,10 @@ type Resolvers interface { MyQuery_todos(ctx context.Context) ([]Todo, error) } -func MakeExecutableSchema(resolvers Resolvers) graphql.ExecutableSchema { - return &executableSchema{resolvers} +type Todo struct { + ID int + Text string + Done bool } type executableSchema struct { @@ -37,7 +43,7 @@ func (e *executableSchema) Schema() *schema.Schema { func (e *executableSchema) Query(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation) *graphql.Response { ec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx} - data := ec._myQuery(op.Selections, nil) + data := ec._myQuery(op.Selections) ec.wg.Wait() return &graphql.Response{ @@ -49,7 +55,7 @@ func (e *executableSchema) Query(ctx context.Context, doc *query.Document, varia func (e *executableSchema) Mutation(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation) *graphql.Response { ec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx} - data := ec._myMutation(op.Selections, nil) + data := ec._myMutation(op.Selections) ec.wg.Wait() return &graphql.Response{ @@ -76,7 +82,7 @@ type executionContext struct { var myMutationImplementors = []string{"MyMutation"} // nolint: gocyclo, errcheck, gas, goconst -func (ec *executionContext) _myMutation(sel []query.Selection, it *interface{}) graphql.Marshaler { +func (ec *executionContext) _myMutation(sel []query.Selection) graphql.Marshaler { fields := graphql.CollectFields(ec.doc, sel, myMutationImplementors, ec.variables) out := graphql.NewOrderedMap(len(fields)) for i, field := range fields { @@ -145,7 +151,7 @@ func (ec *executionContext) _myMutation(sel []query.Selection, it *interface{}) var myQueryImplementors = []string{"MyQuery"} // nolint: gocyclo, errcheck, gas, goconst -func (ec *executionContext) _myQuery(sel []query.Selection, it *interface{}) graphql.Marshaler { +func (ec *executionContext) _myQuery(sel []query.Selection) graphql.Marshaler { fields := graphql.CollectFields(ec.doc, sel, myQueryImplementors, ec.variables) out := graphql.NewOrderedMap(len(fields)) for i, field := range fields { @@ -283,9 +289,9 @@ func (ec *executionContext) _todo(sel []query.Selection, it *Todo) graphql.Marsh if badArgs { continue } - res := it.ID() + res := it.ID - out.Values[i] = graphql.MarshalID(res) + out.Values[i] = graphql.MarshalInt(res) case "text": badArgs := false if badArgs { @@ -873,7 +879,7 @@ func (ec *executionContext) ___Type(sel []query.Selection, it *introspection.Typ return out } -var parsedSchema = schema.MustParse("schema {\n\tquery: MyQuery\n\tmutation: MyMutation\n}\n\ntype MyQuery {\n\ttodo(id: Int!): Todo\n\tlastTodo: Todo\n\ttodos: [Todo!]!\n}\n\ntype MyMutation {\n\tcreateTodo(text: String!): Todo!\n\tupdateTodo(id: Int!, changes: TodoInput!): Todo\n}\n\ntype Todo {\n\tid: ID!\n\ttext: String!\n\tdone: Boolean!\n}\n\ninput TodoInput {\n\ttext: String\n\tdone: Boolean\n}\n") +var parsedSchema = schema.MustParse("schema {\n\tquery: MyQuery\n\tmutation: MyMutation\n}\n\ntype MyQuery {\n\ttodo(id: Int!): Todo\n\tlastTodo: Todo\n\ttodos: [Todo!]!\n}\n\ntype MyMutation {\n\tcreateTodo(text: String!): Todo!\n\tupdateTodo(id: Int!, changes: TodoInput!): Todo\n}\n\ntype Todo {\n\tid: Int!\n\ttext: String!\n\tdone: Boolean!\n}\n\ninput TodoInput {\n\ttext: String\n\tdone: Boolean\n}\n") func (ec *executionContext) introspectSchema() *introspection.Schema { return introspection.WrapSchema(parsedSchema) diff --git a/example/todo/schema.graphql b/example/todo/schema.graphql index 477809565d0..8f71e76e9d3 100644 --- a/example/todo/schema.graphql +++ b/example/todo/schema.graphql @@ -15,7 +15,7 @@ type MyMutation { } type Todo { - id: ID! + id: Int! text: String! done: Boolean! } diff --git a/example/todo/todo.go b/example/todo/todo.go index b8866d90896..bb343f6edea 100644 --- a/example/todo/todo.go +++ b/example/todo/todo.go @@ -1,4 +1,4 @@ -//go:generate gorunpkg github.com/vektah/gqlgen -out generated.go +//go:generate gorunpkg github.com/vektah/gqlgen -typemap types.json -out generated.go package todo @@ -7,21 +7,9 @@ import ( "errors" "time" - "strconv" - "github.com/mitchellh/mapstructure" ) -type Todo struct { - id int - Text string - Done bool -} - -func (t *Todo) ID() string { - return "t" + strconv.Itoa(t.id) -} - type todoResolver struct { todos []Todo lastID int @@ -30,9 +18,9 @@ type todoResolver struct { func New() *todoResolver { return &todoResolver{ todos: []Todo{ - {id: 1, Text: "A todo not to forget", Done: false}, - {id: 2, Text: "This is the most important", Done: false}, - {id: 3, Text: "Please do this or else", Done: false}, + {ID: 1, Text: "A todo not to forget", Done: false}, + {ID: 2, Text: "This is the most important", Done: false}, + {ID: 3, Text: "Please do this or else", Done: false}, }, lastID: 3, } @@ -41,7 +29,7 @@ func New() *todoResolver { func (r *todoResolver) MyQuery_todo(ctx context.Context, id int) (*Todo, error) { time.Sleep(220 * time.Millisecond) for _, todo := range r.todos { - if todo.id == id { + if todo.ID == id { return &todo, nil } } @@ -63,7 +51,7 @@ func (r *todoResolver) MyMutation_createTodo(ctx context.Context, text string) ( newID := r.id() newTodo := Todo{ - id: newID, + ID: newID, Text: text, Done: false, } @@ -79,7 +67,7 @@ func (r *todoResolver) MyMutation_updateTodo(ctx context.Context, id int, change var affectedTodo *Todo for i := 0; i < len(r.todos); i++ { - if r.todos[i].id == id { + if r.todos[i].ID == id { affectedTodo = &r.todos[i] break } diff --git a/example/todo/todo_test.go b/example/todo/todo_test.go index 10827ac4cb9..897e3d3e49c 100644 --- a/example/todo/todo_test.go +++ b/example/todo/todo_test.go @@ -17,11 +17,11 @@ func TestTodo(t *testing.T) { t.Run("create a new todo", func(t *testing.T) { var resp struct { - CreateTodo struct{ ID string } + CreateTodo struct{ ID int } } c.MustPost(`mutation { createTodo(text:"Fery important") { id } }`, &resp) - require.Equal(t, "t4", resp.CreateTodo.ID) + require.Equal(t, 4, resp.CreateTodo.ID) }) t.Run("update the todo text", func(t *testing.T) { @@ -56,12 +56,12 @@ func TestTodo(t *testing.T) { t.Run("select with alias", func(t *testing.T) { var resp struct { A struct{ Text string } - B struct{ ID string } + B struct{ ID int } } c.MustPost(`{ a: todo(id:1) { text } b: todo(id:2) { id } }`, &resp) require.Equal(t, "A todo not to forget", resp.A.Text) - require.Equal(t, "t2", resp.B.ID) + require.Equal(t, 2, resp.B.ID) }) t.Run("find a missing todo", func(t *testing.T) { @@ -77,17 +77,17 @@ func TestTodo(t *testing.T) { t.Run("select all", func(t *testing.T) { var resp struct { Todo struct { - ID string + ID int Text string Done bool } LastTodo struct { - ID string + ID int Text string Done bool } Todos []struct { - ID string + ID int Text string Done bool } @@ -98,11 +98,11 @@ func TestTodo(t *testing.T) { todos { id text done } }`, &resp) - require.Equal(t, "t1", resp.Todo.ID) - require.Equal(t, "t4", resp.LastTodo.ID) + require.Equal(t, 1, resp.Todo.ID) + require.Equal(t, 4, resp.LastTodo.ID) require.Len(t, resp.Todos, 4) require.Equal(t, "Very important", resp.LastTodo.Text) - require.Equal(t, "t4", resp.LastTodo.ID) + require.Equal(t, 4, resp.LastTodo.ID) }) t.Run("introspection", func(t *testing.T) { diff --git a/example/todo/types.json b/example/todo/types.json index 161ec9074e6..d9e2d575e39 100644 --- a/example/todo/types.json +++ b/example/todo/types.json @@ -1,4 +1,3 @@ { - "Todo": "github.com/vektah/gqlgen/example/todo.Todo", "TodoInput": "map[string]interface{}" } diff --git a/main.go b/main.go index 5fa8b5911be..048bc4aad1f 100644 --- a/main.go +++ b/main.go @@ -8,8 +8,6 @@ import ( "os" "path/filepath" - "syscall" - "github.com/vektah/gqlgen/codegen" "github.com/vektah/gqlgen/neelance/schema" "golang.org/x/tools/imports" @@ -17,7 +15,7 @@ import ( var output = flag.String("out", "-", "the file to write to, - for stdout") var schemaFilename = flag.String("schema", "schema.graphql", "the graphql schema to generate types from") -var typemap = flag.String("typemap", "types.json", "a json map going from graphql to golang types") +var typemap = flag.String("typemap", "", "a json map going from graphql to golang types") var packageName = flag.String("package", "", "the package name") var help = flag.Bool("h", false, "this usage text") @@ -46,10 +44,6 @@ func main() { os.Exit(1) } - if *output != "-" { - _ = syscall.Unlink(*output) - } - build, err := codegen.Bind(schema, loadTypeMap(), dirName()) if err != nil { fmt.Fprintln(os.Stderr, "failed to generate code: "+err.Error()) @@ -120,14 +114,16 @@ func loadTypeMap() map[string]string { "ID": "github.com/vektah/gqlgen/graphql.ID", "Time": "github.com/vektah/gqlgen/graphql.Time", } - b, err := ioutil.ReadFile(*typemap) - if err != nil { - fmt.Fprintln(os.Stderr, "unable to open typemap: "+err.Error()+" creating it.") - return goTypes - } - if err = json.Unmarshal(b, &goTypes); err != nil { - fmt.Fprintln(os.Stderr, "unable to parse typemap: "+err.Error()) - os.Exit(1) + if *typemap != "" { + b, err := ioutil.ReadFile(*typemap) + if err != nil { + fmt.Fprintln(os.Stderr, "unable to open typemap: "+err.Error()) + return goTypes + } + if err = json.Unmarshal(b, &goTypes); err != nil { + fmt.Fprintln(os.Stderr, "unable to parse typemap: "+err.Error()) + os.Exit(1) + } } return goTypes diff --git a/templates/file.go b/templates/file.go index a4bd2b4767a..29247ed13c3 100644 --- a/templates/file.go +++ b/templates/file.go @@ -12,6 +12,10 @@ import ( {{ end }} ) +func MakeExecutableSchema(resolvers Resolvers) graphql.ExecutableSchema { + return &executableSchema{resolvers} +} + type Resolvers interface { {{- range $object := .Objects -}} {{ range $field := $object.Fields -}} @@ -20,9 +24,9 @@ type Resolvers interface { {{- end }} } -func MakeExecutableSchema(resolvers Resolvers) graphql.ExecutableSchema { - return &executableSchema{resolvers} -} +{{- range $model := .Models }} + {{ template "model" $model }} +{{- end}} type executableSchema struct { resolvers Resolvers @@ -36,7 +40,7 @@ func (e *executableSchema) Query(ctx context.Context, doc *query.Document, varia {{- if .QueryRoot }} ec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx} - data := ec._{{.QueryRoot.GQLType|lcFirst}}(op.Selections, nil) + data := ec._{{.QueryRoot.GQLType|lcFirst}}(op.Selections) ec.wg.Wait() return &graphql.Response{ @@ -52,7 +56,7 @@ func (e *executableSchema) Mutation(ctx context.Context, doc *query.Document, va {{- if .MutationRoot }} ec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx} - data := ec._{{.MutationRoot.GQLType|lcFirst}}(op.Selections, nil) + data := ec._{{.MutationRoot.GQLType|lcFirst}}(op.Selections) ec.wg.Wait() return &graphql.Response{ @@ -70,7 +74,7 @@ func (e *executableSchema) Subscription(ctx context.Context, doc *query.Document ec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx} - eventData := ec._{{.SubscriptionRoot.GQLType|lcFirst}}(op.Selections, nil) + eventData := ec._{{.SubscriptionRoot.GQLType|lcFirst}}(op.Selections) if ec.Errors != nil { events<-&graphql.Response{ Data: graphql.Null, diff --git a/templates/input.go b/templates/input.go index 6f6f81361d5..6e86f4e404c 100644 --- a/templates/input.go +++ b/templates/input.go @@ -14,7 +14,7 @@ const inputTpl = ` if err != nil { return it, err } - {{$field.GoVarName}} = {{if $field.Type.IsPtr}}&{{end}}val + it.{{$field.GoVarName}} = {{if $field.Type.IsPtr}}&{{end}}val {{- end }} } } diff --git a/templates/model.go b/templates/model.go new file mode 100644 index 00000000000..ea1e4d90412 --- /dev/null +++ b/templates/model.go @@ -0,0 +1,15 @@ +package templates + +const modelTpl = ` +{{- define "model" }} + type {{.GoType}} struct { + {{- range $field := .Fields }} + {{- if $field.GoVarName }} + {{ $field.GoVarName }} {{$field.Signature}} + {{- else }} + {{ $field.GoFKName }} {{$field.GoFKType}} + {{- end }} + {{- end }} + } +{{- end }} +` diff --git a/templates/object.go b/templates/object.go index 2b3cf34c749..a71f1fe364a 100644 --- a/templates/object.go +++ b/templates/object.go @@ -8,7 +8,7 @@ var {{ $object.GQLType|lcFirst}}Implementors = {{$object.Implementors}} // nolint: gocyclo, errcheck, gas, goconst {{- if .Stream }} -func (ec *executionContext) _{{$object.GQLType|lcFirst}}(sel []query.Selection, it *{{$object.FullName}}) <-chan graphql.Marshaler { +func (ec *executionContext) _{{$object.GQLType|lcFirst}}(sel []query.Selection{{if not $object.Root}}, it *{{$object.FullName}}{{end}}) <-chan graphql.Marshaler { fields := graphql.CollectFields(ec.doc, sel, {{$object.GQLType|lcFirst}}Implementors, ec.variables) if len(fields) != 1 { @@ -28,7 +28,7 @@ func (ec *executionContext) _{{$object.GQLType|lcFirst}}(sel []query.Selection, } {{- if $field.GoVarName }} - results := {{$field.GoVarName}} + results := it.{{$field.GoVarName}} {{- else if $field.GoMethodName }} {{- if $field.NoErr }} results := {{$field.GoMethodName}}({{ $field.CallArgs }}) @@ -65,7 +65,7 @@ func (ec *executionContext) _{{$object.GQLType|lcFirst}}(sel []query.Selection, return channel } {{- else }} -func (ec *executionContext) _{{$object.GQLType|lcFirst}}(sel []query.Selection, it *{{$object.FullName}}) graphql.Marshaler { +func (ec *executionContext) _{{$object.GQLType|lcFirst}}(sel []query.Selection{{if not $object.Root}}, it *{{$object.FullName}} {{end}}) graphql.Marshaler { fields := graphql.CollectFields(ec.doc, sel, {{$object.GQLType|lcFirst}}Implementors, ec.variables) out := graphql.NewOrderedMap(len(fields)) for i, field := range fields { @@ -90,7 +90,7 @@ func (ec *executionContext) _{{$object.GQLType|lcFirst}}(sel []query.Selection, {{- end }} {{- if $field.GoVarName }} - res := {{$field.GoVarName}} + res := it.{{$field.GoVarName}} {{- else if $field.GoMethodName }} {{- if $field.NoErr }} res := {{$field.GoMethodName}}({{ $field.CallArgs }}) diff --git a/templates/templates.go b/templates/templates.go index 23ee30363e7..dac2f5ff08f 100644 --- a/templates/templates.go +++ b/templates/templates.go @@ -1,3 +1,3 @@ package templates -const All = argsTpl + fileTpl + interfaceTpl + objectTpl + inputTpl +const All = argsTpl + fileTpl + interfaceTpl + objectTpl + inputTpl + modelTpl