Skip to content

Commit

Permalink
Merge pull request 99designs#14 from vektah/autogenerate-models
Browse files Browse the repository at this point in the history
Autogenerate models
  • Loading branch information
vektah authored Feb 20, 2018
2 parents 391cfd1 + 3d2711b commit 3323c65
Show file tree
Hide file tree
Showing 32 changed files with 409 additions and 226 deletions.
226 changes: 180 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
14 changes: 12 additions & 2 deletions codegen/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package codegen

import (
"go/build"
"go/types"
"os"
"path/filepath"

Expand All @@ -12,6 +13,7 @@ import (
type Build struct {
PackageName string
Objects Objects
Models Objects
Inputs Objects
Interfaces []*Interface
Imports Imports
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions codegen/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
39 changes: 39 additions & 0 deletions codegen/object_build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()]}

Expand Down
1 change: 1 addition & 0 deletions codegen/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 3323c65

Please sign in to comment.