doc/tutorial: update entgql + gqlgen integration (#2915)

This commit is contained in:
Ariel Mashraki
2022-09-08 17:56:11 +03:00
committed by GitHub
parent d0943109b5
commit 82ee525676
6 changed files with 625 additions and 834 deletions

View File

@@ -20,255 +20,68 @@ cd ent-graphql-example
go run ./cmd/todo/
```
## Go Templates
## Mutation Types
The Ent framework accepts external templates that can extend or override the default generated functionality of its
code generator. In the template below, we generate 2 input types (`CreateTodoInput` and `UpdateTodoInput`) for the
GraphQL mutations, and add additional methods on the different builders to accept these objects as an input type.
Ent supports generating mutation types. A mutation type can be accepted as an input for GraphQL mutations, and it is
handled and verified by Ent. Let's tell Ent that our GraphQL `Todo` type supports create and update operations:
```gotemplate title="ent/template/mutation_input.tmpl"
{{ range $n := $.Nodes }}
{{ $input := print "Create" $n.Name "Input" }}
// {{ $input }} represents a mutation input for creating {{ plural $n.Name | lower }}.
type {{ $input }} struct {
{{- range $f := $n.Fields }}
{{- if not $f.IsEdgeField }}
{{ $f.StructField }} {{ if and (or $f.Optional $f.Default) (not $f.Type.RType.IsPtr) }}*{{ end }}{{ $f.Type }}
{{- end }}
{{- end }}
{{- range $e := $n.Edges }}
{{- if $e.Unique }}
{{- $structField := print (pascal $e.Name) "ID" }}
{{ $structField }} {{ if $e.Optional }}*{{ end }}{{ $e.Type.ID.Type }}
{{- else }}
{{- $structField := print (singular $e.Name | pascal) "IDs" }}
{{ $structField }} []{{ $e.Type.ID.Type }}
{{- end }}
{{- end }}
}
{{/* Additional methods go here. */}}
{{ $input = print "Update" $n.Name "Input" }}
// {{ $input }} represents a mutation input for updating {{ plural $n.Name | lower }}.
type {{ $input }} struct {
{{- range $f := $n.MutableFields }}
{{- if not $f.IsEdgeField }}
{{ $f.StructField }} {{ if not $f.Type.RType.IsPtr }}*{{ end }}{{ $f.Type }}
{{- if $f.Optional }}
{{ print "Clear" $f.StructField }} bool
{{- end }}
{{- end }}
{{- end }}
{{- range $e := $n.Edges }}
{{- if $e.Unique }}
{{- $structField := print (pascal $e.Name) "ID" }}
{{ $structField }} *{{ $e.Type.ID.Type }}
{{ $e.MutationClear }} bool
{{- else }}
{{ $e.MutationAdd }} []{{ $e.Type.ID.Type }}
{{ $e.MutationRemove }} []{{ $e.Type.ID.Type }}
{{- end }}
{{- end }}
}
{{/* Additional methods go here. */}}
{{ end }}
```
The full version of this template exists in the [github.com/a8m/ent-graphql-example/ent/template](https://github.com/a8m/ent-graphql-example/blob/master/ent/template/mutation_input.tmpl).
:::info
If you have no experience with Go templates or if you have not used it before with the Ent code generator, go to the
[template documentation](templates.md) to learn more about it.
The full documentation for the template API (Go types and functions) is available in the
[pkg.go.dev/entgo.io/ent/entc/gen](https://pkg.go.dev/entgo.io/ent/entc/gen).
:::
Now, we tell the Ent code generator to execute this template by passing it as an argument in the `ent/entc.go` file:
```go {8} title="ent/entc.go"
func main() {
ex, err := entgql.NewExtension()
if err != nil {
log.Fatalf("creating entgql extension: %v", err)
```go title="ent/schema/todo.go"
func (Todo) Annotations() []schema.Annotation {
return []schema.Annotation{
entgql.QueryField(),
//highlight-next-line
entgql.Mutations(entgql.MutationCreate(), entgql.MutationUpdate()),
}
opts := []entc.Option{
entc.Extensions(ex),
entc.TemplateDir("./template"),
}
if err := entc.Generate("./schema", &gen.Config{}, opts...); err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}
```
After adding the template file to the `ent/template/` directory and changing the `entc.go` configuration, we're ready
to execute the code generation as follows:
```
go generate ./...
```
You may have noticed that Ent generated a new file `ent/mutation_input.go` with the following content:
```go title="ent/template/mutation_input.go"
// Code generated by ent, DO NOT EDIT.
package ent
import (
"time"
"todo/ent/todo"
)
// CreateTodoInput represents a mutation input for creating todos.
type CreateTodoInput struct {
Text string
CreatedAt time.Time
Status todo.Status
Priority int
Children []int
Parent *int
}
// Mutate applies the CreateTodoInput on the TodoCreate builder.
func (i *CreateTodoInput) Mutate(m *TodoCreate) {
// ...
}
// UpdateTodoInput represents a mutation input for updating todos.
type UpdateTodoInput struct {
Text *string
Status *todo.Status
Priority *int
AddChildIDs []int
RemoveChildIDs []int
Parent *int
ClearParent bool
}
// Mutate applies the UpdateTodoInput on the TodoMutation.
func (i *UpdateTodoInput) Mutate(m *TodoMutation) {
// ...
}
```
## Input Types In GraphQL Schema
Then, run code generation:
The new generated Go types are the GraphQL mutation types. Let's define them manually in the GraphQL schema and `gqlgen`
will map them automatically.
```go
go generate .
```
You'll notice that Ent generated for you 2 types: `ent.CreateTodoInput` and `ent.UpdateTodoInput`.
## Mutations
After generating our mutation inputs, we can connect them to the GraphQL mutations:
```graphql title="todo.graphql"
# Define an input type for the mutation below.
# https://graphql.org/learn/schema/#input-types
#
# Note that, this type is mapped to the generated
# input type in mutation_input.go.
input CreateTodoInput {
status: Status! = IN_PROGRESS
priority: Int
text: String!
text: String
parent: ID
children: [ID!]
}
# Define an input type for the mutation below.
# https://graphql.org/learn/schema/#input-types
#
# Note that, this type is mapped to the generated
# input type in mutation_input.go.
input UpdateTodoInput {
status: Status
priority: Int
text: String
parent: ID
clearParent: Boolean
addChildIDs: [ID!]
removeChildIDs: [ID!]
}
# Define a mutation for creating todos.
# https://graphql.org/learn/queries/#mutations
type Mutation {
createTodo(input: CreateTodoInput!): Todo!
updateTodo(id: ID!, input: UpdateTodoInput!): Todo!
updateTodos(ids: [ID!]!, input: UpdateTodoInput!): [Todo!]!
createTodo(input: CreateTodoInput!): Todo!
updateTodo(id: ID!, input: UpdateTodoInput!): Todo!
}
```
We're ready now to run the `gqlgen` code generator and generate resolvers for the new mutations.
Running code generation we'll generate the actual mutations and the only thing left after that is to bind the resolvers
to Ent.
```go
go generate .
```
go generate ./...
```
The result is as follows:
```go title="todo.resolvers.go"
// CreateTodo is the resolver for the createTodo field.
func (r *mutationResolver) CreateTodo(ctx context.Context, input ent.CreateTodoInput) (*ent.Todo, error) {
panic(fmt.Errorf("not implemented"))
return r.client.Todo.Create().SetInput(input).Save(ctx)
}
// UpdateTodo is the resolver for the updateTodo field.
func (r *mutationResolver) UpdateTodo(ctx context.Context, id int, input ent.UpdateTodoInput) (*ent.Todo, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *mutationResolver) UpdateTodos(ctx context.Context, ids []int, input ent.UpdateTodoInput) ([]*ent.Todo, error) {
panic(fmt.Errorf("not implemented"))
return r.client.Todo.UpdateOneID(id).SetInput(input).Save(ctx)
}
```
## Apply Input Types on `ent.Client`
The `Set<F>` calls in the `CreateTodo` resolver are replaced with one call named `SetInput`:
```diff title="todo.resolvers.go"
func (r *mutationResolver) CreateTodo(ctx context.Context, input ent.CreateTodoInput) (*ent.Todo, error) {
return ent.FromContext(ctx).Todo.
Create().
- SetText(todo.Text).
- SetStatus(todo.Status).
- SetNillablePriority(todo.Priority). // Set the "priority" field if provided.
- SetNillableParentID(todo.Parent). // Set the "parent_id" field if provided.
+ SetInput(input)
Save(ctx)
}
```
The rest of the resolvers (`UpdateTodo` and `UpdateTodos`) will be implemented as follows:
```diff title="todo.resolvers.go"
func (r *mutationResolver) CreateTodo(ctx context.Context, input ent.CreateTodoInput) (*ent.Todo, error) {
return ent.FromContext(ctx).Todo.Create().SetInput(input).Save(ctx)
}
func (r *mutationResolver) UpdateTodo(ctx context.Context, id int, input ent.UpdateTodoInput) (*ent.Todo, error) {
- panic(fmt.Errorf("not implemented"))
+ return ent.FromContext(ctx).Todo.UpdateOneID(id).SetInput(input).Save(ctx)
}
func (r *mutationResolver) UpdateTodos(ctx context.Context, ids []int, input ent.UpdateTodoInput) ([]*ent.Todo, error) {
- panic(fmt.Errorf("not implemented"))
+ client := ent.FromContext(ctx)
+ if err := client.Todo.Update().Where(todo.IDIn(ids...)).SetInput(input).Exec(ctx); err != nil {
+ return nil, err
+ }
+ return client.Todo.Query().Where(todo.IDIn(ids...)).All(ctx)
}
```
Hurray! We're now ready to test our GraphQL resolvers.
## Test the `CreateTodo` Resolver
Let's start with creating 2 todo items by executing this query with the variables below:
Let's start with creating 2 todo items by executing the `createTodo` mutations twice.
#### Mutation
```graphql
mutation CreateTodo($input: CreateTodoInput!) {
createTodo(input: $input) {
mutation CreateTodo {
createTodo(input: {text: "Create GraphQL Example", status: IN_PROGRESS, priority: 2}) {
id
text
createdAt
@@ -280,18 +93,6 @@ mutation CreateTodo($input: CreateTodoInput!) {
}
```
#### 1st query variables
```json
{
"input": {
"text": "Create GraphQL Example",
"status": "IN_PROGRESS",
"priority": 2
}
}
```
#### Output
```json
@@ -308,16 +109,20 @@ mutation CreateTodo($input: CreateTodoInput!) {
}
```
#### 2nd query variables
#### Mutation
```json
{
"input": {
"text": "Create Tracing Example",
"status": "IN_PROGRESS",
"priority": 2
}
}
```graphql
mutation CreateTodo {
createTodo(input: {text: "Create Tracing Example", status: IN_PROGRESS, priority: 2}) {
id
text
createdAt
priority
parent {
id
}
}
}
```
#### Output
@@ -336,67 +141,13 @@ mutation CreateTodo($input: CreateTodoInput!) {
}
```
## Test the `UpdateTodos` Resolver
We continue the example by updating the `priority` of the 2 todo items to `1`.
```graphql
mutation UpdateTodos($ids: [ID!]!, $input: UpdateTodoInput!) {
updateTodos(ids: $ids, input: $input) {
id
text
createdAt
priority
parent {
id
}
}
}
```
#### Query variables
```json
{
"ids": ["1", "2"],
"input": {
"priority": 1
}
}
```
#### Output
```json
{
"data": {
"updateTodos": [
{
"id": "1",
"text": "Create GraphQL Example",
"createdAt": "2021-04-19T10:49:52+03:00",
"priority": 1,
"parent": null
},
{
"id": "2",
"text": "Create Tracing Example",
"createdAt": "2021-04-19T10:50:01+03:00",
"priority": 1,
"parent": null
}
]
}
}
```
## Test the `UpdateTodo` Resolver
The only thing left is to test the `UpdateTodo` resolver. Let's use it to update the `parent` of the 2nd todo item to `1`.
```graphql
mutation UpdateTodo($id: ID!, $input: UpdateTodoInput!) {
updateTodo(id: $id, input: $input) {
mutation UpdateTodo {
updateTodo(id: 2, input: {parent: 1}) {
id
text
createdAt
@@ -409,17 +160,6 @@ mutation UpdateTodo($id: ID!, $input: UpdateTodoInput!) {
}
```
#### Query variables
```json
{
"id": "2",
"input": {
"parent": 1
}
}
```
#### Output
```json
@@ -437,4 +177,4 @@ mutation UpdateTodo($id: ID!, $input: UpdateTodoInput!) {
}
}
}
```
```