mirror of
https://github.com/ent/ent.git
synced 2026-04-28 13:40:56 +03:00
doc/tutorial: update entgql + gqlgen integration (#2915)
This commit is contained in:
@@ -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!) {
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
Reference in New Issue
Block a user