diff --git a/src/errors/PaginationFilterError.ts b/src/errors/PaginationFilterError.ts new file mode 100644 index 00000000..e62faee4 --- /dev/null +++ b/src/errors/PaginationFilterError.ts @@ -0,0 +1,8 @@ +// tslint:disable:no-class +import { BaseError } from 'make-error'; + +export default class PaginationFilterError extends BaseError { + constructor() { + super(); + } +} diff --git a/src/tests/getEntities/paginationTest.ts b/src/tests/getEntities/paginationTest.ts index 53a1099c..ba596252 100644 --- a/src/tests/getEntities/paginationTest.ts +++ b/src/tests/getEntities/paginationTest.ts @@ -1,9 +1,12 @@ import 'mocha'; // tslint:disable-line:no-import-side-effect import * as assert from 'power-assert'; import Facade from '../../Facade'; -import Cursor from '../../types/Cursor'; +import Cursor, { end, start } from '../../types/Cursor'; import Pagination from '../../types/Pagination'; import PaginationDirection, { backward, forward } from '../../types/PaginationDirection'; +import Sort from '../../types/Sort'; +import { asc } from '../../types/SortOrder'; +import createCursorFromEntity from '../../utils/createCursorFromEntity'; import { TestEntity, testEntity } from '../utils/testEntity'; export default (facade: Facade) => { @@ -11,6 +14,7 @@ export default (facade: Facade) => { const secondId = 'test_id_2'; const firstEntity = { ...testEntity, id: firstId }; const secondEntity = { ...testEntity, id: secondId }; + const sort: Sort = { id: asc }; const createTestEntities = async () => { await facade.createEntity({ id: firstId, entity: firstEntity }); @@ -19,61 +23,64 @@ export default (facade: Facade) => { const paginate = (cursor: Cursor, direction: PaginationDirection) => { const pagination: Pagination = { cursor, direction, limit: 1 }; - return facade.getEntities({ pagination }); + 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); }); - it('should return first entity when there are two entities limitted to 1', async () => { + it('should return first entity when paginating forward with start cursor', async () => { await createTestEntities(); - const pagination: Pagination = { cursor: undefined, direction: forward, limit: 1 }; - const result = await facade.getEntities({ pagination }); - assert.deepEqual(result.entities, [firstEntity]); - }); - - it('should return first entity when paginating forward without cursor', async () => { - await createTestEntities(); - const finalResult = await paginate(undefined, forward); + const finalResult = await paginate(start, forward); assert.deepEqual(finalResult.entities, [firstEntity]); + assert.equal(finalResult.previousCursor, end); + assert.equal(finalResult.nextCursor, createCursorFromEntity(firstEntity, sort)); }); it('should return second entity when paginating forward with first cursor', async () => { await createTestEntities(); - const firstResult = await paginate(undefined, forward); + 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); }); - it('should return no entities when paginating forward with second cursor', async () => { + it('should return no entities when paginating forward with end cursor', async () => { await createTestEntities(); - const firstResult = await paginate(undefined, forward); - const secondResult = await paginate(firstResult.nextCursor, forward); - const finalResult = await paginate(secondResult.nextCursor, forward); + const finalResult = await paginate(end, forward); assert.deepEqual(finalResult.entities, []); + assert.equal(finalResult.previousCursor, start); + assert.equal(finalResult.nextCursor, end); }); - it('should return second entity when paginating backward without cursor', async () => { + it('should return second entity when paginating backward with start cursor', async () => { await createTestEntities(); - const finalResult = await paginate(undefined, backward); + const finalResult = await paginate(start, backward); assert.deepEqual(finalResult.entities, [secondEntity]); + assert.equal(finalResult.previousCursor, createCursorFromEntity(secondEntity, sort)); + assert.equal(finalResult.nextCursor, end); }); it('should return first entity when paginating backward with first cursor', async () => { await createTestEntities(); - const firstResult = await paginate(undefined, backward); + 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 no entities when paginating backward with second cursor', async () => { + it('should return no entities when paginating backward with end cursor', async () => { await createTestEntities(); - const firstResult = await paginate(undefined, backward); - const secondResult = await paginate(firstResult.previousCursor, backward); - const finalResult = await paginate(secondResult.previousCursor, backward); + const finalResult = await paginate(end, backward); assert.deepEqual(finalResult.entities, []); + assert.equal(finalResult.previousCursor, end); + assert.equal(finalResult.nextCursor, start); }); }; diff --git a/src/types/Cursor.ts b/src/types/Cursor.ts index df349ac6..299f0103 100644 --- a/src/types/Cursor.ts +++ b/src/types/Cursor.ts @@ -1,3 +1,6 @@ -type Cursor = string | undefined; +export const start = undefined; +export const end = ''; + +type Cursor = string | typeof start | typeof end; export default Cursor; diff --git a/src/utils/createCursorFromEntity/index.test.ts b/src/utils/createCursorFromEntity/index.test.ts index 75d1a985..3f625808 100644 --- a/src/utils/createCursorFromEntity/index.test.ts +++ b/src/utils/createCursorFromEntity/index.test.ts @@ -1,13 +1,14 @@ 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, undefined); + assert.equal(actualResult, end); }); it('should return the correct cursor when the entity is defined', () => { diff --git a/src/utils/createCursorFromEntity/index.ts b/src/utils/createCursorFromEntity/index.ts index 048aba1a..f9cccc5b 100644 --- a/src/utils/createCursorFromEntity/index.ts +++ b/src/utils/createCursorFromEntity/index.ts @@ -1,12 +1,12 @@ import * as btoa from 'btoa'; import { get, set } from 'lodash'; -import Cursor from '../../types/Cursor'; +import Cursor, { end } 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 undefined; + return end; } const sortKeys = Object.keys(sort); const cursorResult = sortKeys.reduce>((result, sortKey) => { diff --git a/src/utils/createEndCursorResult/index.ts b/src/utils/createEndCursorResult/index.ts new file mode 100644 index 00000000..558dea53 --- /dev/null +++ b/src/utils/createEndCursorResult/index.ts @@ -0,0 +1,12 @@ +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/index.ts b/src/utils/createGetEntitiesResult/index.ts new file mode 100644 index 00000000..e48fbac1 --- /dev/null +++ b/src/utils/createGetEntitiesResult/index.ts @@ -0,0 +1,37 @@ +import { first, last } from 'lodash'; +import { Result } from '../../signatures/GetEntities'; +import { end, 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'; + +export interface Opts { + readonly entities: E[]; + readonly isEnd: boolean; + readonly pagination: Pagination; + readonly sort: Sort; +} + +export default ({ entities, isEnd, pagination, sort }: Opts): Result => { + const nextCursor = createCursorFromEntity(last(entities), sort); + const previousCursor = createCursorFromEntity(first(entities), sort); + + if (isEnd && pagination.direction === forward) { + return { entities, nextCursor: end, previousCursor }; + } + if (isEnd && pagination.direction === backward) { + return { entities, nextCursor, previousCursor: end }; + } + + 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 }; +}; diff --git a/src/utils/createPaginationFilter/index.test.ts b/src/utils/createPaginationFilter/index.test.ts index 70e369f8..52b2afa5 100644 --- a/src/utils/createPaginationFilter/index.test.ts +++ b/src/utils/createPaginationFilter/index.test.ts @@ -1,6 +1,8 @@ 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 { Filter } from '../../types/Filter'; import Pagination from '../../types/Pagination'; import { backward, forward } from '../../types/PaginationDirection'; @@ -12,13 +14,23 @@ import createPaginationFilter from './index'; describe('createCursorFromEntity', () => { const sort: Sort = { id: asc, numberProp: desc }; - it('should return empty filter when the cursor is undefined', () => { - const pagination: Pagination = { cursor: undefined, direction: forward, limit: 1 }; + it('should return empty filter when the cursor is start', () => { + const pagination: Pagination = { cursor: start, direction: forward, limit: 1 }; const actualResult = createPaginationFilter(pagination, sort); const expectedResult = {}; 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 38c23529..c7b19dcb 100644 --- a/src/utils/createPaginationFilter/index.ts +++ b/src/utils/createPaginationFilter/index.ts @@ -1,5 +1,7 @@ import * as atob from 'atob'; import { get, mapValues } from 'lodash'; +import PaginationFilterError from '../../errors/PaginationFilterError'; +import { end, start } from '../../types/Cursor'; import Entity from '../../types/Entity'; // tslint:disable-next-line:no-unused import Filter, { ConditionFilter, EntityFilter } from '../../types/Filter'; @@ -13,10 +15,14 @@ const xor = (conditionA: boolean, conditionB: boolean) => { }; export default (pagination: Pagination, sort: Sort): Filter => { - if (pagination.cursor === undefined) { + if (pagination.cursor === start) { return {}; } - const cursorObj = JSON.parse(atob(pagination.cursor)); + if (pagination.cursor === end) { + throw new PaginationFilterError(); + } + const cursor = pagination.cursor; + const cursorObj = JSON.parse(atob(cursor)); const filter = mapValues(cursorObj, (cursorValue, sortKey) => { const ascendingPagination = !xor( get(sort, sortKey) === asc,