diff --git a/.editorconfig b/.editorconfig index 17934a118c..157d83ec0d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,3 +10,6 @@ indent_size = 4 [*.{go,gotpl}] indent_style = tab + +[*.{yml,md}] +indent_size = 2 diff --git a/.gitignore b/.gitignore index e6ac9bd268..10ed19500d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /internal/tests/testdata/graphql-js /vendor +/docs/public diff --git a/README.md b/README.md index d2cf50ccad..7724ef0e4a 100644 --- a/README.md +++ b/README.md @@ -2,294 +2,4 @@ This is a library for quickly creating strictly typed graphql servers in golang. -### Getting started - -#### install gqlgen -```bash -go get github.com/vektah/gqlgen -``` - - -#### define a schema -schema.graphql -```graphql schema -schema { - query: Query - mutation: Mutation -} - -type Query { - todos: [Todo!]! -} - -type Mutation { - createTodo(text: String!): Todo! -} - -type Todo { - id: ID! - text: String! - done: Boolean! - user: User! -} - -type User { - id: ID! - name: String! -} -``` - - -#### 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 -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 string -} - -type User struct { - ID string - Name string -} - -type executableSchema struct { - resolvers Resolvers -} - -func (e *executableSchema) Schema() *schema.Schema { - return parsedSchema -} -``` - -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/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 regenerate, this time specifying the type map: - -```bash -gqlgen -out generated.go -package main -typemap types.json -``` - -gqlgen will look at the user defined types and match the fields up finding fields and functions by matching names. - - -#### Finishing touches - -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 -``` - -at the top of our main.go: -```go -//go:generate gorunpkg github.com/vektah/gqlgen -typemap types.json -out generated.go -package main - -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. - - -### Included custom scalar types - -Included in gqlgen there are some custom scalar types that will just work out of the box. - -- Time: An RFC3339 date as a quoted string -- Map: a json object represented as a map[string]interface{}. Useful change sets. - -You are free to redefine these any way you want in types.json, see the [custom scalar example](./example/scalars). - -### Prior art - -#### neelance/graphql-go - -The gold standard of graphql servers in golang. It provided the inspiration, and a good chunk of code for gqlgen. Its -strictly typed and uses your schema and some reflection to build up a resolver graph. The biggest downside is the amount -of work building up all of the resolvers, wrapping every object manually. - -Reasons to use gqlgen instead: - - We bind directly to your types, you dont need to bind manually https://github.com/neelance/graphql-go/issues/28 - - We show you the interface required, no guess work https://github.com/neelance/graphql-go/issues/159 - - We use separate resolvers for query and mutation https://github.com/neelance/graphql-go/issues/145 - - Code generation makes nil pointer juggling explicit, fixing issues like https://github.com/neelance/graphql-go/issues/125 - - Code generating makes binding issues obvious https://github.com/neelance/graphql-go/issues/33 - - Separating the resolvers from the data graph means we only need gofuncs around database calls, reducing the cost of https://github.com/neelance/graphql-go/pull/102 - - arrays work just fine https://github.com/neelance/graphql-go/issues/144 - - first class dataloader support, see examples/dataloader - -https://github.com/neelance/graphql-go - -#### graphql-go/graphql - -With this library you write the schema using its internal DSL as go code, and bind in all your resolvers. No go type -information is used so you can dynamically define new schemas which could be useful for building schema stitching -servers at runtime. - -Reasons to use gqlgen instead: - - strict types. Why go to all the effort of defining gql schemas and then bind it to interface{} everywhere? - - first class dataloader support, see examples/dataloader - - horrible runtime error messages when you mess up defining your schema https://github.com/graphql-go/graphql/issues/234 - - reviewing schema changes written in a go dsl is really hard across teams - -see https://github.com/graphql-go/graphql - -#### Applifier/graphql-codegen and euforic/graphql-gen-go - -Very similar idea, take your schema and generate the code from it. - -gqlgen will build the entire execution environment statically, allowing go's type checker to validate everything across -the the graph. These two libraries generate resolvers that are loaded using reflection by the neelance library, so they -have most of the downsides of that with an added layer of complexity. - -see https://github.com/Applifier/graphql-codegen and https://github.com/euforic/graphql-gen-go +See the [docs](https://vektah.github.io/gqlgen/) for a getting started guide. diff --git a/docs/config.yml b/docs/config.yml new file mode 100644 index 0000000000..a94890a630 --- /dev/null +++ b/docs/config.yml @@ -0,0 +1,18 @@ +baseurl: https://vektah.github.io/gqlgen/ +metadataformat: yaml +title: gqlgen +enableGitInfo: true +pygmentsCodeFences: true +pygmentsUseClasses: true + +params: + name: gqlgen + description: graphql servers the easy way + +menu: + main: + - name: Tutorials + identifier: tutorial + weight: 10 + - name: Reference + identifier: reference diff --git a/docs/content/_index.md b/docs/content/_index.md new file mode 100644 index 0000000000..25853b7cfc --- /dev/null +++ b/docs/content/_index.md @@ -0,0 +1,28 @@ +--- +title: Introduction +description: Type-safe graphql for golang +type: homepage +menu: main +weight: 10 +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: + + - 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. + - Codegen: Let us generate the boring bits, so you can build your app quickly. + + +## Getting started + +First take a look at the [Getting Started](tutorial/getting-started) tutorial. + +If you cant find what your looking for, maybe the [examples](https://github.com/vektah/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. diff --git a/docs/content/reference/scalars.md b/docs/content/reference/scalars.md new file mode 100644 index 0000000000..ff86d9fc0e --- /dev/null +++ b/docs/content/reference/scalars.md @@ -0,0 +1,106 @@ +--- +title: "Custom Scalars" +description: Using custom graphql types in golang +date: 2018-03-17T13:06:47+11:00 +menu: {"main": {"parent": "reference"}} +--- + +There are two different ways to implement scalars in gqlgen, depending on your need. + + +## With user defined types +For user defined types you can implement the graphql.Marshal and graphql.Unmarshal interfaces and they will be called. + +```go +package mypkg + +import ( + "fmt" + "io" + "strings" +) + +type YesNo bool + +// UnmarshalGQL implements the graphql.Marshaler interface +func (y *YesNo) UnmarshalGQL(v interface{}) error { + yes, ok := v.(string) + if !ok { + return fmt.Errorf("points must be strings") + } + + if yes == "yes" { + *y = true + } else { + *y = false + } + return nil +} + +// MarshalGQL implements the graphql.Marshaler interface +func (y YesNo) MarshalGQL(w io.Writer) { + if y { + w.Write([]byte(`"yes"`)) + } else { + w.Write([]byte(`"no"`)) + } +} +``` + +and then in types.json point to the name without the Marshal|Unmarshal in front: +```json +{ + "YesNo": "github.com/me/mypkg.YesNo" +} +``` + + +## Custom scalars for types you don't control + +Sometimes you cant add methods to a type because its in another repo, part of the standard +library (eg string or time.Time). To do this we can build an external marshaler: + +```go +package mypkg + +import ( + "fmt" + "io" + "strings" + + "github.com/vektah/gqlgen/graphql" +) + + +func MarshalMyCustomBooleanScalar(b bool) graphql.Marshaler { + return graphql.WriterFunc(func(w io.Writer) { + if b { + w.Write([]byte("true")) + } else { + w.Write([]byte("false")) + } + }) +} + +func UnmarshalMyCustomBooleanScalar(v interface{}) (bool, error) { + switch v := v.(type) { + case string: + return "true" == strings.ToLower(v), nil + case int: + return v != 0, nil + case bool: + return v, nil + default: + return false, fmt.Errorf("%T is not a bool", v) + } +} +``` + +and then in types.json point to the name without the Marshal|Unmarshal in front: +```json +{ + "MyCustomBooleanScalar": "github.com/me/mypkg.MyCustomBooleanScalar" +} +``` + +see the [example/scalars](https://github.com/vektah/gqlgen/tree/master/example/scalars) package for more examples. diff --git a/docs/content/tutorial/getting-started.md b/docs/content/tutorial/getting-started.md new file mode 100644 index 0000000000..a89f72ba06 --- /dev/null +++ b/docs/content/tutorial/getting-started.md @@ -0,0 +1,240 @@ +--- +title: "Getting Started" +description: Creating a todo graphql server in golang +date: 2018-03-17T13:06:47+11:00 +menu: {"main": {"parent": "tutorial"}} +--- + +## Goal + +The aim for this tutorial is to build a "todo" graphql server that can: + + - get a list of all todos + - create new todos + - mark off todos as they are completed + + +## Install gqlgen + +Assuming you already have a working [go environment](https://golang.org/doc/install) you can simply go get: + +```sh +go get github.com/vektah/gqlgen +``` + + +## 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 + +```graphql +type Todo { + id: ID! + text: String! + done: Boolean! + user: User! +} + +type User { + id: ID! + name: String! +} + +type Query { + todos: [Todo!]! +} + +type Mutation { + createTodo(text: String!): Todo! +} +``` + +## Generate the bindings + +Now that we have defined the shape of our data, and what actions can be taken we can ask gqlgen to convert the schema into code: +```bash +gqlgen -out generated.go -package main +``` + +gqlgen should have created two new files `generated.go` and `models_gen.go`. If we take a peek in both we can see what the server has generated: + +```go +// generated.go +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) +} + +// models_gen.go +type Todo struct { + ID string + Text string + Done bool + UserID string +} + +type User struct { + ID string + Name string +} + +``` + +**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 the resolvers + +Finally, we get to write some code! + +```go +// main.go +package main + +import ( + "context" + "fmt" + "log" + "math/rand" + "net/http" + + "github.com/vektah/gqlgen/handler" +) + +type MyApp struct { + todos []Todo +} + +func (a *MyApp) Query_todos(ctx context.Context) ([]Todo, error) { + return a.todos, nil +} + +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) 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("Todo", "/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 + +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/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 regenerate, this time specifying the type map: + +```bash +gqlgen -out generated.go -package main -typemap types.json +``` + +gqlgen will look at the user defined types and match the fields up finding fields and functions by matching names. + + +## Finishing touches + +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: + +*Note*: If you dont have dep installed yet, you can get it [here](https://github.com/golang/dep) + +```bash +dep init +dep ensure +go get github.com/vektah/gorunpkg +``` + +at the top of our main.go: +```go +//go:generate gorunpkg github.com/vektah/gqlgen -typemap types.json -out generated.go -package main + +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. + diff --git a/docs/layouts/404.html b/docs/layouts/404.html new file mode 100644 index 0000000000..6c298de5ea --- /dev/null +++ b/docs/layouts/404.html @@ -0,0 +1,5 @@ +{{ define "main" }} +