Skip to content

Commit 7e6c11b

Browse files
committed
fix(ConnectionResolver): for compound indexes resolver may miss some records
1 parent 3e6e3ad commit 7e6c11b

File tree

3 files changed

+205
-64
lines changed

3 files changed

+205
-64
lines changed
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/* @flow */
2+
3+
import { prepareCursorQuery } from '../prepareConnectionResolver';
4+
5+
let rawQuery;
6+
7+
describe('prepareConnectionResolver', () => {
8+
describe('prepareCursorQuery()', () => {
9+
describe('single index', () => {
10+
const cursorData = { a: 10 };
11+
const indexKeys = Object.keys(cursorData);
12+
13+
it('asc order', () => {
14+
const indexData = { a: 1 };
15+
16+
// for beforeCursorQuery
17+
rawQuery = {};
18+
prepareCursorQuery(rawQuery, cursorData, indexKeys, indexData, '$lt', '$gt');
19+
expect(rawQuery).toEqual({ a: { $lt: 10 } });
20+
21+
// for afterCursorQuery
22+
rawQuery = {};
23+
prepareCursorQuery(rawQuery, cursorData, indexKeys, indexData, '$gt', '$lt');
24+
expect(rawQuery).toEqual({ a: { $gt: 10 } });
25+
});
26+
27+
it('desc order', () => {
28+
const indexData = { a: -1 };
29+
30+
// for beforeCursorQuery
31+
rawQuery = {};
32+
prepareCursorQuery(rawQuery, cursorData, indexKeys, indexData, '$lt', '$gt');
33+
expect(rawQuery).toEqual({ a: { $gt: 10 } });
34+
35+
// for afterCursorQuery
36+
rawQuery = {};
37+
prepareCursorQuery(rawQuery, cursorData, indexKeys, indexData, '$gt', '$lt');
38+
expect(rawQuery).toEqual({ a: { $lt: 10 } });
39+
});
40+
});
41+
42+
describe('compound index', () => {
43+
const cursorData = { a: 10, b: 100, c: 1000 };
44+
const indexKeys = Object.keys(cursorData);
45+
46+
it('asc order', () => {
47+
const indexData = { a: 1, b: -1, c: 1 };
48+
49+
// for beforeCursorQuery
50+
rawQuery = {};
51+
prepareCursorQuery(rawQuery, cursorData, indexKeys, indexData, '$lt', '$gt');
52+
expect(rawQuery).toEqual({
53+
$or: [
54+
{ a: 10, b: 100, c: { $lt: 1000 } },
55+
{ a: 10, b: { $gt: 100 } },
56+
{ a: { $lt: 10 } },
57+
],
58+
});
59+
60+
// for afterCursorQuery
61+
rawQuery = {};
62+
prepareCursorQuery(rawQuery, cursorData, indexKeys, indexData, '$gt', '$lt');
63+
expect(rawQuery).toEqual({
64+
$or: [
65+
{ a: 10, b: 100, c: { $gt: 1000 } },
66+
{ a: 10, b: { $lt: 100 } },
67+
{ a: { $gt: 10 } },
68+
],
69+
});
70+
});
71+
72+
it('desc order', () => {
73+
const indexData = { a: -1, b: 1, c: -1 };
74+
75+
// for beforeCursorQuery
76+
rawQuery = {};
77+
prepareCursorQuery(rawQuery, cursorData, indexKeys, indexData, '$lt', '$gt');
78+
expect(rawQuery).toEqual({
79+
$or: [
80+
{ a: 10, b: 100, c: { $gt: 1000 } },
81+
{ a: 10, b: { $lt: 100 } },
82+
{ a: { $gt: 10 } },
83+
],
84+
});
85+
86+
// for afterCursorQuery
87+
rawQuery = {};
88+
prepareCursorQuery(rawQuery, cursorData, indexKeys, indexData, '$gt', '$lt');
89+
expect(rawQuery).toEqual({
90+
$or: [
91+
{ a: 10, b: 100, c: { $lt: 1000 } },
92+
{ a: 10, b: { $gt: 100 } },
93+
{ a: { $lt: 10 } },
94+
],
95+
});
96+
});
97+
});
98+
});
99+
});

src/composeWithMongoose.js

