Aller au contenu

Paging

type-graphql-query supports multiple paging strategies that each have their own pros and cons. This documentation will cover the different paging strategies and their applicable use cases.

The following examples are based on the following ProductDTO:

import { EnhancedField, IDField } from '@lib/type-graphql-query';
import { ObjectType, ID, GraphQLISODateTime, Field } from '@lib/type-graphql';
@ObjectType('Product')
export class ProductDTO {
@IDField(() => ID)
id!: string;
@EnhancedField()
name!: string;
@EnhancedField()
price!: number;
@Field(() => GraphQLISODateTime)
created!: Date;
@Field(() => GraphQLISODateTime)
updated!: Date;
}

By default type-graphql-query will expose all query many endpoints as cursor based connections that you can use to page through results.

All cursor connections, regardless of paging strategy, expose the following schema:

scalar ConnectionCursor
type PageInfo {
hasNextPage: Boolean
hasPreviousPage: Boolean
startCursor: ConnectionCursor
endCursor: ConnectionCursor
}
type ProductConnection {
pageInfo: PageInfo!
edges: [ProductEdge!]!
}
type ProductEdge {
node: Product!
cursor: ConnectionCursor!
}

By default all cursors will use a form of offset based paging to back cursors.

Pros Cons
Allow for recursive relation paging Pages and nodes may change between queries
Straight forward to implement Inconsistent Sorting - natural sorting is used by default
Queries grow more inefficient as you go through more pages

Example:

{
products(paging: { first: 2 }) {
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
edges {
node {
id
name
price
created
updated
}
cursor
}
}
}

Response:

{
"data": {
"products": {
"pageInfo": {
"hasNextPage": true,
"hasPreviousPage": false,
"startCursor": "YXJyYXljb25uZWN0aW9uOjA=",
"endCursor": "YXJyYXljb25uZWN0aW9uOjE="
},
"edges": [
{
"node": {
"id": "1",
"name": "Premium Eggs",
"price": 15.99,
"created": "2021-02-14T12:56:01.106Z",
"updated": "2021-02-14T12:56:01.106Z"
},
"cursor": "YXJyYXljb25uZWN0aW9uOjA="
},
{
"node": {
"id": "2",
"name": "Organic Eggs",
"price": 18.99,
"created": "2021-02-14T12:56:01.106Z",
"updated": "2021-02-14T12:56:01.106Z"
},
"cursor": "YXJyYXljb25uZWN0aW9uOjE="
}
]
}
}
}

You have the option to specify a key set on your DTO which will replace the offset with a where clause. A keyset is a set of fields that uniquely identify a record (e.g. id).

Pros Cons
Consistent Results - All cursors uniquely identify a record allowing for deterministic before/after pages Unable to do recursive paging for relations
Stable Sorting - All results will be deterministically sorted A unique key set must be specified
Consistent performance with correct indexes in place Unable to jump to arbitrary pages

To use key set based cursors, annotate each sort field with @KeySetField. Keyset mode is enabled automatically when at least one @KeySetField is present on the DTO.

import {
EnhancedField,
IDField,
QueryOptions,
KeySetField,
PagingStrategies,
} from '@lib/type-graphql-query';
import { ObjectType, ID } from '@lib/type-graphql';
@ObjectType('Product')
@QueryOptions({ pagingStrategy: PagingStrategies.CURSOR })
export class ProductDTO {
@IDField(() => ID)
@KeySetField({ order: 1 })
id!: string;
@EnhancedField()
name!: string;
// ... other fields
}

When using offset based paging, the paging argument accepts limit and offset values, and the result will be an OffsetConnection with nodes instead of edges.

Example:

{
products(paging: { limit: 2 }) {
pageInfo {
hasNextPage
hasPreviousPage
}
nodes {
id
name
price
created
updated
}
}
}

Response:

{
"data": {
"products": {
"pageInfo": {
"hasNextPage": true,
"hasPreviousPage": false
},
"nodes": [
{
"id": "1",
"name": "Premium Eggs",
"price": 15.99,
"created": "2021-02-14T12:56:01.106Z",
"updated": "2021-02-14T12:56:01.106Z"
},
{
"id": "2",
"name": "Organic Eggs",
"price": 18.99,
"created": "2021-02-14T12:56:01.106Z",
"updated": "2021-02-14T12:56:01.106Z"
}
]
}
}
}

In this example we’ll also pass in an offset to fetch the next 2 records:

GraphQL:

{
products(paging: { limit: 2, offset: 2 }) {
pageInfo {
hasNextPage
hasPreviousPage
}
nodes {
id
name
price
created
updated
}
}
}

Response:

{
"data": {
"products": {
"pageInfo": {
"hasNextPage": true,
"hasPreviousPage": true
},
"nodes": [
{
"id": "3",
"name": "Free Range Eggs",
"price": 20.99,
"created": "2021-02-14T12:56:01.106Z",
"updated": "2021-02-14T12:56:01.106Z"
},
{
"id": "4",
"name": "Pasture Raised Eggs",
"price": 22.99,
"created": "2021-02-14T12:56:01.106Z",
"updated": "2021-02-14T12:56:01.106Z"
}
]
}
}
}

When using the NONE paging strategy, the paging argument is removed and all methods will return an ArrayConnection.

Pros Cons
All data is returned at once Not suitable for large datasets

To disable paging, you can set the pagingStrategy option using the @QueryOptions decorator to PagingStrategies.NONE:

import { EnhancedField, IDField, QueryOptions, PagingStrategies } from '@lib/type-graphql-query';
import { ObjectType, ID, GraphQLISODateTime, Field } from '@lib/type-graphql';
@ObjectType('Product')
@QueryOptions({ pagingStrategy: PagingStrategies.NONE })
export class ProductDTO {
@IDField(() => ID)
id!: string;
@EnhancedField()
name!: string;
@EnhancedField()
price!: number;
@Field(() => GraphQLISODateTime)
created!: Date;
@Field(() => GraphQLISODateTime)
updated!: Date;
}

When using this strategy, queries that return multiple records will return an array instead of a connection:

GraphQL:

{
products {
id
name
price
created
updated
}
}

Response:

{
"data": {
"products": [
{
"id": "1",
"name": "Premium Eggs",
"price": 15.99,
"created": "2020-06-12T08:15:18.876Z",
"updated": "2020-06-12T08:15:18.876Z"
},
{
"id": "2",
"name": "Organic Eggs",
"price": 18.99,
"created": "2020-06-12T08:15:18.876Z",
"updated": "2020-06-12T08:15:18.876Z"
},
{
"id": "3",
"name": "Free Range Eggs",
"price": 20.99,
"created": "2020-06-12T08:15:18.876Z",
"updated": "2020-06-12T08:15:18.876Z"
}
]
}
}