Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Relay-style pagination for NestJS GraphQL server.

License

NotificationsYou must be signed in to change notification settings

equalogic/nestjs-graphql-connection

Repository files navigation





Relay-style pagination for NestJS GraphQL server.

Installation

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).

Usage

Create an Edge type

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){}

Create a Connection type

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){}

Create a Connection Arguments type

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;}

Create a Connection Builder

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));}}

Resolve a Connection

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,});}}

Using Offset Pagination

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,});}}

Advanced Topics

Enriching Edges with additional metadata

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,});

Customising Cursors

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()});},});}}

License

MIT

About

Relay-style pagination for NestJS GraphQL server.

Topics

Resources

License

Stars

Watchers

Forks

Contributors5


[8]ページ先頭

©2009-2025 Movatter.jp