Lines changed: 1 addition & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { MongooseModel } from 'mongoose';
88
import type { ConnectionSortMapOpts } from 'graphql-compose-connection';
99
import { convertModelToGraphQL } from './fieldsConverter';
1010
import * as resolvers from './resolvers';
11-
import { getUniqueIndexes, extendByReversedIndexes } from './utils/getIndexesFromModel';
11+
import { prepareConnectionResolver } from './prepareConnectionResolver';
1212
import type {
1313
FilterHelperArgsOpts,
1414
LimitHelperArgsOpts,
@@ -249,66 +249,3 @@ export function preparePaginationResolver(tc: TypeComposer, opts: PaginationReso
249249
...opts,
250250
});
251251
}
252-
253-
export function prepareConnectionResolver(
254-
model: MongooseModel,
255-
tc: TypeComposer,
256-
opts: ConnectionSortMapOpts
257-
) {
258-
try {
259-
require.resolve('graphql-compose-connection');
260-
} catch (e) {
261-
return;
262-
}
263-
const composeWithConnection = require('graphql-compose-connection').default;
264-
265-
const uniqueIndexes = extendByReversedIndexes(getUniqueIndexes(model), {
266-
reversedFirst: true,
267-
});
268-
const sortConfigs = {};
269-
uniqueIndexes.forEach(indexData => {
270-
const keys = Object.keys(indexData);
271-
let name = keys
272-
.join('__')
273-
.toUpperCase()
274-
.replace(/[^_a-zA-Z0-9]/i, '__');
275-
if (indexData[keys[0]] === 1) {
276-
name = `${name}_ASC`;
277-
} else if (indexData[keys[0]] === -1) {
278-
name = `${name}_DESC`;
279-
}
280-
sortConfigs[name] = {
281-
value: indexData,
282-
cursorFields: keys,
283-
beforeCursorQuery: (rawQuery, cursorData) => {
284-
keys.forEach(k => {
285-
if (!rawQuery[k]) rawQuery[k] = {};
286-
if (indexData[k] === 1) {
287-
rawQuery[k].$lt = cursorData[k];
288-
} else {
289-
rawQuery[k].$gt = cursorData[k];
290-
}
291-
});
292-
},
293-
afterCursorQuery: (rawQuery, cursorData) => {
294-
keys.forEach(k => {
295-
if (!rawQuery[k]) rawQuery[k] = {};
296-
if (indexData[k] === 1) {
297-
rawQuery[k].$gt = cursorData[k];
298-
} else {
299-
rawQuery[k].$lt = cursorData[k];
300-
}
301-
});
302-
},
303-
};
304-
});
305-
306-
composeWithConnection(tc, {
307-
findResolverName: 'findMany',
308-
countResolverName: 'count',
309-
sort: {
310-
...sortConfigs,
311-
...opts,
312-
},
313-
});
314-
}

src/prepareConnectionResolver.js

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/* @flow */
2+
/* eslint-disable no-use-before-define, no-param-reassign, global-require */
3+
4+
import type { MongooseModel } from 'mongoose';
5+
import type { ConnectionSortMapOpts } from 'graphql-compose-connection';
6+
import type { TypeComposer } from 'graphql-compose';
7+
import {
8+
getUniqueIndexes,
9+
extendByReversedIndexes,
10+
type IndexT,
11+
} from './utils/getIndexesFromModel';
12+
13+
export function prepareConnectionResolver(
14+
model: MongooseModel,
15+
tc: TypeComposer,
16+
opts: ConnectionSortMapOpts
17+
) {
18+
try {
19+
require.resolve('graphql-compose-connection');
20+
} catch (e) {
21+
return;
22+
}
23+
const composeWithConnection = require('graphql-compose-connection').default;
24+
25+
const uniqueIndexes = extendByReversedIndexes(getUniqueIndexes(model), {
26+
reversedFirst: true,
27+
});
28+
const sortConfigs = {};
29+
uniqueIndexes.forEach(indexData => {
30+
const keys = Object.keys(indexData);
31+
let name = keys
32+
.join('__')
33+
.toUpperCase()
34+
.replace(/[^_a-zA-Z0-9]/i, '__');
35+
if (indexData[keys[0]] === 1) {
36+
name = `${name}_ASC`;
37+
} else if (indexData[keys[0]] === -1) {
38+
name = `${name}_DESC`;
39+
}
40+
sortConfigs[name] = {
41+
value: indexData,
42+
cursorFields: keys,
43+
beforeCursorQuery: (rawQuery, cursorData) => {
44+
prepareCursorQuery(rawQuery, cursorData, keys, indexData, '$lt', '$gt');
45+
},
46+
afterCursorQuery: (rawQuery, cursorData) => {
47+
prepareCursorQuery(rawQuery, cursorData, keys, indexData, '$gt', '$lt');
48+
},
49+
};
50+
});
51+
52+
composeWithConnection(tc, {
53+
findResolverName: 'findMany',
54+
countResolverName: 'count',
55+
sort: {
56+
...sortConfigs,
57+
...opts,
58+
},
59+
});
60+
}
61+
62+
export function prepareCursorQuery(
63+
rawQuery: Object,
64+
cursorData: Object,
65+
indexKeys: Array<string>,
66+
indexData: IndexT,
67+
nextOper: '$gt' | '$lt',
68+
prevOper: '$lt' | '$gt'
69+
): void {
70+
if (indexKeys.length === 1) {
71+
// When single index { a: 1 }, then just add to one criteria to the query:
72+
// rawQuery.a = { $gt|$lt: cursorValue } - for next|prev record
73+
const k = indexKeys[0];
74+
if (!rawQuery[k]) rawQuery[k] = {};
75+
if (indexData[k] === 1) {
76+
rawQuery[k][nextOper] = cursorData[k];
77+
} else {
78+
rawQuery[k][prevOper] = cursorData[k];
79+
}
80+
} else {
81+
// When compound index {a: 1, b: -1, c: 1 } then we should add OR criterias to the query:
82+
// rawQuery.$or = [
83+
// { a: cursorValueA, b: cursorValueB, c: { $gt|$lt: cursorValueC } },
84+
// { a: cursorValueA, b: { $gt|$lt: cursorValueB } },
85+
// { a: { $gt|$lt: cursorValueA } },
86+
// ]
87+
const orCriteries = [];
88+
for (let i = indexKeys.length - 1; i >= 0; i--) {
89+
const criteria = {};
90+
indexKeys.forEach((k, ii) => {
91+
if (ii < i) {
92+
criteria[k] = cursorData[k];
93+
} else if (ii === i) {
94+
if (indexData[k] === 1) {
95+
criteria[k] = { [nextOper]: cursorData[k] };
96+
} else {
97+
criteria[k] = { [prevOper]: cursorData[k] };
98+
}
99+
}
100+
});
101+
orCriteries.push(criteria);
102+
}
103+
rawQuery.$or = orCriteries;
104+
}
105+
}

0 commit comments

Comments
 (0)