Filter Inputs
In this section, we continue the GraphQL example by explaining how to generate type-safe GraphQL filters (i.e. Where
predicates) from our ent/schema
, and allow users to seamlessly map GraphQL queries to Ent queries. For example, the following GraphQL query, maps to the Ent query below:
GraphQL
{
hasParent: true,
hasChildrenWith: {
status: IN_PROGRESS,
}
}
Ent
client.Todo.
Query().
Where(
todo.HasParent(),
todo.HasChildrenWith(
todo.StatusEQ(todo.StatusInProgress),
),
).
All(ctx)
Clone the code (optional)
The code for this tutorial is available under 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 and run the program as follows:
git clone git@github.com:a8m/ent-graphql-example.git
cd ent-graphql-example
go run ./cmd/todo/
Configure Ent
Go to your ent/entc.go
file, and add the 4 highlighted lines (extension options):
ent/entc.go
func main() {
ex, err := entgql.NewExtension(
entgql.WithSchemaGenerator(),
entgql.WithWhereInputs(true),
entgql.WithConfigPath("gqlgen.yml"),
entgql.WithSchemaPath("ent.graphql"),
)
if err != nil {
log.Fatalf("creating entgql extension: %v", err)
}
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)
}
}
The WithWhereInputs
option enables the filter generation, the WithConfigPath
configures the path to the gqlgen
config file, which allows the extension to more accurately map GraphQL to Ent types. The last option WithSchemaPath
, configures a path to a new, or an existing GraphQL schema to write the generated filters to.
After changing the entc.go
configuration, we’re ready to execute the code generation as follows:
go generate .
Observe that Ent has generated <T>WhereInput
for each type in your schema in a file named ent/gql_where_input.go
. Ent also generates a GraphQL schema as well (ent.graphql
), so you don’t need to autobind
them to gqlgen
manually. For example:
ent/gql_where_input.go
// TodoWhereInput represents a where input for filtering Todo queries.
type TodoWhereInput struct {
Not *TodoWhereInput `json:"not,omitempty"`
Or []*TodoWhereInput `json:"or,omitempty"`
And []*TodoWhereInput `json:"and,omitempty"`
// "created_at" field predicates.
CreatedAt *time.Time `json:"createdAt,omitempty"`
CreatedAtNEQ *time.Time `json:"createdAtNEQ,omitempty"`
CreatedAtIn []time.Time `json:"createdAtIn,omitempty"`
CreatedAtNotIn []time.Time `json:"createdAtNotIn,omitempty"`
CreatedAtGT *time.Time `json:"createdAtGT,omitempty"`
CreatedAtGTE *time.Time `json:"createdAtGTE,omitempty"`
CreatedAtLT *time.Time `json:"createdAtLT,omitempty"`
CreatedAtLTE *time.Time `json:"createdAtLTE,omitempty"`
// "status" field predicates.
Status *todo.Status `json:"status,omitempty"`
StatusNEQ *todo.Status `json:"statusNEQ,omitempty"`
StatusIn []todo.Status `json:"statusIn,omitempty"`
StatusNotIn []todo.Status `json:"statusNotIn,omitempty"`
// .. truncated ..
}
ent.graphql
"""
TodoWhereInput is used for filtering Todo objects.
Input was generated by ent.
"""
input TodoWhereInput {
not: TodoWhereInput
and: [TodoWhereInput!]
or: [TodoWhereInput!]
"""created_at field predicates"""
createdAt: Time
createdAtNEQ: Time
createdAtIn: [Time!]
createdAtNotIn: [Time!]
createdAtGT: Time
createdAtGTE: Time
createdAtLT: Time
createdAtLTE: Time
"""status field predicates"""
status: Status
statusNEQ: Status
statusIn: [Status!]
statusNotIn: [Status!]
# .. truncated ..
}
info
If your project contains more than 1 GraphQL schema (e.g. todo.graphql
and ent.graphql
), you should configure gqlgen.yml
file as follows:
schema:
- todo.graphql
# The ent.graphql schema was generated by Ent.
- ent.graphql
Configure GQL
After running the code generation, we’re ready to complete the integration and expose the filtering capabilities in GraphQL:
- Edit the GraphQL schema to accept the new filter types:
ent.graphql
type Query {
todos(
after: Cursor,
first: Int,
before: Cursor,
last: Int,
orderBy: TodoOrder,
where: TodoWhereInput,
): TodoConnection!
}
- Use the new filter types in GraphQL resolvers:
ent.resolvers.go
func (r *queryResolver) Todos(ctx context.Context, after *ent.Cursor, first *int, before *ent.Cursor, last *int, orderBy *ent.TodoOrder, where *ent.TodoWhereInput) (*ent.TodoConnection, error) {
return r.client.Todo.Query().
Paginate(ctx, after, first, before, last,
ent.WithTodoOrder(orderBy),
ent.WithTodoFilter(where.Filter),
)
}
Execute Queries
As mentioned above, with the new GraphQL filter types, you can express the same Ent filters you use in your Go code.
Conjunction, disjunction and negation
The Not
, And
and Or
operators can be added to the where
clause using the not
, and
and or
fields. For example:
query {
todos(
where: {
or: [
{
status: COMPLETED
},
{
not: {
hasParent: true,
status: IN_PROGRESS
}
}
]
}
) {
edges {
node {
id
text
}
cursor
}
}
}
When multiple filter fields are provided, Ent implicitly adds the And
operator.
{
status: COMPLETED,
textHasPrefix: "GraphQL",
}
The above query will produce the following Ent query:
client.Todo.
Query().
Where(
todo.And(
todo.StatusEQ(todo.StatusCompleted),
todo.TextHasPrefix("GraphQL"),
)
).
All(ctx)
Edge/Relation filters
Edge (relation) predicates can be expressed in the same Ent syntax:
{
hasParent: true,
hasChildrenWith: {
status: IN_PROGRESS,
}
}
The above query will produce the following Ent query:
client.Todo.
Query().
Where(
todo.HasParent(),
todo.HasChildrenWith(
todo.StatusEQ(todo.StatusInProgress),
),
).
All(ctx)
Custom filters
Sometimes we need to add custom conditions to our filters, while it is always possible to use Templates and SchemaHooks it’s not always the easiest solution, specially if we only want to add simple conditions.
Luckily by using a combination of the GraphQL object type extensions and custom resolvers, we can achieve this functionality.
Let’s see an example of adding a custom isCompleted
filter that will receive a boolean value and filter all the TODO’s that have the completed
status.
Let’s start by extending the TodoWhereInput
:
todo.graphql
extend input TodoWhereInput {
isCompleted: Boolean
}
After running the code generation, we should see a new field resolver inside the todo.resolvers.go
file:
todo.resolvers.go
func (r *todoWhereInputResolver) IsCompleted(ctx context.Context, obj *ent.TodoWhereInput, data *bool) error {
panic(fmt.Errorf("not implemented"))
}
We can now use the AddPredicates
method inside the ent.TodoWhereInput
struct to implement our custom filtering:
todo.resolvers.go
func (r *todoWhereInputResolver) IsCompleted(ctx context.Context, obj *ent.TodoWhereInput, data *bool) error {
if obj == nil || data == nil {
return nil
}
if *data {
obj.AddPredicates(todo.StatusEQ(todo.StatusCompleted))
} else {
obj.AddPredicates(todo.StatusNEQ(todo.StatusCompleted))
}
return nil
}
We can use this new filtering as any other predicate:
{
isCompleted: true,
}
# including the not, and and or fields
{
not: {
isCompleted: true,
}
}
Usage as predicates
The Filter
option lets use the generated WhereInput
s as regular predicates on any type of query:
query := ent.Todo.Query()
query, err := input.Filter(query)
if err != nil {
return nil, err
}
return query.All(ctx)
Well done! As you can see, by changing a few lines of code our application now exposes a type-safe GraphQL filters that automatically map to Ent queries. Have questions? Need help with getting started? Feel free to join our Discord server or Slack channel.