Skip to content

Commit c55e141

Browse files
committed
feat(ConnectionResolver): Add unique indexed fields to the connection sort argument
1 parent 1004c20 commit c55e141

File tree

6 files changed

+187
-66
lines changed

6 files changed

+187
-66
lines changed

src/composeWithMongoose.js

+43-33
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import { TypeComposer, InputTypeComposer } from 'graphql-compose';
55
import composeWithConnection from 'graphql-compose-connection';
66
import { convertModelToGraphQL } from './fieldsConverter';
77
import * as resolvers from './resolvers';
8+
import {
9+
getUniqueIndexes,
10+
extendByReversedIndexes,
11+
} from './utils/getIndexesFromModel';
812

913
import type {
1014
MongooseModelT,
@@ -125,50 +129,56 @@ export function createResolvers(
125129
});
126130

127131
if (!{}.hasOwnProperty.call(opts, 'connection') || opts.connection !== false) {
128-
prepareConnectionResolver(typeComposer, opts.connection ? opts.connection : {});
132+
prepareConnectionResolver(model, typeComposer, opts.connection ? opts.connection : {});
129133
}
130134
}
131135

132136
export function prepareConnectionResolver(
137+
model: MongooseModelT,
133138
typeComposer: TypeComposer,
134139
opts: connectionSortMapOpts
135140
) {
141+
const uniqueIndexes = extendByReversedIndexes(getUniqueIndexes(model), { reversedFirst: true });
142+
const sortConfigs = {};
143+
uniqueIndexes.forEach((indexData) => {
144+
const keys = Object.keys(indexData);
145+
let name = keys.join('__').toUpperCase().replace(/[^_a-zA-Z0-9]/i, '__');
146+
if (indexData[keys[0]] === 1) {
147+
name = `${name}_ASC`;
148+
} else if (indexData[keys[0]] === -1) {
149+
name = `${name}_DESC`;
150+
}
151+
sortConfigs[name] = {
152+
value: indexData,
153+
cursorFields: keys,
154+
beforeCursorQuery: (rawQuery, cursorData) => {
155+
keys.forEach((k) => {
156+
if (!rawQuery[k]) rawQuery[k] = {};
157+
if (indexData[k] === 1) {
158+
rawQuery[k].$lt = cursorData[k];
159+
} else {
160+
rawQuery[k].$gt = cursorData[k];
161+
}
162+
});
163+
},
164+
afterCursorQuery: (rawQuery, cursorData) => {
165+
keys.forEach((k) => {
166+
if (!rawQuery[k]) rawQuery[k] = {};
167+
if (indexData[k] === 1) {
168+
rawQuery[k].$gt = cursorData[k];
169+
} else {
170+
rawQuery[k].$lt = cursorData[k];
171+
}
172+
});
173+
},
174+
};
175+
});
176+
136177
composeWithConnection(typeComposer, {
137178
findResolverName: 'findMany',
138179
countResolverName: 'count',
139180
sort: {
140-
_ID_DESC: {
141-
value: { _id: -1 },
142-
cursorFields: ['_id'],
143-
beforeCursorQuery: (rawQuery, cursorData) => {
144-
// $FlowFixMe
145-
if (!rawQuery._id) rawQuery._id = {};
146-
// $FlowFixMe
147-
rawQuery._id.$gt = cursorData._id;
148-
},
149-
afterCursorQuery: (rawQuery, cursorData) => {
150-
// $FlowFixMe
151-
if (!rawQuery._id) rawQuery._id = {};
152-
// $FlowFixMe
153-
rawQuery._id.$lt = cursorData._id;
154-
},
155-
},
156-
_ID_ASC: {
157-
value: { _id: 1 },
158-
cursorFields: ['_id'],
159-
beforeCursorQuery: (rawQuery, cursorData) => {
160-
// $FlowFixMe
161-
if (!rawQuery._id) rawQuery._id = {};
162-
// $FlowFixMe
163-
rawQuery._id.$gt = cursorData._id;
164-
},
165-
afterCursorQuery: (rawQuery, cursorData) => {
166-
// $FlowFixMe
167-
if (!rawQuery._id) rawQuery._id = {};
168-
// $FlowFixMe
169-
rawQuery._id.$lt = cursorData._id;
170-
},
171-
},
181+
...sortConfigs,
172182
...opts,
173183
},
174184
});

src/resolvers/helpers/__tests__/sort-test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
getSortTypeFromModel,
99
} from '../sort';
1010
import { UserModel } from '../../../__mocks__/userModel';
11-
import getIndexesFromModel from '../../../utils/getIndexesFromModel';
11+
import { getIndexesFromModel } from '../../../utils/getIndexesFromModel';
1212
import typeStorage from '../../../typeStorage';
1313

