doc/md: add tutorial on how to generate mutation input types (#1476)

* doc/md: add tutorial on how to generate mutation input types

* address codereview comments
This commit is contained in:
Ariel Mashraki
2021-04-19 12:29:16 +03:00
committed by GitHub
parent 9829ffb385
commit 1ea5d64607
3 changed files with 436 additions and 0 deletions

View File

@@ -0,0 +1,429 @@
---
id: tutorial-todo-gql-mutation-input
title: Mutation Inputs
sidebar_label: Mutation Inputs
---
In this section, we continue the [GraphQL example](tutorial-todo-gql.md) by explaining how to extend the Ent code
generator using Go templates and generate [input type](https://graphql.org/graphql-js/mutations-and-input-types/)
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](https://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:
```console
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.
```gotemplate
{{ 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 }}
{{ $f.StructField }} {{ if and (or $f.Optional $f.Default) (not $f.Type.RType.IsPtr) }}*{{ end }}{{ $f.Type }}
{{- end }}
{{- range $e := $n.Edges }}
{{- if $e.Unique }}
{{ $e.StructField }} {{ if $e.Optional }}*{{ end }}{{ $e.Type.ID.Type }}
{{- else }}
{{ $e.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 }}
{{ $f.StructField }} {{ if not $f.Type.RType.IsPtr }}*{{ end }}{{ $f.Type }}
{{- if $f.Optional }}
{{ "Clear" $f.StructField }} bool
{{- end }}
{{- end }}
{{- range $e := $n.Edges }}
{{- if $e.Unique }}
{{ $e.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:
```diff
func main() {
err := entc.Generate("./schema", &gen.Config{
Templates: entgql.AllTemplates,
- })
+ }, entc.TemplateDir("./template"))
if 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
// 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.
```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!]!
}
```
We're ready now to run the `gqlgen` code generator and generate resolvers for the new mutations.
```
go generate ./...
```
The result is as follows:
```go
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`:
```diff
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
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:
```graphql
mutation CreateTodo($input: CreateTodoInput!) {
createTodo(input: $input) {
id
text
createdAt
priority
parent {
id
}
}
}
```
#### 1st query variables
```json
{
"input": {
"text": "Create GraphQL Example",
"status": "IN_PROGRESS",
"priority": 2
}
}
```
#### Output
```json
{
"data": {
"createTodo": {
"id": "1",
"text": "Create GraphQL Example",
"createdAt": "2021-04-19T10:49:52+03:00",
"priority": 2,
"parent": null
}
}
}
```
#### 2nd query variables
```json
{
"input": {
"text": "Create Tracing Example",
"status": "IN_PROGRESS",
"priority": 2
}
}
```
#### Output
```json
{
"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`.
```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) {
id
text
createdAt
priority
parent {
id
text
}
}
}
```
#### Query variables
```json
{
"id": "2",
"input": {
"parent": 1
}
}
```
#### Output
```json
{
"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"
}
}
}
}
```

View File

@@ -49,3 +49,9 @@ func (mutationResolver) CreateTodo(ctx context.Context, todo TodoInput) (*ent.To
Save(ctx)
}
```
---
Great! With a few lines of code, our application now supports automatic transactional mutations. Please continue to the
next section where we explain how to extend the Ent code generator and generate [GraphQL input types](https://graphql.org/graphql-js/mutations-and-input-types/)
for our GraphQL mutations.

View File

@@ -81,6 +81,7 @@ module.exports = {
'tutorial-todo-gql-paginate',
'tutorial-todo-gql-field-collection',
'tutorial-todo-gql-tx-mutation',
'tutorial-todo-gql-mutation-input',
],
collapsed: false,
},