Skip to content

Commit 4641eb6

Browse files
brad-deckermickhansen
authored andcommitted
Cursor Reconfigure For Order (#165)
Signed-off-by: brad-decker <[email protected]> removed defaultOrderBy for lint Signed-off-by: brad-decker <[email protected]> make graphql-sequelize useable from github Signed-off-by: brad-decker <[email protected]> build should use internal version of babel Signed-off-by: brad-decker <[email protected]> use internal node_modules version take 2 Signed-off-by: brad-decker <[email protected]> Remove postinstall, add lib folder temporarily Signed-off-by: brad-decker <[email protected]> Cursor Reconfigure For Order Signed-off-by: brad-decker <[email protected]> make graphql-sequelize useable from github Signed-off-by: brad-decker <[email protected]> build should use internal version of babel Signed-off-by: brad-decker <[email protected]> use internal node_modules version take 2 Signed-off-by: brad-decker <[email protected]> Remove postinstall, add lib folder temporarily Signed-off-by: brad-decker <[email protected]> Cursor Reconfigure For Order Signed-off-by: brad-decker <[email protected]> make graphql-sequelize useable from github Signed-off-by: brad-decker <[email protected]> build should use internal version of babel Signed-off-by: brad-decker <[email protected]> use internal node_modules version take 2 Signed-off-by: brad-decker <[email protected]> Remove postinstall, add lib folder temporarily Signed-off-by: brad-decker <[email protected]> Cursor Reconfigure For Order Signed-off-by: brad-decker <[email protected]> make graphql-sequelize useable from github Signed-off-by: brad-decker <[email protected]> build should use internal version of babel Signed-off-by: brad-decker <[email protected]> use internal node_modules version take 2 Signed-off-by: brad-decker <[email protected]> Remove postinstall, add lib folder temporarily Signed-off-by: brad-decker <[email protected]> Weird conflict issue, fixing. Signed-off-by: brad-decker <[email protected]> add one to startIndex if its not 0 to account for 0 indexed cursor Signed-off-by: brad-decker <[email protected]> update tests to match results Signed-off-by: brad-decker <[email protected]> fixing test Signed-off-by: brad-decker <[email protected]> Cursor Reconfigure For Order Signed-off-by: brad-decker <[email protected]> removed defaultOrderBy for lint Signed-off-by: brad-decker <[email protected]> make graphql-sequelize useable from github Signed-off-by: brad-decker <[email protected]> build should use internal version of babel Signed-off-by: brad-decker <[email protected]> use internal node_modules version take 2 Signed-off-by: brad-decker <[email protected]> Remove postinstall, add lib folder temporarily Signed-off-by: brad-decker <[email protected]> Cursor Reconfigure For Order Signed-off-by: brad-decker <[email protected]> make graphql-sequelize useable from github Signed-off-by: brad-decker <[email protected]> build should use internal version of babel Signed-off-by: brad-decker <[email protected]> use internal node_modules version take 2 Signed-off-by: brad-decker <[email protected]> Remove postinstall, add lib folder temporarily Signed-off-by: brad-decker <[email protected]> Cursor Reconfigure For Order Signed-off-by: brad-decker <[email protected]> removed defaultOrderBy for lint Signed-off-by: brad-decker <[email protected]> make graphql-sequelize useable from github Signed-off-by: brad-decker <[email protected]> build should use internal version of babel Signed-off-by: brad-decker <[email protected]> use internal node_modules version take 2 Signed-off-by: brad-decker <[email protected]> Remove postinstall, add lib folder temporarily Signed-off-by: brad-decker <[email protected]> Cursor Reconfigure For Order Signed-off-by: brad-decker <[email protected]> removed defaultOrderBy for lint Signed-off-by: brad-decker <[email protected]> make graphql-sequelize useable from github Signed-off-by: brad-decker <[email protected]> build should use internal version of babel Signed-off-by: brad-decker <[email protected]> use internal node_modules version take 2 Signed-off-by: brad-decker <[email protected]> Remove postinstall, add lib folder temporarily Signed-off-by: brad-decker <[email protected]> Weird conflict issue, fixing. Signed-off-by: brad-decker <[email protected]> add one to startIndex if its not 0 to account for 0 indexed cursor Signed-off-by: brad-decker <[email protected]> update tests to match results Signed-off-by: brad-decker <[email protected]> removed lib Signed-off-by: brad-decker <[email protected]> Cleanup, fixed tests Signed-off-by: brad-decker <[email protected]> Making requested Changes Signed-off-by: brad-decker <[email protected]> renaming cursor to queriedCursor Signed-off-by: brad-decker <[email protected]> rebase messed up some tests. replacing them Signed-off-by: brad-decker <[email protected]> fixing one more test. Signed-off-by: brad-decker <[email protected]>
1 parent b41f16e commit 4641eb6

File tree

2 files changed

+146
-58
lines changed

2 files changed

+146
-58
lines changed

src/relay.js

Lines changed: 40 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,6 @@ export function sequelizeConnection({name, nodeType, target, orderBy: orderByEnu
128128
});
129129
}
130130

