diff --git a/docs/options.md b/docs/options.md index 50145c81..cc595919 100644 --- a/docs/options.md +++ b/docs/options.md @@ -101,13 +101,13 @@ The filter below is comprehensive example using all of the operators. ### Sort -This is an object where a key represents the entity property to be sorted and the value represents the direction to sort. The value should be `true` to sort in ascending order and `false` to sort in descending order. The properties are sorted in order of their definition in the sort option, for example, the following sort option `{ createdAt: false, id: true }` will sort by the `createdAt` property first and then the the `id` property. +This is an object where a key represents the entity property to be sorted and the value represents the direction to sort. The value should be `'asc'` to sort in ascending order and `'desc'` to sort in descending order. The properties are sorted in order of their definition in the sort option, for example, the following sort option `{ createdAt: false, id: true }` will sort by the `createdAt` property first and then the the `id` property. -This package contains the [TypeScript Sort type definition](../src/types/Sort.ts). +This package contains the [TypeScript Sort type definition](../src/types/Sort.ts). It also contains [constants for `'asc'` and `'desc'`](../src/types/SortOrder.ts) that should always be used to avoid breaking changes in the future. ### Pagination -This is an object with three properties, `limit`, `forward`, and `cursor`. The `limit` property defines how many entities to return (maximum). The `forward` property defines whether the entities should be iterated through forwards (when `true`) or backwards (when `false`) from the `cursor`. The `cursor` property defines where to start iterating through the entities. Cursors have been used instead of `skip` and `limit` to avoid the [pagination issues discussed by Rakhitha Nimesh](https://www.sitepoint.com/paginating-real-time-data-cursor-based-pagination/). +This is an object with three properties, `limit`, `direction`, and `cursor`. The `limit` property defines how many entities to return (maximum). The `direction` property defines whether the entities should be iterated through forwards (when `'forward'`) or backwards (when `'backward'`) from the `cursor`. The `cursor` property defines where to start iterating through the entities. Cursors have been used instead of `skip` and `limit` to avoid the [pagination issues discussed by Rakhitha Nimesh](https://www.sitepoint.com/paginating-real-time-data-cursor-based-pagination/). Concrete implementations of the facade can use the [`createCursorFromEntity`](../src/utils/createCursorFromEntity) and [`createPaginationFilter`](../src/utils/createPaginationFilter) utility functions to generate cursors. -This package also contains the [TypeScript Pagination interface](../src/types/Pagination.ts) and the [TypeScript Cursor type definition](../src/types/Cursor.ts). +This package contains the [TypeScript Pagination interface](../src/types/Pagination.ts) and the [TypeScript Cursor type definition](../src/types/Cursor.ts). It also contains [constants for `'forward'` and `'backward'`](../src/types/PaginationDirection.ts) that should always be used to avoid breaking changes in the future. diff --git a/src/tests/getEntities/paginationTest.ts b/src/tests/getEntities/paginationTest.ts index e84ac9af..53a1099c 100644 --- a/src/tests/getEntities/paginationTest.ts +++ b/src/tests/getEntities/paginationTest.ts @@ -3,6 +3,7 @@ import * as assert from 'power-assert'; import Facade from '../../Facade'; import Cursor from '../../types/Cursor'; import Pagination from '../../types/Pagination'; +import PaginationDirection, { backward, forward } from '../../types/PaginationDirection'; import { TestEntity, testEntity } from '../utils/testEntity'; export default (facade: Facade) => { @@ -16,8 +17,8 @@ export default (facade: Facade) => { await facade.createEntity({ id: secondId, entity: secondEntity }); }; - const paginate = (cursor: Cursor, forward: boolean) => { - const pagination: Pagination = { cursor, forward, limit: 1 }; + const paginate = (cursor: Cursor, direction: PaginationDirection) => { + const pagination: Pagination = { cursor, direction, limit: 1 }; return facade.getEntities({ pagination }); }; @@ -29,50 +30,50 @@ export default (facade: Facade) => { it('should return first entity when there are two entities limitted to 1', async () => { await createTestEntities(); - const pagination: Pagination = { cursor: undefined, forward: true, limit: 1 }; + 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, true); + const finalResult = await paginate(undefined, forward); assert.deepEqual(finalResult.entities, [firstEntity]); }); it('should return second entity when paginating forward with first cursor', async () => { await createTestEntities(); - const firstResult = await paginate(undefined, true); - const finalResult = await paginate(firstResult.nextCursor, true); + const firstResult = await paginate(undefined, forward); + const finalResult = await paginate(firstResult.nextCursor, forward); assert.deepEqual(finalResult.entities, [secondEntity]); }); it('should return no entities when paginating forward with second cursor', async () => { await createTestEntities(); - const firstResult = await paginate(undefined, true); - const secondResult = await paginate(firstResult.nextCursor, true); - const finalResult = await paginate(secondResult.nextCursor, true); + const firstResult = await paginate(undefined, forward); + const secondResult = await paginate(firstResult.nextCursor, forward); + const finalResult = await paginate(secondResult.nextCursor, forward); assert.deepEqual(finalResult.entities, []); }); it('should return second entity when paginating backward without cursor', async () => { await createTestEntities(); - const finalResult = await paginate(undefined, false); + const finalResult = await paginate(undefined, backward); assert.deepEqual(finalResult.entities, [secondEntity]); }); it('should return first entity when paginating backward with first cursor', async () => { await createTestEntities(); - const firstResult = await paginate(undefined, false); - const finalResult = await paginate(firstResult.previousCursor, false); + const firstResult = await paginate(undefined, backward); + const finalResult = await paginate(firstResult.previousCursor, backward); assert.deepEqual(finalResult.entities, [firstEntity]); }); it('should return no entities when paginating backward with second cursor', async () => { await createTestEntities(); - const firstResult = await paginate(undefined, false); - const secondResult = await paginate(firstResult.previousCursor, false); - const finalResult = await paginate(secondResult.previousCursor, false); + const firstResult = await paginate(undefined, backward); + const secondResult = await paginate(firstResult.previousCursor, backward); + const finalResult = await paginate(secondResult.previousCursor, backward); assert.deepEqual(finalResult.entities, []); }); }; diff --git a/src/tests/getEntities/sortTest.ts b/src/tests/getEntities/sortTest.ts index 559d351f..cac38ff1 100644 --- a/src/tests/getEntities/sortTest.ts +++ b/src/tests/getEntities/sortTest.ts @@ -2,6 +2,7 @@ import 'mocha'; // tslint:disable-line:no-import-side-effect import * as assert from 'power-assert'; import Facade from '../../Facade'; import Sort from '../../types/Sort'; +import { asc, desc } from '../../types/SortOrder'; import { TestEntity, testEntity } from '../utils/testEntity'; export default (facade: Facade) => { @@ -26,36 +27,36 @@ export default (facade: Facade) => { it('should sort by one ascending property when entities are ordered', async () => { await facade.createEntity({ id: firstId, entity: firstEntity }); await facade.createEntity({ id: secondId, entity: secondEntity }); - await assertSort([firstEntity, secondEntity], { stringProp: true }); + await assertSort([firstEntity, secondEntity], { stringProp: asc }); }); it('should sort by one ascending property when entities are unordered', async () => { await facade.createEntity({ id: secondId, entity: secondEntity }); await facade.createEntity({ id: firstId, entity: firstEntity }); - await assertSort([firstEntity, secondEntity], { stringProp: true }); + await assertSort([firstEntity, secondEntity], { stringProp: asc }); }); it('should sort by one descending property when entities are ordered', async () => { await facade.createEntity({ id: secondId, entity: secondEntity }); await facade.createEntity({ id: firstId, entity: firstEntity }); - await assertSort([secondEntity, firstEntity], { stringProp: false }); + await assertSort([secondEntity, firstEntity], { stringProp: desc }); }); it('should sort by one descending property when entities are unordered', async () => { await facade.createEntity({ id: firstId, entity: firstEntity }); await facade.createEntity({ id: secondId, entity: secondEntity }); - await assertSort([secondEntity, firstEntity], { stringProp: false }); + await assertSort([secondEntity, firstEntity], { stringProp: desc }); }); it('should sort by two properties when ascending first and descending second', async () => { await facade.createEntity({ id: firstId, entity: firstEntity }); await facade.createEntity({ id: secondId, entity: secondEntity }); - await assertSort([firstEntity, secondEntity], { stringProp: true, numberProp: false }); + await assertSort([firstEntity, secondEntity], { stringProp: asc, numberProp: desc }); }); it('should sort by two properties when descending first and ascending second', async () => { await facade.createEntity({ id: firstId, entity: firstEntity }); await facade.createEntity({ id: secondId, entity: secondEntity }); - await assertSort([secondEntity, firstEntity], { stringProp: false, numberProp: true }); + await assertSort([secondEntity, firstEntity], { stringProp: desc, numberProp: asc }); }); }; diff --git a/src/types/Pagination.ts b/src/types/Pagination.ts index 66d04efd..3f96c3e9 100644 --- a/src/types/Pagination.ts +++ b/src/types/Pagination.ts @@ -1,7 +1,8 @@ import Cursor from './Cursor'; +import PaginationDirection from './PaginationDirection'; export default interface Pagination { readonly cursor: Cursor; - readonly forward: boolean; + readonly direction: PaginationDirection; readonly limit: number; } diff --git a/src/types/PaginationDirection.ts b/src/types/PaginationDirection.ts new file mode 100644 index 00000000..e4b6c700 --- /dev/null +++ b/src/types/PaginationDirection.ts @@ -0,0 +1,6 @@ +export const forward = 'forward'; +export const backward = 'backward'; + +type PaginationDirection = typeof forward | typeof backward; + +export default PaginationDirection; diff --git a/src/types/Sort.ts b/src/types/Sort.ts index a2f4881e..ef827a4d 100644 --- a/src/types/Sort.ts +++ b/src/types/Sort.ts @@ -1,7 +1,8 @@ import Entity from './Entity'; +import SortOrder from './SortOrder'; type Sort = { - readonly [P in keyof E]?: boolean; + readonly [P in keyof E]?: SortOrder; }; export default Sort; diff --git a/src/types/SortOrder.ts b/src/types/SortOrder.ts new file mode 100644 index 00000000..b800d306 --- /dev/null +++ b/src/types/SortOrder.ts @@ -0,0 +1,6 @@ +export const asc = 'asc'; +export const desc = 'desc'; + +type SortOrder = typeof asc | typeof desc; + +export default SortOrder; diff --git a/src/utils/createCursorFromEntity/index.test.ts b/src/utils/createCursorFromEntity/index.test.ts index c04812ae..75d1a985 100644 --- a/src/utils/createCursorFromEntity/index.test.ts +++ b/src/utils/createCursorFromEntity/index.test.ts @@ -1,16 +1,17 @@ import 'mocha'; // tslint:disable-line:no-import-side-effect import * as assert from 'power-assert'; import { TestEntity, testEntity } from '../../tests/utils/testEntity'; +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: true }); + const actualResult = createCursorFromEntity(undefined, { id: asc }); assert.equal(actualResult, undefined); }); it('should return the correct cursor when the entity is defined', () => { - const actualResult = createCursorFromEntity(testEntity, { id: true }); + const actualResult = createCursorFromEntity(testEntity, { id: asc }); assert.equal(actualResult, 'eyJpZCI6InRlc3RfaWQifQ=='); }); }); diff --git a/src/utils/createPaginationFilter/index.test.ts b/src/utils/createPaginationFilter/index.test.ts index f09117ca..70e369f8 100644 --- a/src/utils/createPaginationFilter/index.test.ts +++ b/src/utils/createPaginationFilter/index.test.ts @@ -3,15 +3,17 @@ import * as assert from 'power-assert'; import { TestEntity, testEntity } from '../../tests/utils/testEntity'; import { Filter } from '../../types/Filter'; import Pagination from '../../types/Pagination'; +import { backward, forward } from '../../types/PaginationDirection'; import Sort from '../../types/Sort'; +import { asc, desc } from '../../types/SortOrder'; import createCursorFromEntity from '../createCursorFromEntity'; import createPaginationFilter from './index'; describe('createCursorFromEntity', () => { - const sort: Sort = { id: true, booleanProp: false }; + const sort: Sort = { id: asc, numberProp: desc }; it('should return empty filter when the cursor is undefined', () => { - const pagination: Pagination = { cursor: undefined, forward: true, limit: 1 }; + const pagination: Pagination = { cursor: undefined, direction: forward, limit: 1 }; const actualResult = createPaginationFilter(pagination, sort); const expectedResult = {}; assert.deepEqual(actualResult, expectedResult); @@ -19,22 +21,22 @@ describe('createCursorFromEntity', () => { it('should return the correct filter when the cursor is defined and going forward', () => { const cursor = createCursorFromEntity(testEntity, sort); - const pagination: Pagination = { cursor, forward: true, limit: 1 }; + const pagination: Pagination = { cursor, direction: forward, limit: 1 }; const actualResult = createPaginationFilter(pagination, sort); const expectedResult: Filter = { - booleanProp: { $lte: testEntity.booleanProp }, id: { $gt: testEntity.id }, + numberProp: { $lte: testEntity.numberProp }, }; assert.deepEqual(actualResult, expectedResult); }); it('should return the correct filter when the cursor is defined and going backward', () => { const cursor = createCursorFromEntity(testEntity, sort); - const pagination: Pagination = { cursor, forward: false, limit: 1 }; + const pagination: Pagination = { cursor, direction: backward, limit: 1 }; const actualResult = createPaginationFilter(pagination, sort); const expectedResult: Filter = { - booleanProp: { $gte: testEntity.booleanProp }, id: { $lt: testEntity.id }, + numberProp: { $gte: testEntity.numberProp }, }; assert.deepEqual(actualResult, expectedResult); }); diff --git a/src/utils/createPaginationFilter/index.ts b/src/utils/createPaginationFilter/index.ts index a78060a5..38c23529 100644 --- a/src/utils/createPaginationFilter/index.ts +++ b/src/utils/createPaginationFilter/index.ts @@ -4,7 +4,9 @@ import Entity from '../../types/Entity'; // tslint:disable-next-line:no-unused import Filter, { ConditionFilter, EntityFilter } from '../../types/Filter'; import Pagination from '../../types/Pagination'; +import { forward } from '../../types/PaginationDirection'; import Sort from '../../types/Sort'; +import { asc } from '../../types/SortOrder'; const xor = (conditionA: boolean, conditionB: boolean) => { return (conditionA && !conditionB) || (!conditionA && conditionB); @@ -16,8 +18,11 @@ export default (pagination: Pagination, sort: Sort): Filter } const cursorObj = JSON.parse(atob(pagination.cursor)); const filter = mapValues(cursorObj, (cursorValue, sortKey) => { - const forward = !xor(get(sort, sortKey), pagination.forward); - if (forward) { + const ascendingPagination = !xor( + get(sort, sortKey) === asc, + pagination.direction === forward, + ); + if (ascendingPagination) { if (sortKey === 'id') { return { $gt: cursorValue }; } else {