diff --git a/src/core/server/graph/tenant/schema/schema.graphql b/src/core/server/graph/tenant/schema/schema.graphql index 1f4e3957b..4d4090af1 100644 --- a/src/core/server/graph/tenant/schema/schema.graphql +++ b/src/core/server/graph/tenant/schema/schema.graphql @@ -233,8 +233,20 @@ type PageInfo { Indicates that there are more nodes after this subset. """ hasNextPage: Boolean + + """ + Included for legacy Relay reasons. Always set to false. + """ hasPreviousPage: Boolean + + """ + Included for legacy Relay reasons. Always set to null. + """ startCursor: Cursor + + """ + Specifies the last node's cursor for forwards pagination. + """ endCursor: Cursor } diff --git a/src/core/server/models/comment.ts b/src/core/server/models/comment.ts index ca20647a5..cd4741f3c 100644 --- a/src/core/server/models/comment.ts +++ b/src/core/server/models/comment.ts @@ -2,7 +2,15 @@ import { merge } from "lodash"; import { Db } from "mongodb"; import { Omit, Sub } from "talk-common/types"; import { ActionCounts } from "talk-server/models/actions"; -import { Connection, Cursor } from "talk-server/models/connection"; +import { + Connection, + Cursor, + Edge, + getPageInfo, + nodesToEdges, + NodeToCursorTransformer, + PageInfo, +} from "talk-server/models/connection"; import Query from "talk-server/models/query"; import { TenantResource } from "talk-server/models/tenant"; import uuid from "uuid"; @@ -134,31 +142,18 @@ export interface ConnectionInput { after?: Cursor; } -/** - * nodesToEdge converts a set of nodes and configuration options into a set of - * edges. - * - * @param input connection configuration - * @param nodes nodes returned from the query - */ -function nodesToEdge(input: ConnectionInput, nodes: Comment[]) { - let getCursor: (comment: Comment, index: number) => Cursor; +function cursorGetterFactory( + input: ConnectionInput +): NodeToCursorTransformer { switch (input.orderBy) { case CommentSort.CREATED_AT_DESC: case CommentSort.CREATED_AT_ASC: - getCursor = comment => comment.created_at; - break; + return comment => comment.created_at; case CommentSort.REPLIES_DESC: case CommentSort.RESPECT_DESC: - getCursor = (_, index) => + return (_, index) => (input.after ? (input.after as number) : 0) + index + 1; - break; } - - return nodes.map((comment, index) => ({ - node: comment, - cursor: getCursor(comment, index), - })); } /** @@ -223,8 +218,43 @@ export async function retrieveAssetConnection( async function retrieveConnection( input: ConnectionInput, query: Query -) { +): Promise>>> { // Apply some sorting options. + applyInputToQuery(input, query); + + // We load one more than the limit so we can determine if there is + // another page of entries. This gets trimmed off below after we've checked to + // see if this constitutes another page of edges. + query.first(input.first + 1); + + // Get the cursor. + const cursor = await query.exec(); + + // Get the comments from the cursor. + const nodes = await cursor.toArray(); + + // Convert the nodes to edges (which will include the extra edge we don't need + // if there is more results). + const edges = nodesToEdges(nodes, cursorGetterFactory(input)); + + // Get the pageInfo for the connection. We will use this to also determine if + // we need to trim off the extra edge that we requested by comparing its + // hasNextPage parameter. + const pageInfo = getPageInfo(input, edges); + if (pageInfo.hasNextPage) { + // Because this means that we got one more than expected, we should trim off + // the extra edge that was retrieved. + edges.splice(input.first, 1); + } + + // Return the connection. + return { + edges, + pageInfo, + }; +} + +function applyInputToQuery(input: ConnectionInput, query: Query) { switch (input.orderBy) { case CommentSort.CREATED_AT_DESC: query.orderBy({ created_at: -1 }); @@ -251,38 +281,4 @@ async function retrieveConnection( } break; } - - // We load one more than the limit so we can determine if there is - // another page of entries. - query.first(input.first + 1); - - // Get the cursor. - const cursor = await query.exec(); - - // Get the comments from the cursor. - const nodes = await cursor.toArray(); - - // The hasNextPage is always handled the same (ask for one more than we need, - // if there is one more, than there is more). - let hasNextPage = false; - if (input.first >= 0 && nodes.length > input.first) { - // There was one more than we expected! Set hasNextPage = true and remove - // the last item from the array that we requested. - hasNextPage = true; - nodes.splice(input.first, 1); - } - - // Convert the nodes to edges. - const edges = nodesToEdge(input, nodes); - - // Return the connection. - const connection: Readonly>> = { - edges, - pageInfo: { - hasNextPage, - endCursor: (nodes.length && nodes[nodes.length - 1].created_at) || null, - }, - }; - - return connection; } diff --git a/src/core/server/models/connection.ts b/src/core/server/models/connection.ts index cc69d1f2c..e45e9657a 100644 --- a/src/core/server/models/connection.ts +++ b/src/core/server/models/connection.ts @@ -6,11 +6,63 @@ export interface Edge { } export interface PageInfo { - hasNextPage: boolean; - endCursor: Cursor; + hasNextPage?: boolean; + hasPreviousPage?: boolean; + startCursor?: Cursor; + endCursor?: Cursor; } export interface Connection { edges: Array>; pageInfo: PageInfo; } + +export interface PaginationArgs { + first: number; +} + +export function getPageInfo(args: PaginationArgs, edges: Array>) { + const pageInfo: PageInfo = { + hasNextPage: false, + hasPreviousPage: false, + startCursor: null, + endCursor: null, + }; + + if (edges.length === 0) { + // If there are no edges, then there's nothing to paginate! + return pageInfo; + } + + // The hasNextPage is always handled the same (ask for one more than we need, + // if there is one more, than there is more). + if (args.first >= 0 && edges.length > args.first) { + // There was one more than we expected! Set hasNextPage = true. + pageInfo.hasNextPage = true; + } + + if (pageInfo.hasNextPage && edges.length > 1) { + // There was more than one expected, and there is two entries in the edges + // array. We should grab the second last one because the last one will have + // to be trimmed off after. + pageInfo.endCursor = edges[edges.length - 2].cursor; + } else { + // There was not more than expected, so we should just grab the last edge to + // get the endCursor. + pageInfo.endCursor = edges[edges.length - 1].cursor; + } + + return pageInfo; +} + +export type NodeToCursorTransformer = (node: T, index: number) => Cursor; + +export function nodesToEdges( + nodes: T[], + transformer: NodeToCursorTransformer +): Array> { + return nodes.map((node, index) => ({ + node, + cursor: transformer(node, index), + })); +}