snapshot of https://github.com/ent/ent.git (master, 2026-09-15; blobs > 131072B omitted)
This commit is contained in:
373
doc/md/graphql.md
Normal file
373
doc/md/graphql.md
Normal file
@@ -0,0 +1,373 @@
|
||||
---
|
||||
id: graphql
|
||||
title: GraphQL Integration
|
||||
---
|
||||
|
||||
The Ent framework supports GraphQL using the [99designs/gqlgen](https://github.com/99designs/gqlgen) library and
|
||||
provides various integrations, 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.
|
||||
|
||||
Check out the website's [GraphQL tutorial](tutorial-todo-gql.mdx#basic-setup) for more information.
|
||||
|
||||
|
||||
## Quick Introduction
|
||||
|
||||
In order to enable the [`entgql`](https://github.com/ent/contrib/tree/master/entgql) extension to your
|
||||
project, you need to use the `entc` (ent codegen) package as described [here](code-gen.md#use-entc-as-a-package).
|
||||
Follow these 3 steps to enable it to your project:
|
||||
|
||||
1\. Create a new Go file named `ent/entc.go`, and paste the following content:
|
||||
|
||||
```go title="ent/entc.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()
|
||||
if err != nil {
|
||||
log.Fatalf("creating entgql extension: %v", err)
|
||||
}
|
||||
if err := entc.Generate("./schema", &gen.Config{}, entc.Extensions(ex)); err != nil {
|
||||
log.Fatalf("running ent codegen: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2\. Edit the `ent/generate.go` file to execute the `ent/entc.go` file:
|
||||
|
||||
```go title="ent/generate.go"
|
||||
package ent
|
||||
|
||||
//go:generate go run -mod=mod entc.go
|
||||
```
|
||||
|
||||
Note that `ent/entc.go` is ignored using a build tag, and it's executed by the `go generate` command
|
||||
through the `generate.go` file. The full example can be found in the [ent/contrib repository](https://github.com/ent/contrib/blob/master/entgql/internal/todo).
|
||||
|
||||
|
||||
3\. Run codegen for your ent project:
|
||||
|
||||
```console
|
||||
go generate ./...
|
||||
```
|
||||
|
||||
After running codegen, the following add-ons will be added to your project.
|
||||
|
||||
## Node API
|
||||
|
||||
A new file named `ent/gql_node.go` was created that implements the [Relay Node interface](https://relay.dev/graphql/objectidentification.htm).
|
||||
|
||||
In order to use the new generated `ent.Noder` interface in the [GraphQL resolver](https://gqlgen.com/reference/resolvers/),
|
||||
add the `Node` method to the query resolver, and look at the [configuration](#gql-configuration) section to understand
|
||||
how to use it.
|
||||
|
||||
If you are using the [Universal IDs](migrate.md#universal-ids) option in the schema migration, the NodeType is derived
|
||||
from the id value and can be used as follows:
|
||||
|
||||
```go
|
||||
func (r *queryResolver) Node(ctx context.Context, id int) (ent.Noder, error) {
|
||||
return r.client.Noder(ctx, id)
|
||||
}
|
||||
```
|
||||
|
||||
However, if you use a custom format for the global unique identifiers, you can control the NodeType as follows:
|
||||
|
||||
```go
|
||||
func (r *queryResolver) Node(ctx context.Context, guid string) (ent.Noder, error) {
|
||||
typ, id := parseGUID(guid)
|
||||
return r.client.Noder(ctx, id, ent.WithFixedNodeType(typ))
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## GQL Configuration
|
||||
|
||||
Here's a configuration example for a todo app as exists in [ent/contrib/entgql/todo](https://github.com/ent/contrib/tree/master/entgql/internal/todo).
|
||||
|
||||
```yaml
|
||||
schema:
|
||||
- todo.graphql
|
||||
|
||||
resolver:
|
||||
# Tell gqlgen to generate resolvers next to the schema file.
|
||||
layout: follow-schema
|
||||
dir: .
|
||||
|
||||
# gqlgen will search for any type names in the schema in the generated
|
||||
# ent package. If they match it will use them, otherwise it will new ones.
|
||||
autobind:
|
||||
- entgo.io/contrib/entgql/internal/todo/ent
|
||||
|
||||
models:
|
||||
ID:
|
||||
model:
|
||||
- github.com/99designs/gqlgen/graphql.IntID
|
||||
Node:
|
||||
model:
|
||||
# ent.Noder is the new interface generated by the Node template.
|
||||
- entgo.io/contrib/entgql/internal/todo/ent.Noder
|
||||
```
|
||||
|
||||
## Pagination
|
||||
|
||||
The pagination template adds a pagination support according to the _Relay Cursor Connections Spec_. More info
|
||||
about the Relay Spec can be found in its [website](https://relay.dev/graphql/connections.htm).
|
||||
|
||||
## Connection Ordering
|
||||
|
||||
The ordering option allows us to apply an ordering on the edges returned from a connection.
|
||||
|
||||
### Usage Notes
|
||||
|
||||
- The generated types will be `autobind`ed to GraphQL types if a naming convention is preserved (see example below).
|
||||
- Ordering fields should normally be [indexed](schema-indexes.md) to avoid full table DB scan.
|
||||
- Pagination queries can be sorted by a single field (no order by ... then by ... semantics).
|
||||
|
||||
### Example
|
||||
|
||||
Let's go over the steps needed in order to add ordering to an existing GraphQL type.
|
||||
The code example is based on a todo-app that can be found in [ent/contrib/entql/todo](https://github.com/ent/contrib/tree/master/entgql/internal/todo).
|
||||
|
||||
### Defining order fields in ent/schema
|
||||
|
||||
Ordering can be defined on any comparable field of ent by annotating it with `entgql.Annotation`.
|
||||
Note that the given `OrderField` name must match its enum value in graphql schema.
|
||||
```go
|
||||
func (Todo) Fields() []ent.Field {
|
||||
return []ent.Field{
|
||||
field.Time("created_at").
|
||||
Default(time.Now).
|
||||
Immutable().
|
||||
Annotations(
|
||||
entgql.OrderField("CREATED_AT"),
|
||||
),
|
||||
field.Enum("status").
|
||||
NamedValues(
|
||||
"InProgress", "IN_PROGRESS",
|
||||
"Completed", "COMPLETED",
|
||||
).
|
||||
Annotations(
|
||||
entgql.OrderField("STATUS"),
|
||||
),
|
||||
field.Int("priority").
|
||||
Default(0).
|
||||
Annotations(
|
||||
entgql.OrderField("PRIORITY"),
|
||||
),
|
||||
field.Text("text").
|
||||
NotEmpty().
|
||||
Annotations(
|
||||
entgql.OrderField("TEXT"),
|
||||
),
|
||||
}
|
||||
}
|
||||
```
|
||||
That's all the schema changes required, make sure to run `go generate` to apply them.
|
||||
|
||||
### Define ordering types in GraphQL schema
|
||||
|
||||
Next we need to define the ordering types in graphql schema:
|
||||
```graphql
|
||||
enum OrderDirection {
|
||||
ASC
|
||||
DESC
|
||||
}
|
||||
|
||||
enum TodoOrderField {
|
||||
CREATED_AT
|
||||
PRIORITY
|
||||
STATUS
|
||||
TEXT
|
||||
}
|
||||
|
||||
input TodoOrder {
|
||||
direction: OrderDirection!
|
||||
field: TodoOrderField
|
||||
}
|
||||
```
|
||||
Note that the naming must take the form of `<T>OrderField` / `<T>Order` for `autobind`ing to the generated ent types.
|
||||
Alternatively [@goModel](https://gqlgen.com/config/#inline-config-with-directives) directive can be used for manual type binding.
|
||||
|
||||
### Adding orderBy argument to the pagination query
|
||||
```graphql
|
||||
type Query {
|
||||
todos(
|
||||
after: Cursor
|
||||
first: Int
|
||||
before: Cursor
|
||||
last: Int
|
||||
orderBy: TodoOrder
|
||||
): TodoConnection!
|
||||
}
|
||||
```
|
||||
That's all for the GraphQL schema changes, let's run `gqlgen` code generation.
|
||||
|
||||
### Update the underlying resolver
|
||||
|
||||
Head over to the Todo resolver and update it to pass `orderBy` argument to `.Paginate()` call:
|
||||
|
||||
```go
|
||||
func (r *queryResolver) Todos(ctx context.Context, after *ent.Cursor, first *int, before *ent.Cursor, last *int, orderBy *ent.TodoOrder) (*ent.TodoConnection, error) {
|
||||
return r.client.Todo.Query().
|
||||
Paginate(ctx, after, first, before, last,
|
||||
ent.WithTodoOrder(orderBy),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Use in GraphQL
|
||||
|
||||
```graphql
|
||||
query {
|
||||
todos(first: 3, orderBy: {direction: DESC, field: TEXT}) {
|
||||
edges {
|
||||
node {
|
||||
text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Fields Collection
|
||||
|
||||
The collection template adds support for automatic [GraphQL fields collection](https://spec.graphql.org/June2018/#sec-Field-Collection)
|
||||
for ent-edges using eager-loading. That means, if a query asks for nodes and their edges, entgql will add automatically [`With<E>`](eager-load.mdx#api)
|
||||
steps to the root query, and as a result, the client will execute constant number of queries to the database - and it works recursively.
|
||||
|
||||
For example, given this GraphQL query:
|
||||
|
||||
```graphql
|
||||
query {
|
||||
users(first: 100) {
|
||||
edges {
|
||||
node {
|
||||
photos {
|
||||
link
|
||||
}
|
||||
posts {
|
||||
content
|
||||
comments {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The client will execute 1 query for getting the users, 1 for getting the photos, and another 2 for getting the posts,
|
||||
and their comments (4 in total). This logic works both for root queries/resolvers and for the node(s) API.
|
||||
|
||||
### Schema configuration
|
||||
|
||||
In order to configure this option to specific edges, use the `entgql.Annotation` as follows:
|
||||
|
||||
```go
|
||||
func (Todo) Edges() []ent.Edge {
|
||||
return []ent.Edge{
|
||||
edge.To("children", Todo.Type).
|
||||
Annotations(entgql.Bind()).
|
||||
From("parent").
|
||||
// Bind implies the edge name in graphql schema is
|
||||
// equivalent to the name used in ent schema.
|
||||
Annotations(entgql.Bind()).
|
||||
Unique(),
|
||||
edge.From("owner", User.Type).
|
||||
Ref("tasks").
|
||||
// Map edge names as defined in graphql schema.
|
||||
Annotations(entgql.MapsTo("taskOwner")),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Usage and Configuration
|
||||
|
||||
The GraphQL extension generates also edge-resolvers for the nodes under the `gql_edge.go` file as follows:
|
||||
```go
|
||||
func (t *Todo) Children(ctx context.Context) ([]*Todo, error) {
|
||||
result, err := t.Edges.ChildrenOrErr()
|
||||
if IsNotLoaded(err) {
|
||||
result, err = t.QueryChildren().All(ctx)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
```
|
||||
|
||||
However, if you need to explicitly write these resolvers by hand, you can add the
|
||||
[`forceResolver`](https://gqlgen.com/master/config#inline-config-with-directives) option to your GraphQL schema:
|
||||
|
||||
```graphql
|
||||
type Todo implements Node {
|
||||
id: ID!
|
||||
children: [Todo]! @goField(forceResolver: true)
|
||||
}
|
||||
```
|
||||
|
||||
Then, you can implement it on your type resolver.
|
||||
|
||||
```go
|
||||
func (r *todoResolver) Children(ctx context.Context, obj *ent.Todo) ([]*ent.Todo, error) {
|
||||
// Do something here.
|
||||
return obj.Edges.ChildrenOrErr()
|
||||
}
|
||||
```
|
||||
|
||||
## Enum Implementation
|
||||
|
||||
The enum template implements the MarshalGQL/UnmarshalGQL methods for enums generated by ent.
|
||||
|
||||
## Transactional Mutations
|
||||
|
||||
The `entgql.Transactioner` handler executes each GraphQL mutation in a transaction. The injected client for the resolver
|
||||
is a [transactional `ent.Client`](transactions.md#transactional-client).
|
||||
Hence, code that uses `ent.Client` won't need to be changed. In order to use it, follow these steps:
|
||||
|
||||
|
||||
1\. In the GraphQL server initialization, use the `entgql.Transactioner` handler as follows:
|
||||
|
||||
```go
|
||||
srv := handler.NewDefaultServer(todo.NewSchema(client))
|
||||
srv.Use(entgql.Transactioner{TxOpener: client})
|
||||
```
|
||||
|
||||
2\. Then, in the GraphQL mutations, use the client from context as follows:
|
||||
```go
|
||||
func (mutationResolver) CreateTodo(ctx context.Context, todo TodoInput) (*ent.Todo, error) {
|
||||
client := ent.FromContext(ctx)
|
||||
return client.Todo.
|
||||
Create().
|
||||
SetStatus(todo.Status).
|
||||
SetNillablePriority(todo.Priority).
|
||||
SetText(todo.Text).
|
||||
SetNillableParentID(todo.Parent).
|
||||
Save(ctx)
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
The [ent/contrib](https://github.com/ent/contrib) contains several examples at the moment:
|
||||
1. A complete GraphQL server with a simple [Todo App](https://github.com/ent/contrib/tree/master/entgql/internal/todo) with numeric ID field
|
||||
2. The same [Todo App](https://github.com/ent/contrib/tree/master/entgql/internal/todouuid) in 1, but with UUID type for the ID field
|
||||
3. The same [Todo App](https://github.com/ent/contrib/tree/master/entgql/internal/todopulid) in 1 and 2, but with a prefixed [ULID](https://github.com/ulid/spec) or `PULID` as the ID field. This example supports the Relay Node API by prefixing IDs with the entity type rather than employing the ID space partitioning in [Universal IDs](migrate.md#universal-ids).
|
||||
|
||||
---
|
||||
|
||||
Please note that this documentation is under development. All code parts reside in [ent/contrib/entgql](https://github.com/ent/contrib/tree/master/entgql),
|
||||
and an example of a todo-app can be found in [ent/contrib/entgql/todo](https://github.com/ent/contrib/tree/master/entgql/internal/todo).
|
||||
Reference in New Issue
Block a user