- Notifications
You must be signed in to change notification settings - Fork2
Relay-style pagination for NestJS GraphQL server.
License
equalogic/nestjs-graphql-connection
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
Relay-style pagination for NestJS GraphQL server.
npm i nestjs-graphql-connection
TypeScript type definitions are included in the box.
You must also install@nestjs/graphql as a peer dependency (you should have thisalready).
Extend a class fromcreateEdgeType function, passing it the class of objects to be represented by the edge'snode.
import{ObjectType}from'@nestjs/graphql';import{createEdgeType}from'nestjs-graphql-connection';import{Person}from'./entities';@ObjectType()exportclassPersonEdgeextendscreateEdgeType(Person){}
Extend a class fromcreateConnectionType function, passing it the class of objects to be represented by theconnection'sedges:
import{ObjectType}from'@nestjs/graphql';import{createConnectionType}from'nestjs-graphql-connection';@ObjectType()exportclassPersonConnectionextendscreateConnectionType(PersonEdge){}
Extend a class fromConnectionArgs class to have pagination arguments pre-defined for you. You can additionallydefine your own arguments for filtering, etc.
import{ArgsType,Field,ID}from'@nestjs/graphql';import{ConnectionArgs}from'nestjs-graphql-connection';@ArgsType()exportclassPersonConnectionArgsextendsConnectionArgs{/* * PersonConnectionArgs will inherit `first`, `last`, `before`, `after`, and `page` fields from ConnectionArgs */// EXAMPLE: Defining a custom argument for filtering @Field(type=>ID,{nullable:true})publicpersonId?:string;}
Now define aConnectionBuilder class for yourConnection object. The builder is responsible for interpretingpagination arguments for the connection, and creating the cursors andEdge objects that make up the connection.
import{ConnectionBuilder,Cursor,EdgeInputWithCursor,PageInfo,validateParamsUsingSchema,}from'nestjs-graphql-connection';exporttypePersonCursorParams={id:string};exporttypePersonCursor=Cursor<PersonCursorParams>;exportclassPersonConnectionBuilderextendsConnectionBuilder<PersonConnection,PersonConnectionArgs,PersonEdge,Person,PersonCursor>{publiccreateConnection(fields:{edges:PersonEdge[];pageInfo:PageInfo}):PersonConnection{returnnewPersonConnection(fields);}publiccreateEdge(fields:EdgeInputWithCursor<PersonEdge>):PersonEdge{returnnewPersonEdge(fields);}publiccreateCursor(node:Person):PersonCursor{returnnewCursor({id:node.id});}publicdecodeCursor(encodedString:string):PersonCursor{// A cursor sent to or received from a client is represented as a base64-encoded, URL-style query string containing// one or more key/value pairs describing the referenced node's position in the result set (its ID, a date, etc.)// Validation is optional, but recommended to enforce that cursor values supplied by clients must be well-formed.// This example uses Joi for validation, see documentation at https://joi.dev/api/?v=17#object// The following schema accepts only an object matching the type { id: string }:constschema:Joi.ObjectSchema<PersonCursorParams>=Joi.object({id:Joi.string().empty('').required(),}).unknown(false);returnCursor.fromString(encodedString,params=>validateParamsUsingSchema(params,schema));}}
Your resolvers can now return yourConnection as an object type. Use yourConnectionBuilder class to determine whichpage of results to fetch and to create thePageInfo, cursors, and edges in the result.
import{Args,Query,Resolver}from'@nestjs/graphql';@Resolver()exportclassPersonQueryResolver{ @Query(returns=>PersonConnection)publicasyncpersons(@Args()connectionArgs:PersonConnectionArgs):Promise<PersonConnection>{const{ personId}=connectionArgs;// Create builder instanceconstconnectionBuilder=newPersonConnectionBuilder(connectionArgs);// EXAMPLE: Count the total number of matching persons (without pagination)consttotalEdges=awaitcountPersons({where:{ personId}});// EXAMPLE: Do whatever you need to do to fetch the current page of personsconstpersons=awaitfetchPersons({where:{ personId},take:connectionBuilder.edgesPerPage,// how many rows to fetch});// Return resolved PersonConnection with edges and pageInforeturnconnectionBuilder.build({ totalEdges,nodes:persons,});}}
With offset pagination, cursor values are an encoded representation of the row offset. It is possible for clients topaginate by specifying either anafter argument with the cursor of the last row on the previous page, or to pass apage argument with an explicit page number (based on the rows per page set by thefirst argument). Offset paginatedconnections do not support thelast orbefore connection arguments, results must be fetched in forward order.
Offset pagination is useful when you want to be able to retrieve a page of edges at an arbitrary position in the resultset, without knowing anything about the intermediate entries. For example, to link to "page 10" without firstdetermining what the last result was on page 9.
To use offset cursors, extend your builder class fromOffsetPaginatedConnectionBuilder instead ofConnectionBuilder:
import{EdgeInputWithCursor,OffsetPaginatedConnectionBuilder,PageInfo,validateParamsUsingSchema,}from'nestjs-graphql-connection';exportclassPersonConnectionBuilderextendsOffsetPaginatedConnectionBuilder<PersonConnection,PersonConnectionArgs,PersonEdge,Person>{publiccreateConnection(fields:{edges:PersonEdge[];pageInfo:PageInfo}):PersonConnection{returnnewPersonConnection(fields);}publiccreateEdge(fields:EdgeInputWithCursor<PersonEdge>):PersonEdge{returnnewPersonEdge(fields);}// When extending from OffsetPaginatedConnectionBuilder, cursor encoding/decoding always uses the OffsetCursor type.// So it's not necessary to implement the createCursor() or decodeCursor() methods here.}
In your resolver, you can use thestartOffset property of the builder to determine the zero-indexed offset from whichto begin the result set. For example, this works with SQL databases that accept aSKIP orOFFSET parameter inqueries.
import{Args,Query,Resolver}from'@nestjs/graphql';@Resolver()exportclassPersonQueryResolver{ @Query(returns=>PersonConnection)publicasyncpersons(@Args()connectionArgs:PersonConnectionArgs):Promise<PersonConnection>{const{ personId}=connectionArgs;// Create builder instanceconstconnectionBuilder=newPersonConnectionBuilder(connectionArgs);// EXAMPLE: Count the total number of matching persons (without pagination)consttotalEdges=awaitcountPersons({where:{ personId}});// EXAMPLE: Do whatever you need to do to fetch the current page of personsconstpersons=awaitfetchPersons({where:{ personId},take:connectionBuilder.edgesPerPage,// how many rows to fetchskip:connectionBuilder.startOffset,// row offset to start at});// Return resolved PersonConnection with edges and pageInforeturnconnectionBuilder.build({ totalEdges,nodes:persons,});}}
The previous examples are sufficient for resolving connections that represent simple lists of objects with pagination.However, sometimes you need to model connections and edges that contain additional metadata. For example, you mightrelatePerson objects together into networks of friends using aPersonFriendConnection containingPersonFriendEdgeedges. In this case thenode on each edge is still aPerson object, but the relationship itself may haveproperties -- such as the date that the friend was added, or the type of relationship. (In relational database termsthis is analogous to having a Many-to-Many relation where the intermediate join table contains additional data columnsbeyond just the keys of the two joined tables.)
In this case your edge type would look like the following example. Notice that we also now define aPersonFriendEdgeInterface type which we pass as a generic argument tocreateEdgeType; this ensures correct typingsfor the fields that are allowed to be passed to your edge class's constructor for initialization when doingnew PersonFriendEdge({ ...fields }).
import{Field,GraphQLISODateTime,ObjectType}from'@nestjs/graphql';import{createEdgeType,EdgeInterface}from'nestjs-graphql-connection';import{Person}from'./entities';exportinterfacePersonFriendEdgeInterfaceextendsEdgeInterface<Person>{createdAt:Date;}@ObjectType()exportclassPersonFriendEdgeextendscreateEdgeType<PersonFriendEdgeInterface>(Person)implementsPersonFriendEdgeInterface{ @Field(type=>GraphQLISODateTime)publiccreatedAt:Date;}
To achieve this, you can pass an array of partialedges (instead ofnodes) tobuild(). This enables you toprovide values for any additional fields present on the edges.
The following example assumes you have a GraphQL schema that defines afriends field on yourPerson object, whichresolves to aPersonFriendConnection containing the person's friends. In your database you would have afriend tablethat relates aperson to anotherPerson, and that relationship has acreatedAt date.
import{Args,ResolveField,Resolver,Root}from'@nestjs/graphql';@Resolver(of=>Person)exportclassPersonResolver{ @ResolveField(returns=>PersonFriendConnection)publicasyncfriends( @Root()person:Person, @Args()connectionArgs:PersonFriendConnectionArgs,):Promise<PersonFriendConnection>{// Create builder instanceconstconnectionBuilder=newPersonFriendConnectionBuilder(connectionArgs);// EXAMPLE: Count the total number of this person's friends (without pagination)consttotalEdges=awaitcountFriends({where:{personId:person.id}});// EXAMPLE: Do whatever you need to do to fetch the current page of this person's friendsconstfriends=awaitfetchFriends({where:{personId:person.id},take:connectionBuilder.edgesPerPage,// how many rows to fetch});// Return resolved PersonFriendConnection with edges and pageInforeturnconnectionBuilder.build({ totalEdges,edges:friends.map(friend=>({node:friend.otherPerson,createdAt:friend.createdAt,})),});}}
Alternatively, you can override thecreateEdge() orcreateConnection() methods when callingbuild().
returnconnectionBuilder.build({ totalEdges,nodes:friends.map(friend=>friend.otherPerson),createConnection({ edges, pageInfo}){returnnewPersonFriendConnection({ edges, pageInfo,customField:'hello-world'});},createEdge:({ node, cursor})=>{constfriend=friends.find(friend=>friend.otherPerson.id===node.id);returnnewPersonFriendEdge({ node, cursor,createdAt:friend.createdAt});},});
Finally, if the above methods don't meet your needs you can always build the connection result yourself by replacingconnectionBuilder.build(...) with something like the following:
// Resolve edges with cursor, node, and additional metadataconstedges=friends.map((friend,index)=>newPersonFriendEdge({cursor:connectionBuilder.createCursor(friend.otherPerson,index),node:friend.otherPerson,createdAt:friend.createdAt,}),);// Return resolved PersonFriendConnectionreturnnewPersonFriendConnection({pageInfo:connectionBuilder.createPageInfo({ edges, totalEdges,hasNextPage:true,hasPreviousPage:false,}), edges,});
When using cursors for pagination of connections that allow the client to choose from different sorting options, you mayneed to customise your cursor to reflect the chosen sort order. For example, if the client can sortPersonConnectionby either name or creation date, the cursors you create on each edge will need to be different. It is no use knowing thecreation date of the last node if you are trying to fetch the next page of edges after the name "Smith", and vice versa.
Youcould set the node ID as the cursor in all cases and simply look up the relevant data (name or creation date) fromthe node when given such a cursor. However, if you have a dataset that could change between requests then this approachintroduces the potential for odd behavior and/or missing results.
Imagine you have asortOption field on yourPersonConnectionArgs that determines the requested sort order:
@ArgsType()exportclassPersonConnectionArgsextendsConnectionArgs{// In reality you might want an enum here, but we'll use a string for simplicity @Field(type=>String,{nullable:true})publicsortOption?:string;}
You can customise your cursor based on thesortOption from theConnectionArgs by changing your definition ofcreateCursor anddecodeCursor in your builder class like the following example:
exportclassPersonConnectionBuilderextendsConnectionBuilder<PersonConnection,PersonConnectionArgs,PersonEdge,Person,PersonCursor>{// ... (methods createConnection and createEdge remain unchanged)publiccreateCursor(node:Person):PersonCursor{if(this.connectionArgs.sortOption==='name'){returnnewCursor({name:node.name});}returnnewCursor({createdAt:node.createdAt.toISOString()});}publicdecodeCursor(encodedString:string):PersonCursor{if(this.connectionArgs.sortOption==='name'){returnCursor.fromString(encodedString,params=>validateParamsUsingSchema(params,Joi.object({name:Joi.string().empty('').required(),}).unknown(false),),);}returnCursor.fromString(encodedString,params=>validateParamsUsingSchema(params,Joi.object({id:Joi.string().empty('').required(),}).unknown(false),),);}}
Alternatively,ConnectionBuilder supports overriding thecreateCursor() method when callingbuild(). So you couldalso do it like this:
import{Args,ResolveField,Resolver,Root}from'@nestjs/graphql';@Resolver()exportclassPersonQueryResolver{ @Query(returns=>PersonConnection)publicasyncpersons(@Args()connectionArgs:PersonConnectionArgs):Promise<PersonConnection>{const{ sortOption}=connectionArgs;// Create builder instanceconstconnectionBuilder=newPersonConnectionBuilder(connectionArgs);// EXAMPLE: Do whatever you need to do to fetch the current page of persons using the specified sort orderconstpersons=awaitfetchPersons({where:{ personId},order:sortOption==='name' ?{name:'ASC'} :{createdAt:'ASC'},take:connectionBuilder.edgesPerPage,// how many rows to fetch});// Return resolved PersonConnection with edges and pageInforeturnconnectionBuilder.build({totalEdges:awaitcountPersons(),nodes:persons,createCursor(node){returnnewCursor(sortOption==='name' ?{name:node.name} :{createdAt:node.createdAt.toISOString()});},});}}
About
Relay-style pagination for NestJS GraphQL server.
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Uh oh!
There was an error while loading.Please reload this page.
Contributors5
Uh oh!
There was an error while loading.Please reload this page.
