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

Commit 16be9e1

Browse files
authored
feat: Adds utils for createEndCursorResult and createGetEntitiesResult. (#12)
BREAKING CHANGE: Tests for `getEntities` now assert the cursors returned. BREAKING CHANGE: The `Cursor` type now has `start` and `end` to improve pagination behaviour.
1 parent 9dbf0a6 commit 16be9e1

File tree

9 files changed

+117
-31
lines changed

9 files changed

+117
-31
lines changed

Diff for: src/errors/PaginationFilterError.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// tslint:disable:no-class
2+
import { BaseError } from 'make-error';
3+
4+
export default class PaginationFilterError extends BaseError {
5+
constructor() {
6+
super();
7+
}
8+
}

Diff for: src/tests/getEntities/paginationTest.ts

+30-23
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import 'mocha'; // tslint:disable-line:no-import-side-effect
22
import * as assert from 'power-assert';
33
import Facade from '../../Facade';
4-
import Cursor from '../../types/Cursor';
4+
import Cursor, { end, start } from '../../types/Cursor';
55
import Pagination from '../../types/Pagination';
66
import PaginationDirection, { backward, forward } from '../../types/PaginationDirection';
7+
import Sort from '../../types/Sort';
8+
import { asc } from '../../types/SortOrder';
9+
import createCursorFromEntity from '../../utils/createCursorFromEntity';
710
import { TestEntity, testEntity } from '../utils/testEntity';
811

912
export default (facade: Facade<TestEntity>) => {
1013
const firstId = 'test_id_1';
1114
const secondId = 'test_id_2';
1215
const firstEntity = { ...testEntity, id: firstId };
1316
const secondEntity = { ...testEntity, id: secondId };
17+
const sort: Sort<TestEntity> = { id: asc };
1418

1519
const createTestEntities = async () => {
1620
await facade.createEntity({ id: firstId, entity: firstEntity });
@@ -19,61 +23,64 @@ export default (facade: Facade<TestEntity>) => {
1923

2024
const paginate = (cursor: Cursor, direction: PaginationDirection) => {
2125
const pagination: Pagination = { cursor, direction, limit: 1 };
22-
return facade.getEntities({ pagination });
26+
return facade.getEntities({ pagination, sort });
2327
};
2428

2529
it('should return all entities when pagination is not defined', async () => {
2630
await createTestEntities();
2731
const result = await facade.getEntities({});
2832
assert.deepEqual(result.entities, [firstEntity, secondEntity]);
33+
assert.equal(result.previousCursor, createCursorFromEntity(firstEntity, sort));
34+
assert.equal(result.nextCursor, end);
2935
});
3036

31-
it('should return first entity when there are two entities limitted to 1', async () => {
37+
it('should return first entity when paginating forward with start cursor', async () => {
3238
await createTestEntities();
33-
const pagination: Pagination = { cursor: undefined, direction: forward, limit: 1 };
34-
const result = await facade.getEntities({ pagination });
35-
assert.deepEqual(result.entities, [firstEntity]);
36-
});
37-
38-
it('should return first entity when paginating forward without cursor', async () => {
39-
await createTestEntities();
40-
const finalResult = await paginate(undefined, forward);
39+
const finalResult = await paginate(start, forward);
4140
assert.deepEqual(finalResult.entities, [firstEntity]);
41+
assert.equal(finalResult.previousCursor, end);
42+
assert.equal(finalResult.nextCursor, createCursorFromEntity(firstEntity, sort));
4243
});
4344

4445
it('should return second entity when paginating forward with first cursor', async () => {
4546
await createTestEntities();
46-
const firstResult = await paginate(undefined, forward);
47+
const firstResult = await paginate(start, forward);
4748
const finalResult = await paginate(firstResult.nextCursor, forward);
4849
assert.deepEqual(finalResult.entities, [secondEntity]);
50+
assert.equal(finalResult.previousCursor, createCursorFromEntity(secondEntity, sort));
51+
assert.equal(finalResult.nextCursor, end);
4952
});
5053

51-
it('should return no entities when paginating forward with second cursor', async () => {
54+
it('should return no entities when paginating forward with end cursor', async () => {
5255
await createTestEntities();
53-
const firstResult = await paginate(undefined, forward);
54-
const secondResult = await paginate(firstResult.nextCursor, forward);
55-
const finalResult = await paginate(secondResult.nextCursor, forward);
56+
const finalResult = await paginate(end, forward);
5657
assert.deepEqual(finalResult.entities, []);
58+
assert.equal(finalResult.previousCursor, start);
59+
assert.equal(finalResult.nextCursor, end);
5760
});
5861

59-
it('should return second entity when paginating backward without cursor', async () => {
62+
it('should return second entity when paginating backward with start cursor', async () => {
6063
await createTestEntities();
61-
const finalResult = await paginate(undefined, backward);
64+
const finalResult = await paginate(start, backward);
6265
assert.deepEqual(finalResult.entities, [secondEntity]);
66+
assert.equal(finalResult.previousCursor, createCursorFromEntity(secondEntity, sort));
67+
assert.equal(finalResult.nextCursor, end);
6368
});
6469

6570
it('should return first entity when paginating backward with first cursor', async () => {
6671
await createTestEntities();
67-
const firstResult = await paginate(undefined, backward);
72+
const firstResult = await paginate(start, backward);
6873
const finalResult = await paginate(firstResult.previousCursor, backward);
6974
assert.deepEqual(finalResult.entities, [firstEntity]);
75+
assert.equal(finalResult.previousCursor, end);
76+
assert.equal(finalResult.nextCursor, createCursorFromEntity(firstEntity, sort));
7077
});
7178

72-
it('should return no entities when paginating backward with second cursor', async () => {
79+
it('should return no entities when paginating backward with end cursor', async () => {
7380
await createTestEntities();
74-
const firstResult = await paginate(undefined, backward);
75-
const secondResult = await paginate(firstResult.previousCursor, backward);
76-
const finalResult = await paginate(secondResult.previousCursor, backward);
81+
const finalResult = await paginate(end, backward);
7782
assert.deepEqual(finalResult.entities, []);
83+
assert.equal(finalResult.previousCursor, end);
84+
assert.equal(finalResult.nextCursor, start);
7885
});
7986
};

Diff for: src/types/Cursor.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1-
type Cursor = string | undefined;
1+
export const start = undefined;
2+
export const end = '';
3+
4+
type Cursor = string | typeof start | typeof end;
25

36
export default Cursor;

Diff for: src/utils/createCursorFromEntity/index.test.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import 'mocha'; // tslint:disable-line:no-import-side-effect
22
import * as assert from 'power-assert';
33
import { TestEntity, testEntity } from '../../tests/utils/testEntity';
4+
import { end } from '../../types/Cursor';
45
import { asc } from '../../types/SortOrder';
56
import createCursorFromEntity from './index';
67

78
describe('createCursorFromEntity', () => {
89
it('should return undefined when the entity is undefined', () => {
910
const actualResult = createCursorFromEntity<TestEntity>(undefined, { id: asc });
10-
assert.equal(actualResult, undefined);
11+
assert.equal(actualResult, end);
1112
});
1213

1314
it('should return the correct cursor when the entity is defined', () => {

Diff for: src/utils/createCursorFromEntity/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import * as btoa from 'btoa';
22
import { get, set } from 'lodash';
3-
import Cursor from '../../types/Cursor';
3+
import Cursor, { end } from '../../types/Cursor';
44
import Entity from '../../types/Entity';
55
import Sort from '../../types/Sort';
66

77
export default <E extends Entity>(entity: E | undefined, sort: Sort<E>): Cursor => {
88
if (entity === undefined) {
9-
return undefined;
9+
return end;
1010
}
1111
const sortKeys = Object.keys(sort);
1212
const cursorResult = sortKeys.reduce<Partial<E>>((result, sortKey) => {

Diff for: src/utils/createEndCursorResult/index.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Result } from '../../signatures/GetEntities';
2+
import { end, start } from '../../types/Cursor';
3+
import Entity from '../../types/Entity';
4+
import Pagination from '../../types/Pagination';
5+
import { forward } from '../../types/PaginationDirection';
6+
7+
export default <E extends Entity>(pagination: Pagination): Result<E> => {
8+
if (pagination.direction === forward) {
9+
return { entities: [], previousCursor: start, nextCursor: end };
10+
}
11+
return { entities: [], previousCursor: end, nextCursor: start };
12+
};

Diff for: src/utils/createGetEntitiesResult/index.ts

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { first, last } from 'lodash';
2+
import { Result } from '../../signatures/GetEntities';
3+
import { end, start } from '../../types/Cursor';
4+
import Entity from '../../types/Entity';
5+
import Pagination from '../../types/Pagination';
6+
import { backward, forward } from '../../types/PaginationDirection';
7+
import Sort from '../../types/Sort';
8+
import createCursorFromEntity from '../../utils/createCursorFromEntity';
9+
10+
export interface Opts<E extends Entity> {
11+
readonly entities: E[];
12+
readonly isEnd: boolean;
13+
readonly pagination: Pagination;
14+
readonly sort: Sort<E>;
15+
}
16+
17+
export default <E extends Entity>({ entities, isEnd, pagination, sort }: Opts<E>): Result<E> => {
18+
const nextCursor = createCursorFromEntity(last(entities), sort);
19+
const previousCursor = createCursorFromEntity(first(entities), sort);
20+
21+
if (isEnd && pagination.direction === forward) {
22+
return { entities, nextCursor: end, previousCursor };
23+
}
24+
if (isEnd && pagination.direction === backward) {
25+
return { entities, nextCursor, previousCursor: end };
26+
}
27+
28+
const isStart = pagination.cursor === start;
29+
if (isStart && pagination.direction === forward) {
30+
return { entities, nextCursor, previousCursor: end };
31+
}
32+
if (isStart && pagination.direction === backward) {
33+
return { entities, nextCursor: end, previousCursor };
34+
}
35+
36+
return { entities, nextCursor, previousCursor };
37+
};

Diff for: src/utils/createPaginationFilter/index.test.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import 'mocha'; // tslint:disable-line:no-import-side-effect
22
import * as assert from 'power-assert';
3+
import PaginationFilterError from '../../errors/PaginationFilterError';
34
import { TestEntity, testEntity } from '../../tests/utils/testEntity';
5+
import { end, start } from '../../types/Cursor';
46
import { Filter } from '../../types/Filter';
57
import Pagination from '../../types/Pagination';
68
import { backward, forward } from '../../types/PaginationDirection';
@@ -12,13 +14,23 @@ import createPaginationFilter from './index';
1214
describe('createCursorFromEntity', () => {
1315
const sort: Sort<TestEntity> = { id: asc, numberProp: desc };
1416

15-
it('should return empty filter when the cursor is undefined', () => {
16-
const pagination: Pagination = { cursor: undefined, direction: forward, limit: 1 };
17+
it('should return empty filter when the cursor is start', () => {
18+
const pagination: Pagination = { cursor: start, direction: forward, limit: 1 };
1719
const actualResult = createPaginationFilter<TestEntity>(pagination, sort);
1820
const expectedResult = {};
1921
assert.deepEqual(actualResult, expectedResult);
2022
});
2123

24+
it('should return empty filter when the cursor is end', () => {
25+
const pagination: Pagination = { cursor: end, direction: forward, limit: 1 };
26+
try {
27+
createPaginationFilter<TestEntity>(pagination, sort);
28+
assert.fail();
29+
} catch (err) {
30+
assert.ok(err instanceof PaginationFilterError);
31+
}
32+
});
33+
2234
it('should return the correct filter when the cursor is defined and going forward', () => {
2335
const cursor = createCursorFromEntity<TestEntity>(testEntity, sort);
2436
const pagination: Pagination = { cursor, direction: forward, limit: 1 };

Diff for: src/utils/createPaginationFilter/index.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import * as atob from 'atob';
22
import { get, mapValues } from 'lodash';
3+
import PaginationFilterError from '../../errors/PaginationFilterError';
4+
import { end, start } from '../../types/Cursor';
35
import Entity from '../../types/Entity';
46
// tslint:disable-next-line:no-unused
57
import Filter, { ConditionFilter, EntityFilter } from '../../types/Filter';
@@ -13,10 +15,14 @@ const xor = (conditionA: boolean, conditionB: boolean) => {
1315
};
1416

1517
export default <E extends Entity>(pagination: Pagination, sort: Sort<E>): Filter<E> => {
16-
if (pagination.cursor === undefined) {
18+
if (pagination.cursor === start) {
1719
return {};
1820
}
19-
const cursorObj = JSON.parse(atob(pagination.cursor));
21+
if (pagination.cursor === end) {
22+
throw new PaginationFilterError();
23+
}
24+
const cursor = pagination.cursor;
25+
const cursorObj = JSON.parse(atob(cursor));
2026
const filter = mapValues(cursorObj, (cursorValue, sortKey) => {
2127
const ascendingPagination = !xor(
2228
get(sort, sortKey) === asc,

0 commit comments

Comments
 (0)