Skip to content

Commit cd5e8ee

Browse files
authored
Merge pull request #18 from Terminal-Systems/feature/support-nulls
Support nulls in knex pagination
2 parents 4caa6ec + b7faeee commit cd5e8ee

File tree

3 files changed

+124
-9
lines changed

3 files changed

+124
-9
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "apollo-cursor-pagination",
3-
"version": "0.4.5",
3+
"version": "0.5.0",
44
"description": "Relay's Connection implementation for Apollo Server GraphQL library",
55
"main": "dist/index.js",
66
"repository": "https://github.com/Terminal-Systems/apollo-cursor-pagination",

src/orm-connectors/knex/custom-pagination.js

+40-4
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,21 @@ const formatColumnIfAvailable = (column, formatColumnFn) => {
4343
return column;
4444
};
4545

46+
const getOpossiteComparator = (comparator) => {
47+
switch (comparator) {
48+
case '<':
49+
return '>=';
50+
case '>':
51+
return '<=';
52+
case '<=':
53+
return '>';
54+
case '>=':
55+
return '<';
56+
default:
57+
return '<>';
58+
}
59+
};
60+
4661
const buildRemoveNodesFromBeforeOrAfter = (beforeOrAfter) => {
4762
const getComparator = (orderDirection) => {
4863
if (beforeOrAfter === 'after') return orderDirection === 'asc' ? '<' : '>';
@@ -53,8 +68,9 @@ const buildRemoveNodesFromBeforeOrAfter = (beforeOrAfter) => {
5368
}) => {
5469
const data = getDataFromCursor(cursorOfInitialNode);
5570
const [id, columnValue] = data;
71+
5672
const initialValue = nodesAccessor.clone();
57-
const result = operateOverScalarOrArray(initialValue, orderColumn, (orderBy, index, prev) => {
73+
const executeFilterQuery = query => operateOverScalarOrArray(query, orderColumn, (orderBy, index, prev) => {
5874
let orderDirection;
5975
const values = columnValue;
6076
let currValue;
@@ -67,6 +83,7 @@ const buildRemoveNodesFromBeforeOrAfter = (beforeOrAfter) => {
6783
}
6884
const comparator = getComparator(orderDirection);
6985

86+
7087
if (index > 0) {
7188
const operation = (isAggregateFn && isAggregateFn(orderColumn[index - 1])) ? 'orHavingRaw' : 'orWhereRaw';
7289
const nested = prev[operation](
@@ -77,21 +94,40 @@ const buildRemoveNodesFromBeforeOrAfter = (beforeOrAfter) => {
7794
return nested;
7895
}
7996

80-
const operation = (isAggregateFn && isAggregateFn(orderBy)) ? 'havingRaw' : 'whereRaw';
97+
if (currValue === null || currValue === undefined) {
98+
return prev;
99+
}
81100

101+
const operation = (isAggregateFn && isAggregateFn(orderBy)) ? 'havingRaw' : 'whereRaw';
82102
return prev[operation](`(${formatColumnIfAvailable(orderBy, formatColumnFn)} ${comparator} ?)`, [currValue]);
83103
}, (prev, isArray) => {
84104
// Result is sorted by id as the last column
85105
const comparator = getComparator(ascOrDesc);
86106
const lastOrderColumn = isArray ? orderColumn.pop() : orderColumn;
87107
const lastValue = columnValue.pop();
108+
109+
// If value is null, we are forced to filter by id instead
88110
const operation = (isAggregateFn && isAggregateFn(lastOrderColumn)) ? 'orHavingRaw' : 'orWhereRaw';
89-
const nested = prev[operation](
111+
if (lastValue === null || lastValue === undefined) {
112+
return prev[operation](
113+
`(${formatColumnIfAvailable('id', formatColumnFn)} ${comparator} ?) or (${formatColumnIfAvailable(lastOrderColumn, formatColumnFn)} IS NOT NULL)`,
114+
[id],
115+
);
116+
}
117+
118+
return prev[operation](
90119
`(${formatColumnIfAvailable(lastOrderColumn, formatColumnFn)} = ? and ${formatColumnIfAvailable('id', formatColumnFn)} ${comparator} ?)`,
91120
[lastValue, id],
92121
);
93-
return nested;
94122
});
123+
let result;
124+
125+
if ((isAggregateFn && Array.isArray(orderColumn) && isAggregateFn(orderColumn[0]))
126+
|| (isAggregateFn && !Array.isArray(orderColumn) && isAggregateFn(orderColumn))) {
127+
result = executeFilterQuery(initialValue);
128+
} else {
129+
result = initialValue.andWhere(query => executeFilterQuery(query));
130+
}
95131
return result;
96132
};
97133
};

tests/test-app/tests/apollo-cursor-pagination/knex-implementation.test.js

+83-4
Original file line numberDiff line numberDiff line change
@@ -581,12 +581,18 @@ describe('getCatsByOwner root query', () => {
581581
cursor = response.body.data.catsConnection.edges[0].cursor;
582582
});
583583

584-
it('brings the correct amount for a segmented query', async () => {
584+
it('paginates segmentating by null values', async () => {
585+
const [unnamedCat] = await catFactory.model.query().limit(1);
586+
await catFactory.model.query().where({ id: unnamedCat.id }).patch({ lastName: null });
585587
const query = `
586588
{
587-
catsConnection(first: 2, after: "${cursor}") {
589+
catsConnection(first: 1, orderBy: "lastName", orderDirection: asc) {
588590
edges {
589591
cursor
592+
node {
593+
id
594+
lastName
595+
}
590596
}
591597
totalCount
592598
}
@@ -595,8 +601,81 @@ describe('getCatsByOwner root query', () => {
595601
const response = await graphqlQuery(app, query);
596602

597603
expect(response.body.errors).not.toBeDefined();
598-
expect(response.body.data.catsConnection.totalCount).toBeDefined();
599-
expect(response.body.data.catsConnection.totalCount).toEqual(3);
604+
// In SQLITE nulls come first.
605+
expect(response.body.data.catsConnection.edges[0].node.lastName).toEqual(null);
606+
607+
const { cursor } = response.body.data.catsConnection.edges[0];
608+
609+
const query2 = `
610+
{
611+
catsConnection(
612+
first: 2, orderBy: "lastName", orderDirection: asc, after: "${cursor}"
613+
) {
614+
edges {
615+
cursor
616+
node {
617+
id
618+
lastName
619+
}
620+
}
621+
}
622+
}
623+
`;
624+
const response2 = await graphqlQuery(app, query2);
625+
626+
expect(response2.body.errors).not.toBeDefined();
627+
expect(response2.body.data.catsConnection.edges).toHaveLength(2);
628+
expect(response2.body.data.catsConnection.edges[0].node.lastName).not.toEqual(null);
629+
expect(response2.body.data.catsConnection.edges[1].node.lastName).not.toEqual(null);
630+
});
631+
632+
it('paginates segmentating in the middle of null values', async () => {
633+
const [unnamedCat1, unnamedCat2] = await catFactory.model.query().limit(2);
634+
await catFactory.model.query().where({ id: unnamedCat1.id }).patch({ lastName: null });
635+
await catFactory.model.query().where({ id: unnamedCat2.id }).patch({ lastName: null });
636+
const query = `
637+
{
638+
catsConnection(first: 1, orderBy: "lastName", orderDirection: asc) {
639+
edges {
640+
cursor
641+
node {
642+
id
643+
lastName
644+
}
645+
}
646+
totalCount
647+
}
648+
}
649+
`;
650+
const response = await graphqlQuery(app, query);
651+
652+
expect(response.body.errors).not.toEqual(null);
653+
// In SQLITE nulls come first.
654+
expect(response.body.data.catsConnection.edges[0].node.lastName).toEqual(null);
655+
656+
const { cursor } = response.body.data.catsConnection.edges[0];
657+
658+
const query2 = `
659+
{
660+
catsConnection(
661+
first: 2, orderBy: "lastName", orderDirection: asc, after: "${cursor}"
662+
) {
663+
edges {
664+
cursor
665+
node {
666+
id
667+
lastName
668+
}
669+
}
670+
}
671+
}
672+
`;
673+
const response2 = await graphqlQuery(app, query2);
674+
675+
expect(response2.body.errors).not.toEqual(null);
676+
expect(response2.body.data.catsConnection.edges).toHaveLength(2);
677+
expect(response2.body.data.catsConnection.edges[0].node.lastName).toEqual(null);
678+
expect(response2.body.data.catsConnection.edges[1].node.lastName).not.toEqual(null);
600679
});
601680
});
602681
});

0 commit comments

Comments
 (0)