Skip to content
This repository was archived by the owner on Jun 22, 2021. It is now read-only.

refactor: Changes pagination and sorting constants to improve readability. #8

Merged
merged 1 commit into from
Mar 18, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
31 changes: 16 additions & 15 deletions src/tests/getEntities/paginationTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TestEntity>) => {
Expand All @@ -16,8 +17,8 @@ export default (facade: Facade<TestEntity>) => {
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 });
};

Expand All @@ -29,50 +30,50 @@ export default (facade: Facade<TestEntity>) => {

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, []);
});
};
13 changes: 7 additions & 6 deletions src/tests/getEntities/sortTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TestEntity>) => {
Expand All @@ -26,36 +27,36 @@ export default (facade: Facade<TestEntity>) => {
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 });
});
};
3 changes: 2 additions & 1 deletion src/types/Pagination.ts
Original file line number Diff line number Diff line change
@@ -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;
}
6 changes: 6 additions & 0 deletions src/types/PaginationDirection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const forward = 'forward';
export const backward = 'backward';

type PaginationDirection = typeof forward | typeof backward;

export default PaginationDirection;
3 changes: 2 additions & 1 deletion src/types/Sort.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import Entity from './Entity';
import SortOrder from './SortOrder';

type Sort<E extends Entity> = {
readonly [P in keyof E]?: boolean;
readonly [P in keyof E]?: SortOrder;
};

export default Sort;
6 changes: 6 additions & 0 deletions src/types/SortOrder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const asc = 'asc';
export const desc = 'desc';

type SortOrder = typeof asc | typeof desc;

export default SortOrder;
5 changes: 3 additions & 2 deletions src/utils/createCursorFromEntity/index.test.ts
Original file line number Diff line number Diff line change
@@ -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<TestEntity>(undefined, { id: true });
const actualResult = createCursorFromEntity<TestEntity>(undefined, { id: asc });
assert.equal(actualResult, undefined);
});

it('should return the correct cursor when the entity is defined', () => {
const actualResult = createCursorFromEntity<TestEntity>(testEntity, { id: true });
const actualResult = createCursorFromEntity<TestEntity>(testEntity, { id: asc });
assert.equal(actualResult, 'eyJpZCI6InRlc3RfaWQifQ==');
});
});
14 changes: 8 additions & 6 deletions src/utils/createPaginationFilter/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,40 @@ 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<TestEntity> = { id: true, booleanProp: false };
const sort: Sort<TestEntity> = { 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<TestEntity>(pagination, sort);
const expectedResult = {};
assert.deepEqual(actualResult, expectedResult);
});

it('should return the correct filter when the cursor is defined and going forward', () => {
const cursor = createCursorFromEntity<TestEntity>(testEntity, sort);
const pagination: Pagination = { cursor, forward: true, limit: 1 };
const pagination: Pagination = { cursor, direction: forward, limit: 1 };
const actualResult = createPaginationFilter<TestEntity>(pagination, sort);
const expectedResult: Filter<TestEntity> = {
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>(testEntity, sort);
const pagination: Pagination = { cursor, forward: false, limit: 1 };
const pagination: Pagination = { cursor, direction: backward, limit: 1 };
const actualResult = createPaginationFilter<TestEntity>(pagination, sort);
const expectedResult: Filter<TestEntity> = {
booleanProp: { $gte: testEntity.booleanProp },
id: { $lt: testEntity.id },
numberProp: { $gte: testEntity.numberProp },
};
assert.deepEqual(actualResult, expectedResult);
});
Expand Down
9 changes: 7 additions & 2 deletions src/utils/createPaginationFilter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -16,8 +18,11 @@ export default <E extends Entity>(pagination: Pagination, sort: Sort<E>): 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 {
Expand Down