跳到主要内容

Mutation Inputs

In this section, we continue the GraphQL example by explaining how to extend the Ent code generator using Go templates and generate input type objects for our GraphQL mutations that can be applied directly on Ent mutations.

Clone the code (optional)

The code for this tutorial is available under github.com/a8m/ent-graphql-example, and tagged (using Git) in each step. If you want to skip the basic setup and start with the initial version of the GraphQL server, you can clone the repository and run the program as follows:

git clone git@github.com:a8m/ent-graphql-example.git
cd ent-graphql-example
go run ./cmd/todo/

Go Templates

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.

{{ 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.

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 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. :::

Now, we tell the Ent code generator to execute this template by passing it as an argument in the ent/entc.go file:

ent/entc.go
func main() {
ex, err := entgql.NewExtension()
if err != nil {
log.Fatalf("creating entgql extension: %v", err)
}
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:

// Code generated by entc, 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

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.

# 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!]!
}

We're ready now to run the gqlgen code generator and generate resolvers for the new mutations.

go generate ./...

The result is as follows:

func (r *mutationResolver) CreateTodo(ctx context.Context, input ent.CreateTodoInput) (*ent.Todo, error) {
panic(fmt.Errorf("not implemented"))
}

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"))
}

Apply Input Types on ent.Client

The Set<F> calls in the CreateTodo resolver are replaced with one call named SetInput:

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:

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:

mutation CreateTodo($input: CreateTodoInput!) {
createTodo(input: $input) {
id
text
createdAt
priority
parent {
id
}
}
}

1st query variables

{
"input": {
"text": "Create GraphQL Example",
"status": "IN_PROGRESS",
"priority": 2
}
}

Output

{
"data": {
"createTodo": {
"id": "1",
"text": "Create GraphQL Example",
"createdAt": "2021-04-19T10:49:52+03:00",
"priority": 2,
"parent": null
}
}
}

2nd query variables

{
"input": {
"text": "Create Tracing Example",
"status": "IN_PROGRESS",
"priority": 2
}
}

Output

{
"data": {
"createTodo": {
"id": "2",
"text": "Create Tracing Example",
"createdAt": "2021-04-19T10:50:01+03:00",
"priority": 2,
"parent": null
}
}
}

Test the UpdateTodos Resolver

We continue the example by updating the priority of the 2 todo items to 1.

mutation UpdateTodos($ids: [ID!]!, $input: UpdateTodoInput!) {
updateTodos(ids: $ids, input: $input) {
id
text
createdAt
priority
parent {
id
}
}
}

Query variables

{
"ids": ["1", "2"],
"input": {
"priority": 1
}
}

Output

{
"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.

mutation UpdateTodo($id: ID!, $input: UpdateTodoInput!) {
updateTodo(id: $id, input: $input) {
id
text
createdAt
priority
parent {
id
text
}
}
}

Query variables

{
"id": "2",
"input": {
"parent": 1
}
}

Output

{
"data": {
"updateTodo": {
"id": "2",
"text": "Create Tracing Example",
"createdAt": "2021-04-19T10:50:01+03:00",
"priority": 1,
"parent": {
"id": "1",
"text": "Create GraphQL Example"
}
}
}
}