Files
ent/doc/md/tutorial-todo-gql.mdx

508 lines
12 KiB
Plaintext

---
id: tutorial-todo-gql
title: Introduction
sidebar_label: Introduction
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
In this tutorial, we will learn how to connect Ent to [GraphQL](https://graphql.org) and set up the various integrations
Ent provides, such as:
1. Generating a GraphQL schema for nodes and edges defined in an Ent schema.
2. Auto-generated `Query` and `Mutation` resolvers and provide seamless integration with the [Relay framework](https://relay.dev/).
3. Filtering, pagination (including nested) and compliant support with the [Relay Cursor Connections Spec](https://relay.dev/graphql/connections.htm).
4. Efficient [field collection](tutorial-todo-gql-field-collection.md) to overcome the N+1 problem without requiring data
loaders.
5. [Transactional mutations](tutorial-todo-gql-tx-mutation.md) to ensure consistency in case of failures.
If you're not familiar with GraphQL, it's recommended to go over its [introduction guide](https://graphql.org/learn/)
before going over this tutorial.
#### 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 as follows:
```shell
git clone git@github.com:a8m/ent-graphql-example.git
cd ent-graphql-example
go run ./cmd/todo
```
## Basic Setup
This tutorial begins where the previous one ended (with a working Todo-list schema). We start by installing the
[contrib/entgql](https://pkg.go.dev/entgo.io/contrib/entgql) Ent extension and use it for generating our first schema. Then,
install and configure the [99designs/gqlgen](https://github.com/99designs/gqlgen) framework for building our GraphQL
server and explore the official integration Ent provides to it.
#### Install and configure `entgql`
1\. Install `entgql`:
```shell
go get entgo.io/contrib/entgql@master
```
2\. Add the following annotations to the `Todo` schema to enable `Query` and `Mutation` (creation) capabilities:
```go title="ent/schema/todo.go" {3-4}
func (Todo) Annotations() []schema.Annotation {
return []schema.Annotation{
entgql.QueryField(),
entgql.Mutations(entgql.MutationCreate()),
}
}
```
3\. Create a new Go file named `ent/entc.go`, and paste the following content:
```go title="ent/entc.go"
//go:build ignore
package main
import (
"log"
"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
"entgo.io/contrib/entgql"
)
func main() {
ex, err := entgql.NewExtension(
// Tell Ent to generate a GraphQL schema for
// the Ent schema in a file named ent.graphql.
entgql.WithSchemaGenerator(),
entgql.WithSchemaPath("ent.graphql"),
)
if err != nil {
log.Fatalf("creating entgql extension: %v", err)
}
opts := []entc.Option{
entc.Extensions(ex),
}
if err := entc.Generate("./ent/schema", &gen.Config{}, opts...); err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}
```
:::note
The `ent/entc.go` is ignored using a build tag, and it is executed by the `go generate` command through the `generate.go`
file.
:::
**4\.** Remove the `ent/generate.go` file and create a new one in the **root of the project** with the following
contents. In next steps, `gqlgen` commands will be added to this file as well.
```go title="generate.go"
package todo
//go:generate go run -mod=mod ./ent/entc.go
```
#### Running schema generation
After installing and configuring `entgql`, it is time to execute the codegen:
```shell
go generate .
```
You'll notice a new file was created named `ent.graphql`:
```graphql title="ent.graphql"
directive @goField(forceResolver: Boolean, name: String) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
directive @goModel(model: String, models: [String!]) on OBJECT | INPUT_OBJECT | SCALAR | ENUM | INTERFACE | UNION
"""
Define a Relay Cursor type:
https://relay.dev/graphql/connections.htm#sec-Cursor
"""
scalar Cursor
"""
An object with an ID.
Follows the [Relay Global Object Identification Specification](https://relay.dev/graphql/objectidentification.htm)
"""
interface Node @goModel(model: "todo/ent.Noder") {
"""The id of the object."""
id: ID!
}
# ...
```
#### Install and configure `gqlgen`
1\. Install `99designs/gqlgen`:
```shell
go get github.com/99designs/gqlgen
```
2\. The gqlgen package can be configured using a `gqlgen.yml` file that is automatically loaded from the current directory.
Let's add this file to the root of the project. Follow the comments in this file to understand what each config directive
means:
```yaml title="gqlgen.yml"
# schema tells gqlgen where the GraphQL schema is located.
schema:
- ent.graphql
# resolver reports where the resolver implementations go.
resolver:
layout: follow-schema
dir: .
# gqlgen will search for any type names in the schema in these go packages
# if they match it will use them, otherwise it will generate them.
# autobind tells gqngen to search for any type names in the GraphQL schema in the
# provided package. If they match it will use them, otherwise it will generate new.
autobind:
- todo/ent
- todo/ent/todo
# This section declares type mapping between the GraphQL and Go type systems.
models:
# Defines the ID field as Go 'int'.
ID:
model:
- github.com/99designs/gqlgen/graphql.IntID
Node:
model:
- todo/ent.Noder
```
3\. Edit the `ent/entc.go` to let Ent know about the `gqlgen` configuration:
```go
//go:build ignore
package main
import (
"log"
"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
"entgo.io/contrib/entgql"
)
func main() {
ex, err := entgql.NewExtension(
// Tell Ent to generate a GraphQL schema for
// the Ent schema in a file named ent.graphql.
entgql.WithSchemaGenerator(),
entgql.WithSchemaPath("ent.graphql"),
//highlight-next-line
entgql.WithConfigPath("gqlgen.yml"),
)
if err != nil {
log.Fatalf("creating entgql extension: %v", err)
}
opts := []entc.Option{
entc.Extensions(ex),
}
if err := entc.Generate("./ent/schema", &gen.Config{}, opts...); err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}
```
4\. Add the `gqlgen` generate command to the `generate.go` file:
```go title="generate.go"
package todo
//go:generate go run -mod=mod ./ent/entc.go
//highlight-next-line
//go:generate go run -mod=mod github.com/99designs/gqlgen
```
Now, we're ready to run `go generate` to trigger `ent` and `gqlgen` code generation. Execute the following command from
the root of the project:
```shell
go generate .
```
You may have noticed that some files were generated by `gqlgen`:
```console
tree -L 1
.
├── ent/
├── ent.graphql
//highlight-next-line
├── ent.resolvers.go
├── example_test.go
├── generate.go
//highlight-next-line
├── generated.go
├── go.mod
├── go.sum
├── gqlgen.yml
//highlight-next-line
└── resolver.go
```
## Basic Server
Before building the GraphQL server we need to set up the main schema `Resolver` defined in `resolver.go`.
`gqlgen` allows changing the generated `Resolver` and adding dependencies to it. Let's use `ent.Client` as
a dependency by pasting the following in `resolver.go`:
```go title="resolver.go"
package todo
import (
"todo/ent"
"github.com/99designs/gqlgen/graphql"
)
// Resolver is the resolver root.
type Resolver struct{ client *ent.Client }
// NewSchema creates a graphql executable schema.
func NewSchema(client *ent.Client) graphql.ExecutableSchema {
return NewExecutableSchema(Config{
Resolvers: &Resolver{client},
})
}
```
After setting up the main resolver, we create a new directory `cmd/todo` and a `main.go` file with the following code
to set up a GraphQL server:
```go title="cmd/todo/main.go"
package main
import (
"context"
"log"
"net/http"
"todo"
"todo/ent"
"todo/ent/migrate"
"entgo.io/ent/dialect"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
_ "github.com/mattn/go-sqlite3"
)
func main() {
// Create ent.Client and run the schema migration.
client, err := ent.Open(dialect.SQLite, "file:ent?mode=memory&cache=shared&_fk=1")
if err != nil {
log.Fatal("opening ent client", err)
}
if err := client.Schema.Create(
context.Background(),
migrate.WithGlobalUniqueID(true),
); err != nil {
log.Fatal("opening ent client", err)
}
// Configure the server and start listening on :8081.
srv := handler.NewDefaultServer(todo.NewSchema(client))
http.Handle("/",
playground.Handler("Todo", "/query"),
)
http.Handle("/query", srv)
log.Println("listening on :8081")
if err := http.ListenAndServe(":8081", nil); err != nil {
log.Fatal("http server terminated", err)
}
}
```
Run the server using the command below, and open [localhost:8081](http://localhost:8081):
```console
go run ./cmd/todo
```
You should see the interactive playground:
![tutorial-todo-playground](https://entgo.io/images/assets/tutorial-gql-playground.png)
If you are having trouble with getting the playground to run, go to [first section](#clone-the-code-optional) and
clone the example repository.
## Query Todos
If we try to query our todo list, we'll get an error as the resolver method is not yet implemented.
Let's implement the resolver by replacing the `Todos` implementation in the query resolver:
```diff title="ent.resolvers.go"
func (r *queryResolver) Todos(ctx context.Context) ([]*ent.Todo, error) {
- panic(fmt.Errorf("not implemented"))
+ return r.client.Todo.Query().All(ctx)
}
```
Then, running this GraphQL query should return an empty todo list:
<Tabs
defaultValue="graphql"
values={[
{label: 'GraphQL', value: 'graphql'},
{label: 'Output', value: 'output'},
]}>
<TabItem value="graphql">
```graphql
query AllTodos {
todos {
id
}
}
```
</TabItem>
<TabItem value="output">
```json
{
"data": {
"todos": []
}
}
```
</TabItem>
</Tabs>
## Mutating Todos
As you can see above, our GraphQL schema returns an empty list of todo items. Let's create a few todo items, but this time
we'll do it from GraphQL. Luckily, Ent provides auto generated mutations for creating and updating nodes and edges.
1\. We start by extending our GraphQL schema with custom mutations. Let's create a new file named `todo.graphql`
and add our `Mutation` type:
```graphql title="todo.graphql"
type Mutation {
# The input and the output are types generated by Ent.
createTodo(input: CreateTodoInput!): Todo
}
```
2\. Add the custom GraphQL schema to `gqlgen.yml` configuration:
```yaml title="gqlgen.yml"
schema:
- ent.graphql
//highlight-next-line
- todo.graphql
# ...
```
3\. Run code generation:
```shell
go generate .
```
As you can see, `gqlgen` generated for us a new file named `todo.resolvers.go` with the `createTodo` resolver. Let's
connect it to Ent generated code, and ask Ent to handle this mutation:
```diff title="todo.resolvers.go"
func (r *mutationResolver) CreateTodo(ctx context.Context, input ent.CreateTodoInput) (*ent.Todo, error) {
- panic(fmt.Errorf("not implemented: CreateTodo - createTodo"))
+ return r.client.Todo.Create().SetInput(input).Save(ctx)
}
```
4\. Re-run `go run ./cmd/todo` again and go to the playground:
## Demo
At this stage, we are ready to create a todo item and query it:
<Tabs
defaultValue="mutation"
values={[
{label: 'Mutation', value: 'mutation'},
{label: 'Mutation Output', value: 'mutation_output'},
{label: 'Query', value: 'query'},
{label: 'Query Output', value: 'query_output'},
]}>
<TabItem value="mutation">
```graphql
mutation CreateTodo {
createTodo(input: {text: "Create GraphQL Example", status: IN_PROGRESS, priority: 1}) {
id
text
createdAt
priority
}
}
```
</TabItem>
<TabItem value="mutation_output">
```json
{
"data": {
"createTodo": {
"id": "1",
"text": "Create GraphQL Example",
"createdAt": "2022-09-08T15:20:58.696576+03:00",
"priority": 1,
}
}
}
```
</TabItem>
<TabItem value="query">
```graphql
query {
todos {
id
text
status
}
}
```
</TabItem>
<TabItem value="query_output">
```json
{
"data": {
"todos": [
{
"id": "1",
"text": "Create GraphQL Example",
"status": "IN_PROGRESS"
}
]
}
}
```
</TabItem>
</Tabs>
If you're having trouble with getting this example to work, go to [first section](#clone-the-code-optional) and clone the
example repository.
---
Please continue to the next section where we explain how to implement the
[Relay Node Interface](https://relay.dev/graphql/objectidentification.htm) and learn how Ent automatically supports this.