mirror of
https://github.com/ent/ent.git
synced 2026-04-28 13:40:56 +03:00
508 lines
12 KiB
Plaintext
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) 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 when 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:
|
|
|
|

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