Paging, Sorting, and Filtering
Table of Contents
Now that we have ent
and gqlgen
wired up, we can look at updating our list notes resolver. In this section, we will
look at implementing paging, sorting, filtering, and where clauses to provide a more robust experience to our client
applications.
This section is going to be using references from the Relay Cursor Connection Spec which covers what the specifications should look like when using this kind of pattern in GraphQL.
First let’s look at paging. This is going to allow us to return smaller datasets to the client applications as well as giving the ability to use a “Cursor” to navigate to the next page of the dataset until the end.
First, let’s update our notes
query to take the parameters we need to provide this functionality to the client. We
also want to update the response object to be a new NoteConnection
struct which will contain both our data and
information on paging:
type NoteConnection {
edges: [NoteEdge]
pageInfo: PageInfo!
totalCount: Int!
}
type NoteEdge {
node: Note
cursor: Cursor!
}
input NoteInput {
title: String!
body: String!
}
extend type Query {
notes(after: Cursor, first: Int, before: Cursor, last: Int): NoteConnection!
notes: [Note!]!
}
With this updated, you may notice that we are using a couple of types that haven’t been defined yet. Let’s define those
types in the common.graphql
schema file now:
scalar Cursor
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: Cursor
endCursor: Cursor
}
You can put these anywhere in the schema file. However, I generally group my scalar
s together after any @directive
s
(we don’t currently have any in this project) and before any interface
s or type
s. Also, the reason I’m deciding to
put these in the common schema instead of the note schema is these types are generic and can be used by any schema.
With the schema files updated, we can regenerate our code:
go generate ./...
If you get a panic: interface conversion: types.Type is *types.Alias, not *types.Named
back from running generate
,
try updating the generate command for gqlgen
to:
//go:generate go run github.com/99designs/gqlgen@latest generate
//go:generate go run github.com/99designs/gqlgen generate
This time you will probably get an error within the gql/note.go
file when running go generate
because
there is a compilation error. We can ignore this error since it’s directly related to the change in response of our
notes
resolver.
Let’s update the resolver to fix the errors:
// Notes is the resolver for the notes field.
func (r *queryResolver) Notes(ctx context.Context, after *entgql.Cursor[int], first *int, before *entgql.Cursor[int], last *int) (*ent.NoteConnection, error) {
return r.ent.Note.Query().Paginate(ctx, after, first, before, last)
}
Because of the specific naming and structure of the magic type
s we defined in our schema, we don’t need to translate
anything from ent
when we return. The Paginate()
method of Query()
will automatically return the data structures
we need to satisfy the NoteConnection
struct defined within the schema. That’s all we needed to do to get this working.
We can now test getting records back in the list using the GraphiQL interface:
query {
notes(first: 10) {
edges {
node {
id
title
}
}
pageInfo {
hasPreviousPage
startCursor
hasNextPage
endCursor
}
}
}
With this we should get back:
{
"data": {
"notes": {
"edges": [
{
"node": {
"id": 1,
"title": "Example"
}
}
],
"pageInfo": {
"hasPreviousPage": false,
"startCursor": "gaFpAQ",
"hasNextPage": false,
"endCursor": "gaFpAQ"
}
}
}
}
If we have more records we can use the after
parameter with a cursor
to go to records beyond that point. This is a
more robust way of handling paging than the traditional page
and size
as it will also take into account things like
the sorting order, where clauses, and not skip records or repeat results when new items are added.
For ordering, we need to update the code generation that happens within ent
by defining what fields we want to be used
for ordering and what name (in GraphQL) they go by. We will update our ent/schema/note.go
file to include these
annotations for the Title
, CreatedAt
, and the UpdatedAt
fields of our model:
// Fields of the Note.
func (Note) Fields() []ent.Field {
return []ent.Field{
field.String("title").
Annotations(
entgql.OrderField("TITLE"),
),
field.Text("body"),
field.Time("createdAt").
Default(time.Now).
Annotations(
entgql.OrderField("CREATED_AT"),
),
field.Time("updatedAt").
Default(time.Now).
UpdateDefault(time.Now).
Annotations(
entgql.OrderField("UPDATED_AT"),
),
}
}
Notice that we define the OrderField()
property with SCREAMING_SNAKE_CASE
as these are the values that we will be
using within our enum
in the GraphQL schema. So let’s update the note schema to utilize these:
input NoteOrder {
direction: OrderDirection!
field: NoteOrderField
}
enum NoteOrderField {
TITLE
CREATED_AT
UPDATED_AT
}
extend type Query {
notes(
after: Cursor
first: Int
before: Cursor
last: Int
orderBy: NoteOrder
): NoteConnection!
}
If you notice, once again, we have introduced a new type but not defined that type (OrderDirection
). We will define
this type in the common schema as it’s just a simple and generic Enum:
enum OrderDirection {
ASC
DESC
}
Now that everything is in place, we can generate our code:
go generate ./...
With our new ent
options added and our GraphQL schema files updated to allow for ordering, we can update the resolver
to utilize the order field:
// Notes is the resolver for the notes field.
func (r *queryResolver) Notes(ctx context.Context, after *entgql.Cursor[int], first *int, before *entgql.Cursor[int], last *int, orderBy *ent.NoteOrder) (*ent.NoteConnection, error) {
return r.ent.Note.Query().Paginate(ctx, after, first, before, last, ent.WithNoteOrder(orderBy))
}
For filtering, we need to update the ent
extension responsible for GraphQL generation. The extension needs to be
configured to generate WhereInput
s:
func main() {
ex, err := entgql.NewExtension(entgql.WithWhereInputs(true))
if err != nil {
log.Fatalf("creating entgql extension: %s", err)
}
if err = entc.Generate("./schema", &gen.Config{}, entc.Extensions(ex)); err != nil {
log.Fatalf("running ent codegen: %s", err)
}
}
With that, we can now add the WhereInput
to our schema file. The WhereInput
defines where conditions that can be
used to filter the content using complex queries:
input NoteWhereInput {
not: NoteWhereInput
and: [NoteWhereInput!]
or: [NoteWhereInput!]
title: String
titleNEQ: String
titleIn: [String!]
titleNotIn: [String!]
titleGT: String
titleGTE: String
titleLT: String
titleLTE: String
titleContains: String
titleHasPrefix: String
titleHasSuffix: String
titleEqualFold: String
titleContainsFold: String
createdAt: Time
createdAtNEQ: Time
createdAtIn: [Time!]
createdAtNotIn: [Time!]
createdAtGT: Time
createdAtGTE: Time
createdAtLT: Time
createdAtLTE: Time
updatedAt: Time
updatedAtNEQ: Time
updatedAtIn: [Time!]
updatedAtNotIn: [Time!]
updatedAtGT: Time
updatedAtGTE: Time
updatedAtLT: Time
updatedAtLTE: Time
}
extend type Query {
notes(
after: Cursor
first: Int
before: Cursor
last: Int
orderBy: NoteOrder
where: NoteWhereInput
): NoteConnection!
}
Notice that within the WhereInput
we have not
, and
, as well as or
conditions also of the NoteWhereInput
type.
Doing this allows us to create nested where conditions for notes. Later, we can expand this to also traverse where
conditions based on other ent models as well.
With our schema updated, regenerate our code again:
go generate ./...
Finally, we can update our resolver once again to add a where filtering clause to the Paginate()
:
// Notes is the resolver for the notes field.
func (r *queryResolver) Notes(ctx context.Context, after *entgql.Cursor[int], first *int, before *entgql.Cursor[int], last *int, orderBy *ent.NoteOrder, where *ent.NoteWhereInput) (*ent.NoteConnection, error) {
return r.ent.Note.Query().
Paginate(
ctx,
after,
first,
before,
last,
ent.WithNoteOrder(orderBy),
ent.WithNoteFilter(where.Filter),
)
}
With that you should now be able to filter data by conditions that are listed within the NoteWhereInput
struct in our
GraphQL schema. Go ahead and test that you can properly filter data.