131-
let defaultOrderBy = orderByEnum._values[0].value;
132-
133131
before = before || ((options) => options);
134132

135133
let $connectionArgs = {
@@ -143,20 +141,30 @@ export function sequelizeConnection({name, nodeType, target, orderBy: orderByEnu
143141
return orderBy[0][0];
144142
};
145143

146-
let toCursor = function (value, orderBy) {
147-
let id = value.get(model.primaryKeyAttribute);
148-
let orderValue = value.get(orderByAttribute(orderBy));
149-
return base64(PREFIX + id + SEPERATOR + orderValue);
144+
/**
145+
* Creates a cursor given a item returned from the Database
146+
* @param {Object} item sequelize model instance
147+
* @param {Integer} index the index of this item within the results, 0 indexed
148+
* @return {String} The Base64 encoded cursor string
149+
*/
150+
let toCursor = function (item, index) {
151+
let id = item.get(model.primaryKeyAttribute);
152+
return base64(PREFIX + id + SEPERATOR + index);
150153
};
151154

155+
/**
156+
* Decode a cursor into its component parts
157+
* @param {String} cursor Base64 encoded cursor
158+
* @return {Object} Object containing ID and index
159+
*/
152160
let fromCursor = function (cursor) {
153161
cursor = unbase64(cursor);
154162
cursor = cursor.substring(PREFIX.length, cursor.length);
155-
let [id, orderValue] = cursor.split(SEPERATOR);
163+
let [id, index] = cursor.split(SEPERATOR);
156164

157165
return {
158166
id,
159-
orderValue
167+
index
160168
};
161169
};
162170

@@ -171,13 +179,12 @@ export function sequelizeConnection({name, nodeType, target, orderBy: orderByEnu
171179
return result;
172180
};
173181

174-
let resolveEdge = function (item, args = {}, source) {
175-
if (!args.orderBy) {
176-
args.orderBy = [defaultOrderBy];
177-
}
178-
182+
let resolveEdge = function (item, index, queriedCursor, args = {}, source) {
183+
let startIndex = 0;
184+
if (queriedCursor) startIndex = Number(queriedCursor.index);
185+
if (startIndex !== 0) startIndex++;
179186
return {
180-
cursor: toCursor(item, args.orderBy),
187+
cursor: toCursor(item, index + startIndex),
181188
node: item,
182189
source: source
183190
};
@@ -187,11 +194,10 @@ export function sequelizeConnection({name, nodeType, target, orderBy: orderByEnu
187194
handleConnection: false,
188195
include: true,
189196
list: true,
190-
before: function (options, args, context, info) {
197+
before: function (options, args, context) {
191198
if (args.first || args.last) {
192199
options.limit = parseInt(args.first || args.last, 10);
193200
}
194-
195201
if (!args.orderBy) {
196202
args.orderBy = [orderByEnum._values[0].value];
197203
} else if (typeof args.orderBy === 'string') {
@@ -233,43 +239,22 @@ export function sequelizeConnection({name, nodeType, target, orderBy: orderByEnu
233239

234240
if (args.after || args.before) {
235241
let cursor = fromCursor(args.after || args.before);
236-
let orderValue = cursor.orderValue;
242+
let startIndex = Number(cursor.index);
237243

238-
if (model.rawAttributes[orderAttribute].type instanceof model.sequelize.constructor.DATE) {
239-
orderValue = new Date(orderValue);
240-
}
241-
242-
let slicingWhere = {
243-
$or: [
244-
{
245-
[orderAttribute]: {
246-
[orderDirection === 'ASC' ? '$gt' : '$lt']: orderValue
247-
}
248-
},
249-
{
250-
[orderAttribute]: {
251-
$eq: orderValue
252-
},
253-
[model.primaryKeyAttribute]: {
254-
$gt: cursor.id
255-
}
256-
}
257-
]
258-
};
259-
260-
// TODO, do a proper merge that won't kill another $or
261-
_.assign(options.where, slicingWhere);
244+
if (startIndex > 0) options.offset = startIndex + 1;
262245
}
263-
264-
// apply uniq to the attributes
265246
options.attributes = _.uniq(options.attributes);
247+
return before(options, args, root, context);
248+
},
249+
after: function (values, args, root, {source}) {
250+
var cursor = null;
266251

252+
if (args.after || args.before) {
253+
cursor = fromCursor(args.after || args.before);
254+
}
267255

268-
return before(options, args, context, info);
269-
},
270-
after: function (values, args, context, {source}) {
271-
let edges = values.map((value) => {
272-
return resolveEdge(value, args, source);
256+
let edges = values.map((value, idx) => {
257+
return resolveEdge(value, idx, cursor, args, source);
273258
});
274259

275260
let firstEdge = edges[0];
@@ -282,6 +267,11 @@ export function sequelizeConnection({name, nodeType, target, orderBy: orderByEnu
282267
if (model.sequelize.dialect.name === 'postgres' && (args.first || args.last)) {
283268
if (fullCount === null || fullCount === undefined) throw new Error('No fullcount available');
284269
}
270+
let hasMorePages = false;
271+
if (args.first || args.last) {
272+
let index = cursor ? Number(cursor.index) : 0;
273+
hasMorePages = index + 1 + parseInt(args.first || args.last, 10) < fullCount;
274+
}
285275

286276
return {
287277
source,
@@ -291,8 +281,8 @@ export function sequelizeConnection({name, nodeType, target, orderBy: orderByEnu
291281
pageInfo: {
292282
startCursor: firstEdge ? firstEdge.cursor : null,
293283
endCursor: lastEdge ? lastEdge.cursor : null,
294-
hasPreviousPage: args.last !== null && args.last !== undefined ? fullCount > parseInt(args.last, 10) : false,
295-
hasNextPage: args.first !== null && args.first !== undefined ? fullCount > parseInt(args.first, 10) : false,
284+
hasPreviousPage: hasMorePages,
285+
hasNextPage: hasMorePages
296286
}
297287
};
298288
}

test/integration/relay/connection.test.js

Lines changed: 106 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import attributeFields from '../../../src/attributeFields';
88
import resolver from '../../../src/resolver';
99
import {uniq} from 'lodash';
1010

11+
1112
const {
1213
sequelize,
1314
Promise
@@ -18,13 +19,10 @@ import {
1819
} from '../../../src/relay';
1920

2021
import {
21-
GraphQLString,
2222
GraphQLInt,
23-
GraphQLFloat,
2423
GraphQLNonNull,
2524
GraphQLBoolean,
2625
GraphQLEnumType,
27-
GraphQLList,
2826
GraphQLObjectType,
2927
GraphQLSchema,
3028
graphql
@@ -121,9 +119,19 @@ if (helper.sequelize.dialect.name === 'postgres') {
121119
values: {
122120
ID: {value: [this.Task.primaryKeyAttribute, 'ASC']},
123121
LATEST: {value: ['createdAt', 'DESC']},
122+
CUSTOM: {value: ['updatedAt', 'DESC']},
124123
NAME: {value: ['name', 'ASC']}
125124
}
126125
}),
126+
before: (options) => {
127+
if (options.order[0][0] === 'updatedAt') {
128+
options.order = Sequelize.literal(`
129+
CASE
130+
WHEN completed = true THEN "createdAt"
131+
ELSE "updatedAt" End ASC`);
132+
}
133+
return options;
134+
},
127135
connectionFields: () => ({
128136
totalCount: {
129137
type: GraphQLInt,
@@ -193,12 +201,12 @@ if (helper.sequelize.dialect.name === 'postgres') {
193201
orderBy: new GraphQLEnumType({
194202
name: 'Viewer' + this.Task.name + 'ConnectionOrder',
195203
values: {
196-
ID: {value: [this.Task.primaryKeyAttribute, 'ASC']},
204+
ID: {value: [this.Task.primaryKeyAttribute, 'ASC']}
197205
}
198206
}),
199-
before: (options, args, root) => {
207+
before: (options, args, context, {viewer}) => {
200208
options.where = options.where || {};
201-
options.where.userId = root.viewer.get('id');
209+
options.where.userId = viewer.get('id');
202210
return options;
203211
}
204212
});
@@ -340,7 +348,7 @@ if (helper.sequelize.dialect.name === 'postgres') {
340348
name: 'userProject',
341349
nodeType: this.projectType,
342350
target: this.User.Projects,
343-
before(options){
351+
before(options) {
344352
// compare a uniq set of attributes against what is returned by the sequelizeConnection resolver
345353
let getUnique = uniq(options.attributes);
346354
projectConnectionAttributesUnique = getUnique.length === options.attributes.length;
@@ -384,7 +392,7 @@ if (helper.sequelize.dialect.name === 'postgres') {
384392
})
385393
});
386394

387-
let result = await graphql(schema, `
395+
await graphql(schema, `
388396
{
389397
user(id: ${this.userA.id}) {
390398
projects {
@@ -476,6 +484,96 @@ if (helper.sequelize.dialect.name === 'postgres') {
476484
expect(lastResult.data.user.tasks.pageInfo.hasNextPage).to.equal(false);
477485
});
478486

487+
it('should support in-query slicing and pagination with first and CUSTOM orderBy', async function () {
488+
const correctOrder = await graphql(this.schema, `
489+
{
490+
user(id: ${this.userA.id}) {
491+
tasks(first: 9, orderBy: CUSTOM) {
492+
edges {
493+
cursor
494+
node {
495+
id
496+
name
497+
}
498+
}
499+
pageInfo {
500+
hasNextPage
501+
endCursor
502+
}
503+
}
504+
}
505+
}
506+
`);
507+
const reordered = correctOrder.data.user.tasks.edges.map(({node}) => {
508+
const targetId = fromGlobalId(node.id).id;
509+
return this.userA.tasks.find(task => {
510+
return task.id === Number(targetId);
511+
});
512+
});
513+
514+
let lastThree = reordered.slice(this.userA.tasks.length - 3, this.userA.tasks.length);
515+
let nextThree = reordered.slice(this.userA.tasks.length - 6, this.userA.tasks.length - 3);
516+
let firstThree = reordered.slice(this.userA.tasks.length - 9, this.userA.tasks.length - 6);
517+
518+
expect(firstThree.length).to.equal(3);
519+
expect(nextThree.length).to.equal(3);
520+
expect(lastThree.length).to.equal(3);
521+
522+
523+
let verify = function (result, expectedTasks) {
524+
if (result.errors) throw new Error(result.errors[0].stack);
525+
526+
var resultTasks = result.data.user.tasks.edges.map(function (edge) {
527+
return edge.node;
528+
});
529+
530+
let resultIds = resultTasks.map((task) => {
531+
return parseInt(fromGlobalId(task.id).id, 10);
532+
}).sort();
533+
534+
let expectedIds = expectedTasks.map(function (task) {
535+
return task.get('id');
536+
}).sort();
537+
538+
expect(resultTasks.length).to.equal(3);
539+
expect(resultIds).to.deep.equal(expectedIds);
540+
};
541+
542+
let query = (after) => {
543+
return graphql(this.schema, `
544+
{
545+
user(id: ${this.userA.id}) {
546+
tasks(first: 3, ${after ? 'after: "' + after + '", ' : ''} orderBy: CUSTOM) {
547+
edges {
548+
cursor
549+
node {
550+
id
551+
name
552+
}
553+
}
554+
pageInfo {
555+
hasNextPage
556+
endCursor
557+
}
558+
}
559+
}
560+
}
561+
`);
562+
};
563+
564+
let firstResult = await query();
565+
verify(firstResult, firstThree);
566+
expect(firstResult.data.user.tasks.pageInfo.hasNextPage).to.equal(true);
567+
568+
let nextResult = await query(firstResult.data.user.tasks.pageInfo.endCursor);
569+
verify(nextResult, nextThree);
570+
expect(nextResult.data.user.tasks.pageInfo.hasNextPage).to.equal(true);
571+
572+
let lastResult = await query(nextResult.data.user.tasks.edges[2].cursor);
573+
verify(lastResult, lastThree);
574+
expect(lastResult.data.user.tasks.pageInfo.hasNextPage).to.equal(false);
575+
});
576+
479577
it('should support in-query slicing with user provided args/where', async function () {
480578
let result = await graphql(this.schema, `
481579
{

0 commit comments

Comments
 (0)