GraphQL Integration
In order to enable the entgql extension to your project, you need to use the entc
(ent codegen) package as described . Follow these 3 steps to enable it to your project:
- Create a new Go file named
ent/entc.go
, and paste the following content:
ent/entc.go
- Edit the
ent/generate.go
file to execute theent/entc.go
file:
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.
- Run codegen for your ent project:
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.
In order to use the new generated ent.Noder
interface in the , add the Node
method to the query resolver, and look at the configuration section to understand how to use it.
If you are using the option in the schema migration, the NodeType is derived from the id value and can be used as follows:
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:
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
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:
Node:
model:
# ent.Noder is the new interface generated by the Node template.
- entgo.io/contrib/entgql/internal/todo/ent.Noder
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 .
Connection Ordering
The ordering option allows us to apply an ordering on the edges returned from a connection.
- The generated types will be
autobind
ed to GraphQL types if a naming convention is preserved (see example below). - Ordering can only be defined on ent fields (no edges).
- Ordering fields should normally be 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 .
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.
That’s all the schema changes required, make sure to run go generate
to apply them.
Next we need to define the ordering types in graphql schema:
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 directive can be used for manual type binding.
Adding orderBy argument to the pagination query
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:
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),
)
}
query {
todos(first: 3, orderBy: {direction: DESC, field: TEXT}) {
edges {
node {
text
}
}
}
}
Fields Collection
For example, given this GraphQL query:
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:
Usage and Configuration
The GraphQL extension generates also edge-resolvers for the nodes under the gql_edge.go
file as follows:
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 option to your GraphQL schema:
type Todo implements Node {
id: ID!
children: [Todo]! @goField(forceResolver: true)
}
Then, you can implement it on your type resolver.
func (r *todoResolver) Children(ctx context.Context, obj *ent.Todo) ([]*ent.Todo, error) {
// Do something here.
return obj.Edges.ChildrenOrErr()
}
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 . Hence, code that uses ent.Client
won’t need to be changed. In order to use it, follow these steps:
- In the GraphQL server initialization, use the
entgql.Transactioner
handler as follows:
srv := handler.NewDefaultServer(todo.NewSchema(client))
srv.Use(entgql.Transactioner{TxOpener: client})
- Then, in the GraphQL mutations, use the client from context as follows:
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 contains several examples at the moment:
- A complete GraphQL server with a simple Todo App with numeric ID field
- The same in 1, but with UUID type for the ID field
- The same Todo App in 1 and 2, but with a prefixed 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.