Skip to content

Commit

Permalink
Merge pull request #18 from Terminal-Systems/feature/support-nulls
Browse files Browse the repository at this point in the history
Support nulls in knex pagination
  • Loading branch information
dmerrill6 authored Aug 12, 2019
2 parents 4caa6ec + b7faeee commit cd5e8ee
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 9 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "apollo-cursor-pagination",
"version": "0.4.5",
"version": "0.5.0",
"description": "Relay's Connection implementation for Apollo Server GraphQL library",
"main": "dist/index.js",
"repository": "https://github.com/Terminal-Systems/apollo-cursor-pagination",
Expand Down
44 changes: 40 additions & 4 deletions src/orm-connectors/knex/custom-pagination.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@ const formatColumnIfAvailable = (column, formatColumnFn) => {
return column;
};

const getOpossiteComparator = (comparator) => {
switch (comparator) {
case '<':
return '>=';
case '>':
return '<=';
case '<=':
return '>';
case '>=':
return '<';
default:
return '<>';
}
};

const buildRemoveNodesFromBeforeOrAfter = (beforeOrAfter) => {
const getComparator = (orderDirection) => {
if (beforeOrAfter === 'after') return orderDirection === 'asc' ? '<' : '>';
Expand All @@ -53,8 +68,9 @@ const buildRemoveNodesFromBeforeOrAfter = (beforeOrAfter) => {
}) => {
const data = getDataFromCursor(cursorOfInitialNode);
const [id, columnValue] = data;

const initialValue = nodesAccessor.clone();
const result = operateOverScalarOrArray(initialValue, orderColumn, (orderBy, index, prev) => {
const executeFilterQuery = query => operateOverScalarOrArray(query, orderColumn, (orderBy, index, prev) => {
let orderDirection;
const values = columnValue;
let currValue;
Expand All @@ -67,6 +83,7 @@ const buildRemoveNodesFromBeforeOrAfter = (beforeOrAfter) => {
}
const comparator = getComparator(orderDirection);


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

const operation = (isAggregateFn && isAggregateFn(orderBy)) ? 'havingRaw' : 'whereRaw';
if (currValue === null || currValue === undefined) {
return prev;
}

const operation = (isAggregateFn && isAggregateFn(orderBy)) ? 'havingRaw' : 'whereRaw';
return prev[operation](`(${formatColumnIfAvailable(orderBy, formatColumnFn)} ${comparator} ?)`, [currValue]);
}, (prev, isArray) => {
// Result is sorted by id as the last column
const comparator = getComparator(ascOrDesc);
const lastOrderColumn = isArray ? orderColumn.pop() : orderColumn;
const lastValue = columnValue.pop();

// If value is null, we are forced to filter by id instead
const operation = (isAggregateFn && isAggregateFn(lastOrderColumn)) ? 'orHavingRaw' : 'orWhereRaw';
const nested = prev[operation](
if (lastValue === null || lastValue === undefined) {
return prev[operation](
`(${formatColumnIfAvailable('id', formatColumnFn)} ${comparator} ?) or (${formatColumnIfAvailable(lastOrderColumn, formatColumnFn)} IS NOT NULL)`,
[id],
);
}

return prev[operation](
`(${formatColumnIfAvailable(lastOrderColumn, formatColumnFn)} = ? and ${formatColumnIfAvailable('id', formatColumnFn)} ${comparator} ?)`,
[lastValue, id],
);
return nested;
});
let result;

if ((isAggregateFn && Array.isArray(orderColumn) && isAggregateFn(orderColumn[0]))
|| (isAggregateFn && !Array.isArray(orderColumn) && isAggregateFn(orderColumn))) {
result = executeFilterQuery(initialValue);
} else {
result = initialValue.andWhere(query => executeFilterQuery(query));
}
return result;
};
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -581,12 +581,18 @@ describe('getCatsByOwner root query', () => {
cursor = response.body.data.catsConnection.edges[0].cursor;
});

it('brings the correct amount for a segmented query', async () => {
it('paginates segmentating by null values', async () => {
const [unnamedCat] = await catFactory.model.query().limit(1);
await catFactory.model.query().where({ id: unnamedCat.id }).patch({ lastName: null });
const query = `
{
catsConnection(first: 2, after: "${cursor}") {
catsConnection(first: 1, orderBy: "lastName", orderDirection: asc) {
edges {
cursor
node {
id
lastName
}
}
totalCount
}
Expand All @@ -595,8 +601,81 @@ describe('getCatsByOwner root query', () => {
const response = await graphqlQuery(app, query);

expect(response.body.errors).not.toBeDefined();
expect(response.body.data.catsConnection.totalCount).toBeDefined();
expect(response.body.data.catsConnection.totalCount).toEqual(3);
// In SQLITE nulls come first.
expect(response.body.data.catsConnection.edges[0].node.lastName).toEqual(null);

const { cursor } = response.body.data.catsConnection.edges[0];

const query2 = `
{
catsConnection(
first: 2, orderBy: "lastName", orderDirection: asc, after: "${cursor}"
) {
edges {
cursor
node {
id
lastName
}
}
}
}
`;
const response2 = await graphqlQuery(app, query2);

expect(response2.body.errors).not.toBeDefined();
expect(response2.body.data.catsConnection.edges).toHaveLength(2);
expect(response2.body.data.catsConnection.edges[0].node.lastName).not.toEqual(null);
expect(response2.body.data.catsConnection.edges[1].node.lastName).not.toEqual(null);
});

it('paginates segmentating in the middle of null values', async () => {
const [unnamedCat1, unnamedCat2] = await catFactory.model.query().limit(2);
await catFactory.model.query().where({ id: unnamedCat1.id }).patch({ lastName: null });
await catFactory.model.query().where({ id: unnamedCat2.id }).patch({ lastName: null });
const query = `
{
catsConnection(first: 1, orderBy: "lastName", orderDirection: asc) {
edges {
cursor
node {
id
lastName
}
}
totalCount
}
}
`;
const response = await graphqlQuery(app, query);

expect(response.body.errors).not.toEqual(null);
// In SQLITE nulls come first.
expect(response.body.data.catsConnection.edges[0].node.lastName).toEqual(null);

const { cursor } = response.body.data.catsConnection.edges[0];

const query2 = `
{
catsConnection(
first: 2, orderBy: "lastName", orderDirection: asc, after: "${cursor}"
) {
edges {
cursor
node {
id
lastName
}
}
}
}
`;
const response2 = await graphqlQuery(app, query2);

expect(response2.body.errors).not.toEqual(null);
expect(response2.body.data.catsConnection.edges).toHaveLength(2);
expect(response2.body.data.catsConnection.edges[0].node.lastName).toEqual(null);
expect(response2.body.data.catsConnection.edges[1].node.lastName).not.toEqual(null);
});
});
});

0 comments on commit cd5e8ee

Please sign in to comment.