From 3a9195cb333ec416b2c796e3c4a5b391b9289a0a Mon Sep 17 00:00:00 2001 From: Thomas Leonard Date: Mon, 5 Sep 2022 22:15:42 +0200 Subject: [PATCH] fix: align previous/next page flag with relay standard --- .../__tests__/arrayConnection-test.ts | 441 ++++++++++++++++-- src/connection/arrayConnection.ts | 34 +- 2 files changed, 433 insertions(+), 42 deletions(-) diff --git a/src/connection/__tests__/arrayConnection-test.ts b/src/connection/__tests__/arrayConnection-test.ts index 2a73c13..9bf172f 100644 --- a/src/connection/__tests__/arrayConnection-test.ts +++ b/src/connection/__tests__/arrayConnection-test.ts @@ -93,6 +93,170 @@ describe('connectionFromArray()', () => { }); describe('pagination', () => { + it('respects first', () => { + let c = connectionFromArray(arrayABCDE, { + first: 2, + }); + expect(c).to.deep.equal({ + edges: [edgeA, edgeB], + pageInfo: { + startCursor: cursorA, + endCursor: cursorB, + hasPreviousPage: false, + hasNextPage: true, + }, + }); + + c = connectionFromArray(arrayABCDE, { + first: 0, + }); + expect(c).to.deep.equal({ + edges: [], + pageInfo: { + startCursor: null, + endCursor: null, + hasPreviousPage: false, + hasNextPage: true, + }, + }); + + c = connectionFromArray(arrayABCDE, { + first: 5, + }); + expect(c).to.deep.equal({ + edges: [edgeA, edgeB, edgeC, edgeD, edgeE], + pageInfo: { + startCursor: cursorA, + endCursor: cursorE, + hasPreviousPage: false, + hasNextPage: false, + }, + }); + + c = connectionFromArray(arrayABCDE, { + first: 100, + }); + expect(c).to.deep.equal({ + edges: [edgeA, edgeB, edgeC, edgeD, edgeE], + pageInfo: { + startCursor: cursorA, + endCursor: cursorE, + hasPreviousPage: false, + hasNextPage: false, + }, + }); + }); + + it('respects last', () => { + let c = connectionFromArray(arrayABCDE, { + last: 2, + }); + expect(c).to.deep.equal({ + edges: [edgeD, edgeE], + pageInfo: { + startCursor: cursorD, + endCursor: cursorE, + hasPreviousPage: true, + hasNextPage: false, + }, + }); + + c = connectionFromArray(arrayABCDE, { + last: 0, + }); + expect(c).to.deep.equal({ + edges: [], + pageInfo: { + startCursor: null, + endCursor: null, + hasPreviousPage: true, + hasNextPage: false, + }, + }); + + c = connectionFromArray(arrayABCDE, { + last: 5, + }); + expect(c).to.deep.equal({ + edges: [edgeA, edgeB, edgeC, edgeD, edgeE], + pageInfo: { + startCursor: cursorA, + endCursor: cursorE, + hasPreviousPage: false, + hasNextPage: false, + }, + }); + + c = connectionFromArray(arrayABCDE, { + last: 100, + }); + expect(c).to.deep.equal({ + edges: [edgeA, edgeB, edgeC, edgeD, edgeE], + pageInfo: { + startCursor: cursorA, + endCursor: cursorE, + hasPreviousPage: false, + hasNextPage: false, + }, + }); + }); + + it('respects after', () => { + let c = connectionFromArray(arrayABCDE, { + after: cursorB, + }); + expect(c).to.deep.equal({ + edges: [edgeC, edgeD, edgeE], + pageInfo: { + startCursor: cursorC, + endCursor: cursorE, + hasPreviousPage: true, + hasNextPage: false, + }, + }); + + c = connectionFromArray(arrayABCDE, { + after: cursorE, + }); + expect(c).to.deep.equal({ + edges: [], + pageInfo: { + startCursor: null, + endCursor: null, + hasPreviousPage: true, + hasNextPage: false, + }, + }); + }); + + it('respects before', () => { + let c = connectionFromArray(arrayABCDE, { + before: cursorC, + }); + expect(c).to.deep.equal({ + edges: [edgeA, edgeB], + pageInfo: { + startCursor: cursorA, + endCursor: cursorB, + hasPreviousPage: false, + hasNextPage: true, + }, + }); + + c = connectionFromArray(arrayABCDE, { + before: cursorA, + }); + expect(c).to.deep.equal({ + edges: [], + pageInfo: { + startCursor: null, + endCursor: null, + hasPreviousPage: false, + hasNextPage: true, + }, + }); + }); + it('respects first and after', () => { const c = connectionFromArray(arrayABCDE, { first: 2, @@ -103,7 +267,7 @@ describe('connectionFromArray()', () => { pageInfo: { startCursor: cursorC, endCursor: cursorD, - hasPreviousPage: false, + hasPreviousPage: true, hasNextPage: true, }, }); @@ -119,7 +283,7 @@ describe('connectionFromArray()', () => { pageInfo: { startCursor: cursorC, endCursor: cursorE, - hasPreviousPage: false, + hasPreviousPage: true, hasNextPage: false, }, }); @@ -136,7 +300,7 @@ describe('connectionFromArray()', () => { startCursor: cursorB, endCursor: cursorC, hasPreviousPage: true, - hasNextPage: false, + hasNextPage: true, }, }); }); @@ -152,7 +316,7 @@ describe('connectionFromArray()', () => { startCursor: cursorA, endCursor: cursorC, hasPreviousPage: false, - hasNextPage: false, + hasNextPage: true, }, }); }); @@ -168,13 +332,16 @@ describe('connectionFromArray()', () => { pageInfo: { startCursor: cursorB, endCursor: cursorC, - hasPreviousPage: false, + hasPreviousPage: true, hasNextPage: true, }, }); }); it('respects first and after and before, too many', () => { + // `hasNextPage=false` because the spec says: + // "If first is set: [...] If edges contains more than first elements return true, otherwise false." + // and the array after cursor have been applied contains 3 elements <= first (4) const c = connectionFromArray(arrayABCDE, { first: 4, after: cursorA, @@ -185,13 +352,16 @@ describe('connectionFromArray()', () => { pageInfo: { startCursor: cursorB, endCursor: cursorD, - hasPreviousPage: false, + hasPreviousPage: true, hasNextPage: false, }, }); }); it('respects first and after and before, exactly right', () => { + // `hasNextPage=false` because the spec says: + // "If first is set: [...] If edges contains more than first elements return true, otherwise false." + // and the array after cursor have been applied contains 3 elements <= first (3) const c = connectionFromArray(arrayABCDE, { first: 3, after: cursorA, @@ -202,7 +372,7 @@ describe('connectionFromArray()', () => { pageInfo: { startCursor: cursorB, endCursor: cursorD, - hasPreviousPage: false, + hasPreviousPage: true, hasNextPage: false, }, }); @@ -220,12 +390,15 @@ describe('connectionFromArray()', () => { startCursor: cursorC, endCursor: cursorD, hasPreviousPage: true, - hasNextPage: false, + hasNextPage: true, }, }); }); it('respects last and after and before, too many', () => { + // `hasPreviousPage=false` because the spec says: + // "If last is set: [...] If edges contains more than last elements return true, otherwise false." + // and the array after cursor have been applied contains 3 elements <= last (4) const c = connectionFromArray(arrayABCDE, { last: 4, after: cursorA, @@ -237,12 +410,15 @@ describe('connectionFromArray()', () => { startCursor: cursorB, endCursor: cursorD, hasPreviousPage: false, - hasNextPage: false, + hasNextPage: true, }, }); }); it('respects last and after and before, exactly right', () => { + // `hasPreviousPage=false` because the spec says: + // "If last is set: [...] If edges contains more than last elements return true, otherwise false." + // and the array after cursor have been applied contains 3 elements <= last (3) const c = connectionFromArray(arrayABCDE, { last: 3, after: cursorA, @@ -254,6 +430,169 @@ describe('connectionFromArray()', () => { startCursor: cursorB, endCursor: cursorD, hasPreviousPage: false, + hasNextPage: true, + }, + }); + }); + + it('respects first and last', () => { + let c = connectionFromArray(arrayABCDE, { + first: 2, + last: 1, + }); + expect(c).to.deep.equal({ + edges: [edgeB], + pageInfo: { + startCursor: cursorB, + endCursor: cursorB, + hasPreviousPage: true, + hasNextPage: true, + }, + }); + + // `hasPreviousPage=true` might seem weird but this is what the spec says: + // "If last is set: [...] If edges contains more than last elements return true, otherwise false." + // and the array contains 5 elements > last (2) + c = connectionFromArray(arrayABCDE, { + first: 2, + last: 2, + }); + expect(c).to.deep.equal({ + edges: [edgeA, edgeB], + pageInfo: { + startCursor: cursorA, + endCursor: cursorB, + hasPreviousPage: true, + hasNextPage: true, + }, + }); + + c = connectionFromArray(arrayABCDE, { + first: 2, + last: 10, + }); + expect(c).to.deep.equal({ + edges: [edgeA, edgeB], + pageInfo: { + startCursor: cursorA, + endCursor: cursorB, + hasPreviousPage: false, + hasNextPage: true, + }, + }); + + c = connectionFromArray(arrayABCDE, { + first: 100, + last: 2, + }); + expect(c).to.deep.equal({ + edges: [edgeD, edgeE], + pageInfo: { + startCursor: cursorD, + endCursor: cursorE, + hasPreviousPage: true, + hasNextPage: false, + }, + }); + + c = connectionFromArray(arrayABCDE, { + first: 100, + last: 10, + }); + expect(c).to.deep.equal({ + edges: [edgeA, edgeB, edgeC, edgeD, edgeE], + pageInfo: { + startCursor: cursorA, + endCursor: cursorE, + hasPreviousPage: false, + hasNextPage: false, + }, + }); + }); + + it('respects first and before', () => { + let c = connectionFromArray(arrayABCDE, { + first: 2, + before: cursorC, + }); + expect(c).to.deep.equal({ + edges: [edgeA, edgeB], + pageInfo: { + startCursor: cursorA, + endCursor: cursorB, + hasPreviousPage: false, + hasNextPage: false, + }, + }); + + c = connectionFromArray(arrayABCDE, { + first: 2, + before: cursorE, + }); + expect(c).to.deep.equal({ + edges: [edgeA, edgeB], + pageInfo: { + startCursor: cursorA, + endCursor: cursorB, + hasPreviousPage: false, + hasNextPage: true, + }, + }); + + c = connectionFromArray(arrayABCDE, { + first: 10, + before: cursorC, + }); + expect(c).to.deep.equal({ + edges: [edgeA, edgeB], + pageInfo: { + startCursor: cursorA, + endCursor: cursorB, + hasPreviousPage: false, + hasNextPage: false, + }, + }); + }); + + it('respects last and after', () => { + let c = connectionFromArray(arrayABCDE, { + last: 2, + after: cursorA, + }); + expect(c).to.deep.equal({ + edges: [edgeD, edgeE], + pageInfo: { + startCursor: cursorD, + endCursor: cursorE, + hasPreviousPage: true, + hasNextPage: false, + }, + }); + + c = connectionFromArray(arrayABCDE, { + last: 2, + after: cursorC, + }); + expect(c).to.deep.equal({ + edges: [edgeD, edgeE], + pageInfo: { + startCursor: cursorD, + endCursor: cursorE, + hasPreviousPage: false, + hasNextPage: false, + }, + }); + + c = connectionFromArray(arrayABCDE, { + last: 10, + after: cursorC, + }); + expect(c).to.deep.equal({ + edges: [edgeD, edgeE], + pageInfo: { + startCursor: cursorD, + endCursor: cursorE, + hasPreviousPage: false, hasNextPage: false, }, }); @@ -298,7 +637,10 @@ describe('connectionFromArray()', () => { }); it('returns all elements if cursors are on the outside', () => { - const allEdges = { + let c = connectionFromArray(arrayABCDE, { + before: offsetToCursor(6), + }); + expect(c).to.deep.equal({ edges: [edgeA, edgeB, edgeC, edgeD, edgeE], pageInfo: { startCursor: cursorA, @@ -306,21 +648,52 @@ describe('connectionFromArray()', () => { hasPreviousPage: false, hasNextPage: false, }, - }; + }); - expect( - connectionFromArray(arrayABCDE, { before: offsetToCursor(6) }), - ).to.deep.equal(allEdges); - expect( - connectionFromArray(arrayABCDE, { before: offsetToCursor(-1) }), - ).to.deep.equal(allEdges); + // `hasNextPage=false` because the spec says: + // "If before is set: If the server can efficiently determine that elements exist following `before`, return true." + // and `before` is before the beginning of the array so there are elements after. + c = connectionFromArray(arrayABCDE, { + before: offsetToCursor(-1), + }); + expect(c).to.deep.equal({ + edges: [edgeA, edgeB, edgeC, edgeD, edgeE], + pageInfo: { + startCursor: cursorA, + endCursor: cursorE, + hasPreviousPage: false, + hasNextPage: true, + }, + }); + + // `hasPreviousPage=true` because the spec says: + // "If after is set: If the server can efficiently determine that elements exist prior to `after`, return true" + // and `after` is after the end of the array so there are elements before. + c = connectionFromArray(arrayABCDE, { + after: offsetToCursor(6), + }); + expect(c).to.deep.equal({ + edges: [edgeA, edgeB, edgeC, edgeD, edgeE], + pageInfo: { + startCursor: cursorA, + endCursor: cursorE, + hasPreviousPage: true, + hasNextPage: false, + }, + }); - expect( - connectionFromArray(arrayABCDE, { after: offsetToCursor(6) }), - ).to.deep.equal(allEdges); - expect( - connectionFromArray(arrayABCDE, { after: offsetToCursor(-1) }), - ).to.deep.equal(allEdges); + c = connectionFromArray(arrayABCDE, { + after: offsetToCursor(-1), + }); + expect(c).to.deep.equal({ + edges: [edgeA, edgeB, edgeC, edgeD, edgeE], + pageInfo: { + startCursor: cursorA, + endCursor: cursorE, + hasPreviousPage: false, + hasNextPage: false, + }, + }); }); it('returns no elements if cursors cross', () => { @@ -333,8 +706,8 @@ describe('connectionFromArray()', () => { pageInfo: { startCursor: null, endCursor: null, - hasPreviousPage: false, - hasNextPage: false, + hasPreviousPage: true, + hasNextPage: true, }, }); }); @@ -404,7 +777,7 @@ describe('connectionFromArraySlice()', () => { pageInfo: { startCursor: cursorB, endCursor: cursorC, - hasPreviousPage: false, + hasPreviousPage: true, hasNextPage: true, }, }); @@ -427,7 +800,7 @@ describe('connectionFromArraySlice()', () => { pageInfo: { startCursor: cursorB, endCursor: cursorC, - hasPreviousPage: false, + hasPreviousPage: true, hasNextPage: true, }, }); @@ -450,7 +823,7 @@ describe('connectionFromArraySlice()', () => { pageInfo: { startCursor: cursorC, endCursor: cursorC, - hasPreviousPage: false, + hasPreviousPage: true, hasNextPage: true, }, }); @@ -473,7 +846,7 @@ describe('connectionFromArraySlice()', () => { pageInfo: { startCursor: cursorC, endCursor: cursorC, - hasPreviousPage: false, + hasPreviousPage: true, hasNextPage: true, }, }); @@ -496,7 +869,7 @@ describe('connectionFromArraySlice()', () => { pageInfo: { startCursor: cursorD, endCursor: cursorE, - hasPreviousPage: false, + hasPreviousPage: true, hasNextPage: false, }, }); @@ -519,8 +892,8 @@ describe('connectionFromArraySlice()', () => { pageInfo: { startCursor: cursorC, endCursor: cursorD, - hasPreviousPage: false, - hasNextPage: true, + hasPreviousPage: true, + hasNextPage: false, }, }); }); @@ -542,8 +915,8 @@ describe('connectionFromArraySlice()', () => { pageInfo: { startCursor: cursorD, endCursor: cursorD, - hasPreviousPage: false, - hasNextPage: true, + hasPreviousPage: true, + hasNextPage: false, }, }); }); diff --git a/src/connection/arrayConnection.ts b/src/connection/arrayConnection.ts index 1e3b1c8..33f253e 100644 --- a/src/connection/arrayConnection.ts +++ b/src/connection/arrayConnection.ts @@ -58,28 +58,32 @@ export function connectionFromArraySlice( let startOffset = Math.max(sliceStart, 0); let endOffset = Math.min(sliceEnd, arrayLength); + let firstEdgeOffset = 0; const afterOffset = getOffsetWithDefault(after, -1); if (0 <= afterOffset && afterOffset < arrayLength) { startOffset = Math.max(startOffset, afterOffset + 1); + firstEdgeOffset = afterOffset + 1; } - const beforeOffset = getOffsetWithDefault(before, endOffset); + let lastEdgeOffset = arrayLength - 1; + const beforeOffset = getOffsetWithDefault(before, arrayLength); if (0 <= beforeOffset && beforeOffset < arrayLength) { endOffset = Math.min(endOffset, beforeOffset); + lastEdgeOffset = beforeOffset - 1; } + const numberEdgesAfterCursors = lastEdgeOffset - firstEdgeOffset + 1; + if (typeof first === 'number') { if (first < 0) { throw new Error('Argument "first" must be a non-negative integer'); } - endOffset = Math.min(endOffset, startOffset + first); } if (typeof last === 'number') { if (last < 0) { throw new Error('Argument "last" must be a non-negative integer'); } - startOffset = Math.max(startOffset, endOffset - last); } @@ -96,16 +100,30 @@ export function connectionFromArraySlice( const firstEdge = edges[0]; const lastEdge = edges[edges.length - 1]; - const lowerBound = after != null ? afterOffset + 1 : 0; - const upperBound = before != null ? beforeOffset : arrayLength; + + // Determine hasPreviousPage + let hasPreviousPage = false; + if (typeof last === 'number') { + hasPreviousPage = numberEdgesAfterCursors > last; + } else if (after) { + hasPreviousPage = afterOffset >= 0; + } + + // Determine hasNextPage + let hasNextPage = false; + if (typeof first === 'number') { + hasNextPage = numberEdgesAfterCursors > first; + } else if (before) { + hasNextPage = beforeOffset < arrayLength; + } + return { edges, pageInfo: { startCursor: firstEdge ? firstEdge.cursor : null, endCursor: lastEdge ? lastEdge.cursor : null, - hasPreviousPage: - typeof last === 'number' ? startOffset > lowerBound : false, - hasNextPage: typeof first === 'number' ? endOffset < upperBound : false, + hasPreviousPage, + hasNextPage, }, }; }