1414
describe('Resolver helper `sort` ->', () => {

src/resolvers/helpers/filter.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
getNamedType,
99
} from 'graphql';
1010
import { TypeComposer, InputTypeComposer } from 'graphql-compose';
11-
import getIndexesFromModel from '../../utils/getIndexesFromModel';
11+
import { getIndexesFromModel } from '../../utils/getIndexesFromModel';
1212
import { isObject } from '../../utils/is';
1313
import { toDottedObject, upperFirst } from '../../utils';
1414
import typeStorage from '../../typeStorage';

src/resolvers/helpers/sort.js

+2-26
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
/* eslint-disable no-use-before-define */
33

44
import { GraphQLEnumType } from 'graphql';
5-
import getIndexesFromModel from '../../utils/getIndexesFromModel';
5+
import { getIndexesFromModel, extendByReversedIndexes } from '../../utils/getIndexesFromModel';
66
import typeStorage from '../../typeStorage';
77
import type {
88
ExtendedResolveParams,
@@ -49,7 +49,7 @@ export function getSortTypeFromModel(
4949
typeName: string,
5050
model: MongooseModelT
5151
): GraphQLEnumType {
52-
const indexes = getIndexesFromModelWithReverse(model);
52+
const indexes = extendByReversedIndexes(getIndexesFromModel(model));
5353

5454
const sortEnumValues = {};
5555
indexes.forEach((indexData) => {
@@ -74,27 +74,3 @@ export function getSortTypeFromModel(
7474
})
7575
);
7676
}
77-
78-
79-
export function getIndexesFromModelWithReverse(model: MongooseModelT) {
80-
const indexes = getIndexesFromModel(model);
81-
const result: ObjectMap[] = [];
82-
83-
indexes.forEach((indexObj) => {
84-
let hasSpecificIndex = false;
85-
// https://docs.mongodb.org/manual/tutorial/sort-results-with-indexes/#sort-on-multiple-fields
86-
const reversedIndexObj = Object.assign({}, indexObj);
87-
Object.keys(reversedIndexObj).forEach((f) => {
88-
if (reversedIndexObj[f] === 1) reversedIndexObj[f] = -1;
89-
else if (reversedIndexObj[f] === -1) reversedIndexObj[f] = 1;
90-
else hasSpecificIndex = true;
91-
});
92-
93-
result.push(indexObj);
94-
if (!hasSpecificIndex) {
95-
result.push(reversedIndexObj);
96-
}
97-
});
98-
99-
return result;
100-
}

src/utils/__tests__/getIndexesFromModel-test.js

+68-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { expect } from 'chai';
22
import mongoose from 'mongoose';
3-
import getIndexesFromModel from '../getIndexesFromModel';
3+
import {
4+
getIndexesFromModel,
5+
getUniqueIndexes,
6+
extendByReversedIndexes,
7+
} from '../getIndexesFromModel';
48

59
const AgentSchema = new mongoose.Schema(
610
{
@@ -17,6 +21,11 @@ const AgentSchema = new mongoose.Schema(
1721
description: 'Person name',
1822
},
1923

24+
someUniqField: {
25+
type: String,
26+
unique: true,
27+
},
28+
2029
age: {
2130
type: Number,
2231
description: 'Full years',
@@ -43,18 +52,21 @@ const AgentSchema = new mongoose.Schema(
4352
AgentSchema.set('autoIndex', false);
4453
AgentSchema.index({ name: 1, age: -1 });
4554
AgentSchema.index({ 'subDoc.field2': 1 });
46-
AgentSchema.index({ name: 'text', skills: 'text' });
55+
AgentSchema.index({ name: 'text', skills: 'text' }, { unique: true });
56+
AgentSchema.index({ name: 1, someOtherField: -1 }, { unique: true });
4757

4858
const AgentModel = mongoose.model('Agent', AgentSchema);
4959

50-
describe('getIndexesFromModel', () => {
60+
describe('getIndexesFromModel()', () => {
5161
it('should get regular indexes and extract compound idx by default', () => {
5262
const idx = getIndexesFromModel(AgentModel);
5363
expect(idx).to.deep.have.all.members([
5464
{ _id: 1 },
5565
{ name: 1 },
5666
{ name: 1, age: -1 },
5767
{ 'subDoc.field2': 1 },
68+
{ someUniqField: 1 },
69+
{ name: 1, someOtherField: -1 },
5870
]);
5971
});
6072

@@ -64,18 +76,70 @@ describe('getIndexesFromModel', () => {
6476
{ _id: 1 },
6577
{ name: 1, age: -1 },
6678
{ 'subDoc.field2': 1 },
79+
{ someUniqField: 1 },
80+
{ name: 1, someOtherField: -1 },
6781
]);
6882
});
6983

7084
it('it should return specialIndexes indexes', () => {
71-
const idx = getIndexesFromModel(AgentModel, { skipSpecificIndexes : false });
85+
const idx = getIndexesFromModel(AgentModel, { skipSpecificIndexes: false });
7286
expect(idx).to.deep.have.all.members([
7387
{ _id: 1 },
7488
{ name: 1 },
7589
{ name: 1, age: -1 },
7690
{ 'subDoc.field2': 1 },
7791
{ name: 'text' },
7892
{ name: 'text', skills: 'text' },
93+
{ someUniqField: 1 },
94+
{ name: 1, someOtherField: -1 },
95+
]);
96+
});
97+
});
98+
99+
100+
describe('getUniqueIndexes()', () => {
101+
it('should return unique indexes', () => {
102+
const idx = getUniqueIndexes(AgentModel);
103+
expect(idx).to.deep.have.all.members([
104+
{ _id: 1 },
105+
{ someUniqField: 1 },
106+
{ name: 1, someOtherField: -1 },
107+
]);
108+
});
109+
});
110+
111+
describe('extendByReversedIndexes()', () => {
112+
it('should return extended indexes list', () => {
113+
const idxSource = [
114+
{ _id: 1 },
115+
{ someUniqField: 1 },
116+
{ name: 1, someOtherField: -1 },
117+
];
118+
const idx = extendByReversedIndexes(idxSource);
119+
expect(idx).deep.equal([
120+
{ _id: 1 },
121+
{ _id: -1 },
122+
{ someUniqField: 1 },
123+
{ someUniqField: -1 },
124+
{ name: 1, someOtherField: -1 },
125+
{ name: -1, someOtherField: 1 },
126+
]);
127+
});
128+
129+
it('should return extended indexes list with reversed first', () => {
130+
const idxSource = [
131+
{ _id: 1 },
132+
{ someUniqField: 1 },
133+
{ name: 1, someOtherField: -1 },
134+
];
135+
const idx = extendByReversedIndexes(idxSource, { reversedFirst: true });
136+
expect(idx).deep.equal([
137+
{ _id: -1 },
138+
{ _id: 1 },
139+
{ someUniqField: -1 },
140+
{ someUniqField: 1 },
141+
{ name: -1, someOtherField: 1 },
142+
{ name: 1, someOtherField: -1 },
79143
]);
80144
});
81145
});

src/utils/getIndexesFromModel.js

+72-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ function isSpecificIndex(idx) {
2525
* Get mongoose model, and return array of fields with indexes.
2626
* MongooseModel -> [ { _id: 1 }, { name: 1, surname: -1 } ]
2727
**/
28-
export default function getIndexesFromModel(
28+
export function getIndexesFromModel(
2929
mongooseModel: MongooseModelT,
3030
opts: getIndexesFromModelOpts = {}
3131
): ObjectMap[] {
@@ -73,3 +73,74 @@ export default function getIndexesFromModel(
7373

7474
return indexedFields;
7575
}
76+
77+
78+
export function getUniqueIndexes(mongooseModel: MongooseModelT): ObjectMap[] {
79+
const indexedFields = [];
80+
81+
// add _id field if existed
82+
if (mongooseModel.schema.paths._id) {
83+
indexedFields.push({ _id: 1 });
84+
}
85+
86+
// scan all fields on index presence [MONGOOSE FIELDS LEVEL INDEX]
87+
Object.keys(mongooseModel.schema.paths).forEach((name) => {
88+
if (mongooseModel.schema.paths[name]._index
89+
&& mongooseModel.schema.paths[name]._index.unique) {
90+
indexedFields.push({ [name]: 1 }); // ASC by default
91+
}
92+
});
93+
94+
// scan compound and special indexes [MONGOOSE SCHEMA LEVEL INDEXES]
95+
if (Array.isArray(mongooseModel.schema._indexes)) {
96+
mongooseModel.schema._indexes.forEach((idxData) => {
97+
const idxFields = idxData[0];
98+
const idxCfg = idxData[1];
99+
if (idxCfg.unique && !isSpecificIndex(idxFields)) {
100+
indexedFields.push(idxFields);
101+
}
102+
});
103+
}
104+
105+
return indexedFields;
106+
}
107+
108+
export type extendByReversedIndexesOpts = {
109+
reversedFirst?: boolean, // false by default
110+
}
111+
112+
export function extendByReversedIndexes(
113+
indexes: ObjectMap[],
114+
opts: extendByReversedIndexesOpts = {}
115+
) {
116+
const reversedFirst = opts.reversedFirst === undefined
117+
? false
118+
: Boolean(opts.reversedFirst);
119+
120+
const result: ObjectMap[] = [];
121+
122+
indexes.forEach((indexObj) => {
123+
let hasSpecificIndex = false;
124+
// https://docs.mongodb.org/manual/tutorial/sort-results-with-indexes/#sort-on-multiple-fields
125+
const reversedIndexObj = Object.assign({}, indexObj);
126+
Object.keys(reversedIndexObj).forEach((f) => {
127+
if (reversedIndexObj[f] === 1) reversedIndexObj[f] = -1;
128+
else if (reversedIndexObj[f] === -1) reversedIndexObj[f] = 1;
129+
else hasSpecificIndex = true;
130+
});
131+
132+
if (reversedFirst) {
133+
if (!hasSpecificIndex) {
134+
result.push(reversedIndexObj);
135+
}
136+
result.push(indexObj);
137+
} else {
138+
result.push(indexObj);
139+
if (!hasSpecificIndex) {
140+
result.push(reversedIndexObj);
141+
}
142+
}
143+
});
144+
145+
return result;
146+
}

0 commit comments

Comments
 (0)