diff --git a/docs/facade.md b/docs/facade.md index bb18081a..95434081 100644 --- a/docs/facade.md +++ b/docs/facade.md @@ -15,6 +15,8 @@ This package also contains some utility functions outside of the Facade that you - [convertPropertyFilter](./utils.md#convertpropertyfilter) - [createCursorFromEntity](./utils.md#createcursorfromentity) +- [createCursorsFromEntities](./utils.md#createcursorsfromentities) +- [createGetEntitiesResult](./utils.md#creategetentitiesresult) - [createPaginationFilter](./utils.md#createpaginationfilter) The [facade in this package is a TypeScript interface](../src/Facade.ts), but concrete implementations of the interface are listed below. diff --git a/docs/functions.md b/docs/functions.md index f7122e79..3d4a9766 100644 --- a/docs/functions.md +++ b/docs/functions.md @@ -49,22 +49,29 @@ Retreives a sorted paginated array of entities that match the [`filter`](./optio ```ts import { backward, forward } from '@js-entity-repos/core/dist/types/PaginationDirection'; import { asc, desc } from '@js-entity-repos/core/dist/types/SortOrder'; +import { start } from '@js-entity-repos/core/dist/types/Cursor'; -const { entities, nextCursor, previousCursor } = await facade.getEntities({ +const firstForwardPage = await facade.getEntities({ filter: { foo: 'demo' }, sort: { id: asc, bar: desc }, - pagination: { limit: 10, direction: forward, cursor: undefined }, -}); -const secondPage = await facade.getEntities({ - filter: { foo: 'demo' }, - sort: { id: asc, bar: desc }, - pagination: { limit: 10, direction: forward, cursor: nextCursor }, -}); -const firstPage = await facade.getEntities({ - filter: { foo: 'demo' }, - sort: { id: asc, bar: desc }, - pagination: { limit: 10, direction: backward, cursor: secondPage.previousCursor }, + pagination: { limit: 10, direction: forward, cursor: start }, }); +const firstPageEntities = firstForwardPage.entities; +if (firstForwardPage.hasMoreForward) { + const secondForwardPage = await facade.getEntities({ + filter: { foo: 'demo' }, + sort: { id: asc, bar: desc }, + pagination: { limit: 10, direction: forward, cursor: firstForwardPage.forwardCursor }, + }); + const secondPageEntities = secondForwardPage.entities; + if (secondForwardPage.hasMoreBackward) { + const firstPage = await facade.getEntities({ + filter: { foo: 'demo' }, + sort: { id: asc, bar: desc }, + pagination: { limit: 10, direction: backward, cursor: secondForwardPage.backwardCursor }, + }); + } +} ``` This package contains the [get entities tests](../src/tests/getEntities) and the [get entities signature](../src/signatures/GetEntities.ts) for this function. diff --git a/docs/utils.md b/docs/utils.md index 19494784..184e1c1f 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -4,6 +4,8 @@ This package contains some common utility functions that can be used by both use - [convertPropertyFilter](#convertpropertyfilter) - [createCursorFromEntity](#createcursorfromentity) +- [createCursorsFromEntities](#createcursorsfromentities) +- [createGetEntitiesResult](#creategetentitiesresult) - [createPaginationFilter](#createpaginationfilter) ### convertPropertyFilter @@ -21,48 +23,37 @@ convertPropertyFilter({ ``` ### createCursorFromEntity -Exactly what it says on the tin, this creates a cursor from an entity. A cursor is constructed by creating a [filter](./options#filter) that will filter out entities not expected in the next set of paginated results. This filter is then stringified to JSON and base 64 encoded. This function is usually used by [concrete implementations of the Facade](./facade.md#facade) like [Knex's getEntities implementation](https://github.com/js-entity-repos/knex/blob/master/src/functions/getEntities.ts).. +Exactly what it says on the tin, this creates a cursor from an entity. A cursor is constructed by creating a [filter](./options#filter) that will filter out entities not expected in the next set of paginated results. This filter is then stringified to JSON and base 64 encoded. This function is usually used by the [`createCursorsFromEntities` util](#createcursorsfromentities). Check out the [`createCursorsFromEntities` implementation](../src/utils/createCursorsFromEntities/index.ts) for an example of its use. -```ts -import createCursorFromEntity from '@js-entity-repos/core/dist/utils/createCursorFromEntity'; -import { asc } from '@js-entity-repos/core/dist/types/SortOrder'; - -createCursorFromEntity(undefined, { id: asc }); -// Returns undefined +### createCursorsFromEntities +Creates a `forwardCursor` and a `backwardCursor` from an array of entities along with a sort and cursor. This function is usually used by the [`createGetEntitiesResult` util](#creategetentitiesresult). Check out the [`createGetEntitiesResult` implementation](../src/utils/createGetEntitiesResult/index.ts) for an example of its use. -createCursorFromEntity({ - booleanProp: true, - id: 'test_id_1', - numberProp: 1, - stringProp: 'test_string_prop', -}, { - id: asc, -}); -// Returns eyJpZCI6InRlc3RfaWQifQ== -``` +### createGetEntitiesResult +This function is usually used by [concrete implementations of the Facade](./facade.md#facade) to construct the result for a request to get entities. Check out [Knex's getEntities implementation](https://github.com/js-entity-repos/knex/blob/master/src/functions/getEntities.ts) for an example of its use. ### createPaginationFilter Takes a [pagination option](./options#pagination) and a [sort option](./options#sort)) to produces a [filter](./options#filter) that can filter out entities not expected in the next set of paginated results. This function is usually used by [concrete implementations of the Facade](./facade.md#facade) like [Knex's getEntities implementation](https://github.com/js-entity-repos/knex/blob/master/src/functions/getEntities.ts). ```ts import createPaginationFilter from '@js-entity-repos/core/dist/utils/createPaginationFilter'; +import { start } from '@js-entity-repos/core/dist/types/Cursor'; import { asc, desc } from '@js-entity-repos/core/dist/types/SortOrder'; import { backward, forward } from '@js-entity-repos/core/dist/types/PaginationDirection'; createPaginationFilter( - { cursor: undefined, direction: forward, limit: 1 }, + { cursor: start, direction: forward, limit: 1 }, { id: asc, numberProp: desc } ); // Returns {} createPaginationFilter( - { cursor: nextCursor, direction: forward, limit: 1 }, + { cursor: forwardCursor, direction: forward, limit: 1 }, { id: asc, numberProp: desc } ); // Returns the result of { id: { $gt: lastEntity.id }, numberProp: { $lte: lastEntity.numberProp } } createPaginationFilter( - { cursor: prevCursor, direction: backward, limit: 1 }, + { cursor: backwardCursor, direction: backward, limit: 1 }, { id: asc, numberProp: desc } ); // Returns the result of { id: { $lt: firstEntity.id }, numberProp: { $gte: firstEntity.numberProp } } diff --git a/src/errors/PaginationFilterError.ts b/src/errors/PaginationFilterError.ts deleted file mode 100644 index e62faee4..00000000 --- a/src/errors/PaginationFilterError.ts +++ /dev/null @@ -1,8 +0,0 @@ -// tslint:disable:no-class -import { BaseError } from 'make-error'; - -export default class PaginationFilterError extends BaseError { - constructor() { - super(); - } -} diff --git a/src/signatures/GetEntities.ts b/src/signatures/GetEntities.ts index 9d8f26c4..2fb43823 100644 --- a/src/signatures/GetEntities.ts +++ b/src/signatures/GetEntities.ts @@ -12,8 +12,10 @@ export interface Opts { export interface Result { readonly entities: E[]; - readonly nextCursor: Cursor; - readonly previousCursor: Cursor; + readonly forwardCursor: Cursor; + readonly backwardCursor: Cursor; + readonly hasMoreForward: boolean; + readonly hasMoreBackward: boolean; } export type Signature = (opts: Opts) => Promise>; diff --git a/src/tests/getEntities/paginationTest.ts b/src/tests/getEntities/paginationTest.ts index ba596252..0d225600 100644 --- a/src/tests/getEntities/paginationTest.ts +++ b/src/tests/getEntities/paginationTest.ts @@ -1,7 +1,8 @@ +// tslint:disable:max-file-line-count import 'mocha'; // tslint:disable-line:no-import-side-effect import * as assert from 'power-assert'; import Facade from '../../Facade'; -import Cursor, { end, start } from '../../types/Cursor'; +import Cursor, { start } from '../../types/Cursor'; import Pagination from '../../types/Pagination'; import PaginationDirection, { backward, forward } from '../../types/PaginationDirection'; import Sort from '../../types/Sort'; @@ -10,77 +11,100 @@ import createCursorFromEntity from '../../utils/createCursorFromEntity'; import { TestEntity, testEntity } from '../utils/testEntity'; export default (facade: Facade) => { + const sort: Sort = { id: asc }; const firstId = 'test_id_1'; const secondId = 'test_id_2'; const firstEntity = { ...testEntity, id: firstId }; const secondEntity = { ...testEntity, id: secondId }; - const sort: Sort = { id: asc }; + const firstCursor = createCursorFromEntity(firstEntity, sort); + const secondCursor = createCursorFromEntity(secondEntity, sort); const createTestEntities = async () => { await facade.createEntity({ id: firstId, entity: firstEntity }); await facade.createEntity({ id: secondId, entity: secondEntity }); }; - const paginate = (cursor: Cursor, direction: PaginationDirection) => { + const paginate = async (cursor: Cursor, direction: PaginationDirection) => { const pagination: Pagination = { cursor, direction, limit: 1 }; + await createTestEntities(); return facade.getEntities({ pagination, sort }); }; it('should return all entities when pagination is not defined', async () => { await createTestEntities(); const result = await facade.getEntities({}); - assert.deepEqual(result.entities, [firstEntity, secondEntity]); - assert.equal(result.previousCursor, createCursorFromEntity(firstEntity, sort)); - assert.equal(result.nextCursor, end); + assert.deepEqual(result, { + backwardCursor: firstCursor, + entities: [firstEntity, secondEntity], + forwardCursor: secondCursor, + hasMoreBackward: false, + hasMoreForward: false, + }); }); it('should return first entity when paginating forward with start cursor', async () => { - await createTestEntities(); - const finalResult = await paginate(start, forward); - assert.deepEqual(finalResult.entities, [firstEntity]); - assert.equal(finalResult.previousCursor, end); - assert.equal(finalResult.nextCursor, createCursorFromEntity(firstEntity, sort)); + const result = await paginate(start, forward); + assert.deepEqual(result, { + backwardCursor: firstCursor, + entities: [firstEntity], + forwardCursor: firstCursor, + hasMoreBackward: false, + hasMoreForward: true, + }); }); it('should return second entity when paginating forward with first cursor', async () => { - await createTestEntities(); - const firstResult = await paginate(start, forward); - const finalResult = await paginate(firstResult.nextCursor, forward); - assert.deepEqual(finalResult.entities, [secondEntity]); - assert.equal(finalResult.previousCursor, createCursorFromEntity(secondEntity, sort)); - assert.equal(finalResult.nextCursor, end); + const result = await paginate(firstCursor, forward); + assert.deepEqual(result, { + backwardCursor: secondCursor, + entities: [secondEntity], + forwardCursor: secondCursor, + hasMoreBackward: true, + hasMoreForward: false, + }); }); - it('should return no entities when paginating forward with end cursor', async () => { - await createTestEntities(); - const finalResult = await paginate(end, forward); - assert.deepEqual(finalResult.entities, []); - assert.equal(finalResult.previousCursor, start); - assert.equal(finalResult.nextCursor, end); + it('should return no entities when paginating forward with second cursor', async () => { + const result = await paginate(secondCursor, forward); + assert.deepEqual(result, { + backwardCursor: secondCursor, + entities: [], + forwardCursor: secondCursor, + hasMoreBackward: true, + hasMoreForward: false, + }); }); it('should return second entity when paginating backward with start cursor', async () => { - await createTestEntities(); - const finalResult = await paginate(start, backward); - assert.deepEqual(finalResult.entities, [secondEntity]); - assert.equal(finalResult.previousCursor, createCursorFromEntity(secondEntity, sort)); - assert.equal(finalResult.nextCursor, end); + const result = await paginate(start, backward); + assert.deepEqual(result, { + backwardCursor: secondCursor, + entities: [secondEntity], + forwardCursor: secondCursor, + hasMoreBackward: true, + hasMoreForward: false, + }); }); - it('should return first entity when paginating backward with first cursor', async () => { - await createTestEntities(); - const firstResult = await paginate(start, backward); - const finalResult = await paginate(firstResult.previousCursor, backward); - assert.deepEqual(finalResult.entities, [firstEntity]); - assert.equal(finalResult.previousCursor, end); - assert.equal(finalResult.nextCursor, createCursorFromEntity(firstEntity, sort)); + it('should return first entity when paginating backward with second cursor', async () => { + const result = await paginate(secondCursor, backward); + assert.deepEqual(result, { + backwardCursor: firstCursor, + entities: [firstEntity], + forwardCursor: firstCursor, + hasMoreBackward: false, + hasMoreForward: true, + }); }); - it('should return no entities when paginating backward with end cursor', async () => { - await createTestEntities(); - const finalResult = await paginate(end, backward); - assert.deepEqual(finalResult.entities, []); - assert.equal(finalResult.previousCursor, end); - assert.equal(finalResult.nextCursor, start); + it('should return no entities when paginating backward with first cursor', async () => { + const result = await paginate(firstCursor, backward); + assert.deepEqual(result, { + backwardCursor: firstCursor, + entities: [], + forwardCursor: firstCursor, + hasMoreBackward: false, + hasMoreForward: true, + }); }); }; diff --git a/src/types/Cursor.ts b/src/types/Cursor.ts index 299f0103..40145042 100644 --- a/src/types/Cursor.ts +++ b/src/types/Cursor.ts @@ -1,6 +1,5 @@ export const start = undefined; -export const end = ''; -type Cursor = string | typeof start | typeof end; +type Cursor = string | typeof start; export default Cursor; diff --git a/src/utils/createCursorFromEntity/index.test.ts b/src/utils/createCursorFromEntity/index.test.ts index 3f625808..d044ce8f 100644 --- a/src/utils/createCursorFromEntity/index.test.ts +++ b/src/utils/createCursorFromEntity/index.test.ts @@ -1,16 +1,10 @@ import 'mocha'; // tslint:disable-line:no-import-side-effect import * as assert from 'power-assert'; import { TestEntity, testEntity } from '../../tests/utils/testEntity'; -import { end } from '../../types/Cursor'; import { asc } from '../../types/SortOrder'; import createCursorFromEntity from './index'; describe('createCursorFromEntity', () => { - it('should return undefined when the entity is undefined', () => { - const actualResult = createCursorFromEntity(undefined, { id: asc }); - assert.equal(actualResult, end); - }); - it('should return the correct cursor when the entity is defined', () => { const actualResult = createCursorFromEntity(testEntity, { id: asc }); assert.equal(actualResult, 'eyJpZCI6InRlc3RfaWQifQ=='); diff --git a/src/utils/createCursorFromEntity/index.ts b/src/utils/createCursorFromEntity/index.ts index f9cccc5b..b9d498b5 100644 --- a/src/utils/createCursorFromEntity/index.ts +++ b/src/utils/createCursorFromEntity/index.ts @@ -1,13 +1,10 @@ import * as btoa from 'btoa'; import { get, set } from 'lodash'; -import Cursor, { end } from '../../types/Cursor'; +import Cursor from '../../types/Cursor'; import Entity from '../../types/Entity'; import Sort from '../../types/Sort'; -export default (entity: E | undefined, sort: Sort): Cursor => { - if (entity === undefined) { - return end; - } +export default (entity: E, sort: Sort): Cursor => { const sortKeys = Object.keys(sort); const cursorResult = sortKeys.reduce>((result, sortKey) => { return set(result, sortKey, get(entity, sortKey)); diff --git a/src/utils/createCursorsFromEntities/index.test.ts b/src/utils/createCursorsFromEntities/index.test.ts new file mode 100644 index 00000000..3826a49a --- /dev/null +++ b/src/utils/createCursorsFromEntities/index.test.ts @@ -0,0 +1,47 @@ +import 'mocha'; // tslint:disable-line:no-import-side-effect +import * as assert from 'power-assert'; +import { TestEntity, testEntity } from '../../tests/utils/testEntity'; +import { start } from '../../types/Cursor'; +import Sort from '../../types/Sort'; +import { asc } from '../../types/SortOrder'; +import createCursorsFromEntities from './index'; + +describe('createCursorsFromEntities', () => { + const sort: Sort = { id: asc }; + const firstId = 'test_id_1'; + const secondId = 'test_id_2'; + const firstEntity = { ...testEntity, id: firstId }; + const secondEntity = { ...testEntity, id: secondId }; + const firstCursor = 'eyJpZCI6InRlc3RfaWRfMSJ9'; + const secondCursor = 'eyJpZCI6InRlc3RfaWRfMiJ9'; + + it('should return the correct cursors when there are no entities', () => { + const entities: TestEntity[] = []; + const cursor = start; + const actualResult = createCursorsFromEntities({ cursor, entities, sort }); + assert.deepEqual(actualResult, { + backwardCursor: start, + forwardCursor: start, + }); + }); + + it('should return the correct cursors when there is one entity', () => { + const entities = [firstEntity]; + const cursor = start; + const actualResult = createCursorsFromEntities({ cursor, entities, sort }); + assert.deepEqual(actualResult, { + backwardCursor: firstCursor, + forwardCursor: firstCursor, + }); + }); + + it('should return the correct cursors when there are many entities', () => { + const entities = [firstEntity, secondEntity]; + const cursor = start; + const actualResult = createCursorsFromEntities({ cursor, entities, sort }); + assert.deepEqual(actualResult, { + backwardCursor: firstCursor, + forwardCursor: secondCursor, + }); + }); +}); diff --git a/src/utils/createCursorsFromEntities/index.ts b/src/utils/createCursorsFromEntities/index.ts new file mode 100644 index 00000000..2b213a94 --- /dev/null +++ b/src/utils/createCursorsFromEntities/index.ts @@ -0,0 +1,29 @@ +import { first, last } from 'lodash'; +import Cursor from '../../types/Cursor'; +import Entity from '../../types/Entity'; +import Sort from '../../types/Sort'; +import createCursorFromEntity from '../createCursorFromEntity'; + +export interface Opts { + readonly entities: E[]; + readonly cursor: Cursor; + readonly sort: Sort; +} + +export interface Result { + readonly backwardCursor: Cursor; + readonly forwardCursor: Cursor; +} + +export default ({ entities, cursor, sort }: Opts): Result => { + if (entities.length !== 0) { + return { + backwardCursor: createCursorFromEntity(first(entities) as E, sort), + forwardCursor: createCursorFromEntity(last(entities) as E, sort), + }; + } + return { + backwardCursor: cursor, + forwardCursor: cursor, + }; +}; diff --git a/src/utils/createEndCursorResult/index.ts b/src/utils/createEndCursorResult/index.ts deleted file mode 100644 index 558dea53..00000000 --- a/src/utils/createEndCursorResult/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Result } from '../../signatures/GetEntities'; -import { end, start } from '../../types/Cursor'; -import Entity from '../../types/Entity'; -import Pagination from '../../types/Pagination'; -import { forward } from '../../types/PaginationDirection'; - -export default (pagination: Pagination): Result => { - if (pagination.direction === forward) { - return { entities: [], previousCursor: start, nextCursor: end }; - } - return { entities: [], previousCursor: end, nextCursor: start }; -}; diff --git a/src/utils/createGetEntitiesResult/backward.test.ts b/src/utils/createGetEntitiesResult/backward.test.ts new file mode 100644 index 00000000..bf5c67f6 --- /dev/null +++ b/src/utils/createGetEntitiesResult/backward.test.ts @@ -0,0 +1,89 @@ +import 'mocha'; // tslint:disable-line:no-import-side-effect +import * as assert from 'power-assert'; +import { TestEntity, testEntity } from '../../tests/utils/testEntity'; +import { start } from '../../types/Cursor'; +import Pagination from '../../types/Pagination'; +import { backward } from '../../types/PaginationDirection'; +import Sort from '../../types/Sort'; +import { asc } from '../../types/SortOrder'; +import createGetEntitiesResult from './index'; + +describe('createGetEntitiesResult backward', () => { + const sort: Sort = { id: asc }; + const firstId = 'test_id_1'; + const secondId = 'test_id_2'; + const firstEntity = { ...testEntity, id: firstId }; + const secondEntity = { ...testEntity, id: secondId }; + const firstCursor = 'eyJpZCI6InRlc3RfaWRfMSJ9'; + const secondCursor = 'eyJpZCI6InRlc3RfaWRfMiJ9'; + + it('should return the correct result when there are no entities', () => { + const entities: TestEntity[] = []; + const pagination: Pagination = { cursor: start, direction: backward, limit: 1 }; + const isEnd = true; + const result = createGetEntitiesResult({ entities, isEnd, pagination, sort }); + assert.deepEqual(result, { + backwardCursor: start, + entities, + forwardCursor: start, + hasMoreBackward: false, + hasMoreForward: false, + }); + }); + + it('should return the correct result when not at the end going from start', () => { + const entities = [firstEntity, secondEntity]; + const pagination: Pagination = { cursor: start, direction: backward, limit: 1 }; + const isEnd = false; + const result = createGetEntitiesResult({ entities, isEnd, pagination, sort }); + assert.deepEqual(result, { + backwardCursor: firstCursor, + entities, + forwardCursor: secondCursor, + hasMoreBackward: true, + hasMoreForward: false, + }); + }); + + it('should return the correct result when at the end going from start', () => { + const entities = [firstEntity, secondEntity]; + const pagination: Pagination = { cursor: start, direction: backward, limit: 1 }; + const isEnd = true; + const result = createGetEntitiesResult({ entities, isEnd, pagination, sort }); + assert.deepEqual(result, { + backwardCursor: firstCursor, + entities, + forwardCursor: secondCursor, + hasMoreBackward: false, + hasMoreForward: false, + }); + }); + + it('should return the correct result when not at the end going from cursor', () => { + const entities = [secondEntity]; + const pagination: Pagination = { cursor: firstCursor, direction: backward, limit: 1 }; + const isEnd = false; + const result = createGetEntitiesResult({ entities, isEnd, pagination, sort }); + assert.deepEqual(result, { + backwardCursor: secondCursor, + entities, + forwardCursor: secondCursor, + hasMoreBackward: true, + hasMoreForward: true, + }); + }); + + it('should return the correct result when at the end going from cursor', () => { + const entities = [secondEntity]; + const pagination: Pagination = { cursor: firstCursor, direction: backward, limit: 1 }; + const isEnd = true; + const result = createGetEntitiesResult({ entities, isEnd, pagination, sort }); + assert.deepEqual(result, { + backwardCursor: secondCursor, + entities, + forwardCursor: secondCursor, + hasMoreBackward: false, + hasMoreForward: true, + }); + }); +}); diff --git a/src/utils/createGetEntitiesResult/forward.test.ts b/src/utils/createGetEntitiesResult/forward.test.ts new file mode 100644 index 00000000..130b87d3 --- /dev/null +++ b/src/utils/createGetEntitiesResult/forward.test.ts @@ -0,0 +1,89 @@ +import 'mocha'; // tslint:disable-line:no-import-side-effect +import * as assert from 'power-assert'; +import { TestEntity, testEntity } from '../../tests/utils/testEntity'; +import { start } from '../../types/Cursor'; +import Pagination from '../../types/Pagination'; +import { forward } from '../../types/PaginationDirection'; +import Sort from '../../types/Sort'; +import { asc } from '../../types/SortOrder'; +import createGetEntitiesResult from './index'; + +describe('createGetEntitiesResult forward', () => { + const sort: Sort = { id: asc }; + const firstId = 'test_id_1'; + const secondId = 'test_id_2'; + const firstEntity = { ...testEntity, id: firstId }; + const secondEntity = { ...testEntity, id: secondId }; + const firstCursor = 'eyJpZCI6InRlc3RfaWRfMSJ9'; + const secondCursor = 'eyJpZCI6InRlc3RfaWRfMiJ9'; + + it('should return the correct result when there are no entities', () => { + const entities: TestEntity[] = []; + const pagination: Pagination = { cursor: start, direction: forward, limit: 1 }; + const isEnd = true; + const result = createGetEntitiesResult({ entities, isEnd, pagination, sort }); + assert.deepEqual(result, { + backwardCursor: start, + entities, + forwardCursor: start, + hasMoreBackward: false, + hasMoreForward: false, + }); + }); + + it('should return the correct result when not at the end going from start', () => { + const entities = [firstEntity, secondEntity]; + const pagination: Pagination = { cursor: start, direction: forward, limit: 1 }; + const isEnd = false; + const result = createGetEntitiesResult({ entities, isEnd, pagination, sort }); + assert.deepEqual(result, { + backwardCursor: firstCursor, + entities, + forwardCursor: secondCursor, + hasMoreBackward: false, + hasMoreForward: true, + }); + }); + + it('should return the correct result when at the end going from start', () => { + const entities = [firstEntity, secondEntity]; + const pagination: Pagination = { cursor: start, direction: forward, limit: 1 }; + const isEnd = true; + const result = createGetEntitiesResult({ entities, isEnd, pagination, sort }); + assert.deepEqual(result, { + backwardCursor: firstCursor, + entities, + forwardCursor: secondCursor, + hasMoreBackward: false, + hasMoreForward: false, + }); + }); + + it('should return the correct result when not at the end going from cursor', () => { + const entities = [secondEntity]; + const pagination: Pagination = { cursor: firstCursor, direction: forward, limit: 1 }; + const isEnd = false; + const result = createGetEntitiesResult({ entities, isEnd, pagination, sort }); + assert.deepEqual(result, { + backwardCursor: secondCursor, + entities, + forwardCursor: secondCursor, + hasMoreBackward: true, + hasMoreForward: true, + }); + }); + + it('should return the correct result when at the end going from cursor', () => { + const entities = [secondEntity]; + const pagination: Pagination = { cursor: firstCursor, direction: forward, limit: 1 }; + const isEnd = true; + const result = createGetEntitiesResult({ entities, isEnd, pagination, sort }); + assert.deepEqual(result, { + backwardCursor: secondCursor, + entities, + forwardCursor: secondCursor, + hasMoreBackward: true, + hasMoreForward: false, + }); + }); +}); diff --git a/src/utils/createGetEntitiesResult/index.ts b/src/utils/createGetEntitiesResult/index.ts index e48fbac1..7dc9a172 100644 --- a/src/utils/createGetEntitiesResult/index.ts +++ b/src/utils/createGetEntitiesResult/index.ts @@ -1,11 +1,10 @@ -import { first, last } from 'lodash'; import { Result } from '../../signatures/GetEntities'; -import { end, start } from '../../types/Cursor'; +import { start } from '../../types/Cursor'; import Entity from '../../types/Entity'; import Pagination from '../../types/Pagination'; import { backward, forward } from '../../types/PaginationDirection'; import Sort from '../../types/Sort'; -import createCursorFromEntity from '../../utils/createCursorFromEntity'; +import createCursorsFromEntities from '../createCursorsFromEntities'; export interface Opts { readonly entities: E[]; @@ -15,23 +14,14 @@ export interface Opts { } export default ({ entities, isEnd, pagination, sort }: Opts): Result => { - const nextCursor = createCursorFromEntity(last(entities), sort); - const previousCursor = createCursorFromEntity(first(entities), sort); + const { direction, cursor } = pagination; - if (isEnd && pagination.direction === forward) { - return { entities, nextCursor: end, previousCursor }; - } - if (isEnd && pagination.direction === backward) { - return { entities, nextCursor, previousCursor: end }; - } + const isBackward = direction === backward; + const isForward = direction === forward; + const isStart = cursor === start; + const hasMoreBackward = (isBackward && !isEnd) || (isForward && !isStart); + const hasMoreForward = (isForward && !isEnd) || (isBackward && !isStart); - const isStart = pagination.cursor === start; - if (isStart && pagination.direction === forward) { - return { entities, nextCursor, previousCursor: end }; - } - if (isStart && pagination.direction === backward) { - return { entities, nextCursor: end, previousCursor }; - } - - return { entities, nextCursor, previousCursor }; + const { backwardCursor, forwardCursor } = createCursorsFromEntities({ entities, cursor, sort }); + return { entities, forwardCursor, backwardCursor, hasMoreBackward, hasMoreForward }; }; diff --git a/src/utils/createPaginationFilter/index.test.ts b/src/utils/createPaginationFilter/index.test.ts index 52b2afa5..7dd9d2a6 100644 --- a/src/utils/createPaginationFilter/index.test.ts +++ b/src/utils/createPaginationFilter/index.test.ts @@ -1,8 +1,7 @@ import 'mocha'; // tslint:disable-line:no-import-side-effect import * as assert from 'power-assert'; -import PaginationFilterError from '../../errors/PaginationFilterError'; import { TestEntity, testEntity } from '../../tests/utils/testEntity'; -import { end, start } from '../../types/Cursor'; +import { start } from '../../types/Cursor'; import { Filter } from '../../types/Filter'; import Pagination from '../../types/Pagination'; import { backward, forward } from '../../types/PaginationDirection'; @@ -21,16 +20,6 @@ describe('createCursorFromEntity', () => { assert.deepEqual(actualResult, expectedResult); }); - it('should return empty filter when the cursor is end', () => { - const pagination: Pagination = { cursor: end, direction: forward, limit: 1 }; - try { - createPaginationFilter(pagination, sort); - assert.fail(); - } catch (err) { - assert.ok(err instanceof PaginationFilterError); - } - }); - it('should return the correct filter when the cursor is defined and going forward', () => { const cursor = createCursorFromEntity(testEntity, sort); const pagination: Pagination = { cursor, direction: forward, limit: 1 }; diff --git a/src/utils/createPaginationFilter/index.ts b/src/utils/createPaginationFilter/index.ts index c7b19dcb..d3156872 100644 --- a/src/utils/createPaginationFilter/index.ts +++ b/src/utils/createPaginationFilter/index.ts @@ -1,7 +1,6 @@ import * as atob from 'atob'; import { get, mapValues } from 'lodash'; -import PaginationFilterError from '../../errors/PaginationFilterError'; -import { end, start } from '../../types/Cursor'; +import { start } from '../../types/Cursor'; import Entity from '../../types/Entity'; // tslint:disable-next-line:no-unused import Filter, { ConditionFilter, EntityFilter } from '../../types/Filter'; @@ -18,9 +17,6 @@ export default (pagination: Pagination, sort: Sort): Filter if (pagination.cursor === start) { return {}; } - if (pagination.cursor === end) { - throw new PaginationFilterError(); - } const cursor = pagination.cursor; const cursorObj = JSON.parse(atob(cursor)); const filter = mapValues(cursorObj, (cursorValue, sortKey) => {