Skip to content

Commit 1cedf49

Browse files
committed
fix(Filter): Do not dotten internals of mongo query operators, like , , and others
Closes graphql-compose/graphql-compose#30
1 parent 4c19870 commit 1cedf49

File tree

7 files changed

+145
-88
lines changed

7 files changed

+145
-88
lines changed

.eslintrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"no-underscore-dangle": 0,
66
"no-unused-expressions": 0,
77
"arrow-body-style": 0,
8+
"arrow-parens": 0,
89
"import/no-extraneous-dependencies": ["error", {"devDependencies": true}],
910
"comma-dangle": ["error", {
1011
"arrays": "always-multiline",

src/resolvers/helpers/filter.js

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
import { TypeComposer, InputTypeComposer } from 'graphql-compose';
1111
import { getIndexesFromModel } from '../../utils/getIndexesFromModel';
1212
import { isObject } from '../../utils/is';
13-
import { toDottedObject, upperFirst } from '../../utils';
13+
import { toMongoDottedObject, upperFirst } from '../../utils';
1414
import typeStorage from '../../typeStorage';
1515
import type {
1616
GraphQLFieldConfigArgumentMap,
@@ -23,24 +23,27 @@ import type {
2323

2424
export const OPERATORS_FIELDNAME = '_operators';
2525

26-
2726
export const filterHelperArgs = (
2827
typeComposer: TypeComposer,
2928
model: MongooseModelT,
30-
opts: filterHelperArgsOpts
29+
opts: filterHelperArgsOpts,
3130
): GraphQLFieldConfigArgumentMap => {
3231
if (!(typeComposer instanceof TypeComposer)) {
33-
throw new Error('First arg for filterHelperArgs() should be instance of TypeComposer.');
32+
throw new Error(
33+
'First arg for filterHelperArgs() should be instance of TypeComposer.',
34+
);
3435
}
3536

3637
if (!model || !model.modelName || !model.schema) {
3738
throw new Error(
38-
'Second arg for filterHelperArgs() should be instance of MongooseModel.'
39+
'Second arg for filterHelperArgs() should be instance of MongooseModel.',
3940
);
4041
}
4142

4243
if (!opts || !opts.filterTypeName) {
43-
throw new Error('You should provide non-empty `filterTypeName` in options.');
44+
throw new Error(
45+
'You should provide non-empty `filterTypeName` in options.',
46+
);
4447
}
4548

4649
const removeFields = [];
@@ -54,28 +57,32 @@ export const filterHelperArgs = (
5457

5558
if (opts.onlyIndexed) {
5659
const indexedFieldNames = getIndexedFieldNames(model);
57-
Object.keys(typeComposer.getFields()).forEach((fieldName) => {
60+
Object.keys(typeComposer.getFields()).forEach(fieldName => {
5861
if (indexedFieldNames.indexOf(fieldName) === -1) {
5962
removeFields.push(fieldName);
6063
}
6164
});
6265
}
6366

6467
const filterTypeName: string = opts.filterTypeName;
65-
const inputComposer = typeComposer.getInputTypeComposer().clone(filterTypeName);
68+
const inputComposer = typeComposer
69+
.getInputTypeComposer()
70+
.clone(filterTypeName);
6671
inputComposer.removeField(removeFields);
6772

6873
if (opts.requiredFields) {
6974
inputComposer.makeRequired(opts.requiredFields);
7075
}
7176

72-
if (!{}.hasOwnProperty.call(opts, 'operators') || opts.operators !== false) {
77+
if (
78+
!({}).hasOwnProperty.call(opts, 'operators') || opts.operators !== false
79+
) {
7380
addFieldsWithOperator(
7481
// $FlowFixMe
7582
`Operators${opts.filterTypeName}`,
7683
inputComposer,
7784
model,
78-
opts.operators || {}
85+
opts.operators || {},
7986
);
8087
}
8188

@@ -102,25 +109,28 @@ export function filterHelper(resolveParams: ExtendedResolveParams): void {
102109
if (filter && typeof filter === 'object' && Object.keys(filter).length > 0) {
103110
const modelFields = resolveParams.query.schema.paths;
104111
const clearedFilter = {};
105-
Object.keys(filter).forEach((key) => {
112+
Object.keys(filter).forEach(key => {
106113
if (modelFields[key]) {
107114
clearedFilter[key] = filter[key];
108115
}
109116
});
110117
if (Object.keys(clearedFilter).length > 0) {
111-
resolveParams.query = resolveParams.query.where(toDottedObject(clearedFilter)); // eslint-disable-line
118+
resolveParams.query = resolveParams.query.where(
119+
toMongoDottedObject(clearedFilter),
120+
); // eslint-disable-line
112121
}
113122

114123
if (filter[OPERATORS_FIELDNAME]) {
115124
const operatorFields = filter[OPERATORS_FIELDNAME];
116-
Object.keys(operatorFields).forEach((fieldName) => {
125+
Object.keys(operatorFields).forEach(fieldName => {
117126
const fieldOperators = Object.assign({}, operatorFields[fieldName]);
118127
const criteria = {};
119-
Object.keys(fieldOperators).forEach((operatorName) => {
128+
Object.keys(fieldOperators).forEach(operatorName => {
120129
criteria[`$${operatorName}`] = fieldOperators[operatorName];
121130
});
122131
if (Object.keys(criteria).length > 0) {
123-
resolveParams.query = resolveParams.query.where({ // eslint-disable-line
132+
resolveParams.query = resolveParams.query.where({
133+
// eslint-disable-line
124134
[fieldName]: criteria,
125135
});
126136
}
@@ -129,18 +139,19 @@ export function filterHelper(resolveParams: ExtendedResolveParams): void {
129139
}
130140

131141
if (isObject(resolveParams.rawQuery)) {
132-
resolveParams.query = resolveParams.query.where( // eslint-disable-line
133-
// $FlowFixMe
134-
resolveParams.rawQuery
135-
);
142+
resolveParams.query = resolveParams.query
143+
.where( // eslint-disable-line
144+
// $FlowFixMe
145+
resolveParams.rawQuery,
146+
);
136147
}
137148
}
138149

139150
export function getIndexedFieldNames(model: MongooseModelT): string[] {
140151
const indexes = getIndexesFromModel(model);
141152

142153
const fieldNames = [];
143-
indexes.forEach((indexData) => {
154+
indexes.forEach(indexData => {
144155
const keys = Object.keys(indexData);
145156
const clearedName = keys[0].replace(/[^_a-zA-Z0-9]/i, '__');
146157
fieldNames.push(clearedName);
@@ -153,32 +164,39 @@ export function addFieldsWithOperator(
153164
typeName: string,
154165
inputComposer: InputTypeComposer,
155166
model: MongooseModelT,
156-
operatorsOpts: filterOperatorsOpts
167+
operatorsOpts: filterOperatorsOpts,
157168
): InputTypeComposer {
158169
const operatorsComposer = new InputTypeComposer(
159170
typeStorage.getOrSet(
160171
typeName,
161172
new GraphQLInputObjectType({
162173
name: typeName,
163174
fields: {},
164-
})
165-
)
175+
}),
176+
),
166177
);
167178

168-
const availableOperators: filterOperatorNames[]
169-
= ['gt', 'gte', 'lt', 'lte', 'ne', 'in[]', 'nin[]'];
179+
const availableOperators: filterOperatorNames[] = [
180+
'gt',
181+
'gte',
182+
'lt',
183+
'lte',
184+
'ne',
185+
'in[]',
186+
'nin[]',
187+
];
170188

171189
// if `opts.resolvers.[resolverName].filter.operators` is empty and not disabled via `false`
172190
// then fill it up with indexed fields
173191
const indexedFields = getIndexedFieldNames(model);
174192
if (operatorsOpts !== false && Object.keys(operatorsOpts).length === 0) {
175-
indexedFields.forEach((fieldName) => {
193+
indexedFields.forEach(fieldName => {
176194
operatorsOpts[fieldName] = availableOperators; // eslint-disable-line
177195
});
178196
}
179197

180198
const existedFields = inputComposer.getFields();
181-
Object.keys(existedFields).forEach((fieldName) => {
199+
Object.keys(existedFields).forEach(fieldName => {
182200
if (operatorsOpts[fieldName] && operatorsOpts[fieldName] !== false) {
183201
const fields = {};
184202
let operators;
@@ -187,7 +205,7 @@ export function addFieldsWithOperator(
187205
} else {
188206
operators = availableOperators;
189207
}
190-
operators.forEach((operatorName) => {
208+
operators.forEach(operatorName => {
191209
// unwrap from GraphQLNonNull and GraphQLList, if present
192210
const namedType = getNamedType(existedFields[fieldName].type);
193211
if (namedType) {
@@ -214,7 +232,7 @@ export function addFieldsWithOperator(
214232
new GraphQLInputObjectType({
215233
name: operatorTypeName,
216234
fields,
217-
})
235+
}),
218236
),
219237
description: 'Filter value by operator(s)',
220238
});

src/resolvers/updateMany.js

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,27 @@ import { skipHelperArgs, skipHelper } from './helpers/skip';
88
import { limitHelperArgs, limitHelper } from './helpers/limit';
99
import { filterHelperArgs, filterHelper } from './helpers/filter';
1010
import { sortHelperArgs, sortHelper } from './helpers/sort';
11-
import toDottedObject from '../utils/toDottedObject';
11+
import toMongoDottedObject from '../utils/toMongoDottedObject';
1212
import typeStorage from '../typeStorage';
1313
import type {
1414
MongooseModelT,
1515
ExtendedResolveParams,
1616
genResolverOpts,
1717
} from '../definition';
1818

19-
2019
export default function updateMany(
2120
model: MongooseModelT,
2221
typeComposer: TypeComposer,
23-
opts?: genResolverOpts
22+
opts?: genResolverOpts,
2423
): Resolver {
2524
if (!model || !model.modelName || !model.schema) {
2625
throw new Error(
27-
'First arg for Resolver updateMany() should be instance of Mongoose Model.'
26+
'First arg for Resolver updateMany() should be instance of Mongoose Model.',
2827
);
2928
}
3029
if (!(typeComposer instanceof TypeComposer)) {
3130
throw new Error(
32-
'Second arg for Resolver updateMany() should be instance of TypeComposer.'
31+
'Second arg for Resolver updateMany() should be instance of TypeComposer.',
3332
);
3433
}
3534

@@ -44,15 +43,15 @@ export default function updateMany(
4443
description: 'Affected documents number',
4544
},
4645
},
47-
})
46+
}),
4847
);
4948

5049
const resolver = new Resolver({
5150
name: 'updateMany',
5251
kind: 'mutation',
53-
description: 'Update many documents without returning them: '
54-
+ 'Use Query.update mongoose method. '
55-
+ 'Do not apply mongoose defaults, setters, hooks and validation. ',
52+
description: 'Update many documents without returning them: ' +
53+
'Use Query.update mongoose method. ' +
54+
'Do not apply mongoose defaults, setters, hooks and validation. ',
5655
type: outputType,
5756
args: {
5857
...recordHelperArgs(typeComposer, {
@@ -77,14 +76,18 @@ export default function updateMany(
7776
},
7877
// $FlowFixMe
7978
resolve: (resolveParams: ExtendedResolveParams) => {
80-
const recordData = (resolveParams.args && resolveParams.args.record) || {};
79+
const recordData = (resolveParams.args && resolveParams.args.record) || {
80+
};
8181

82-
if (!(typeof recordData === 'object')
83-
|| Object.keys(recordData).length === 0
82+
if (
83+
!(typeof recordData === 'object') ||
84+
Object.keys(recordData).length === 0
8485
) {
8586
return Promise.reject(
86-
new Error(`${typeComposer.getTypeName()}.updateMany resolver requires `
87-
+ 'at least one value in args.record')
87+
new Error(
88+
`${typeComposer.getTypeName()}.updateMany resolver requires ` +
89+
'at least one value in args.record',
90+
),
8891
);
8992
}
9093

@@ -99,18 +102,17 @@ export default function updateMany(
99102
limitHelper(resolveParams);
100103

101104
resolveParams.query = resolveParams.query.setOptions({ multi: true }); // eslint-disable-line
102-
resolveParams.query.update({ $set: toDottedObject(recordData) });
105+
resolveParams.query.update({ $set: toMongoDottedObject(recordData) });
103106

104-
return (
105-
// `beforeQuery` is experemental feature, if you want to use it
106-
// please open an issue with your use case, cause I suppose that
107-
// this option is excessive
108-
// $FlowFixMe
109-
resolveParams.beforeQuery
110-
? Promise.resolve(resolveParams.beforeQuery(resolveParams.query, resolveParams))
111-
: resolveParams.query.exec()
112-
)
113-
.then((res) => {
107+
// `beforeQuery` is experemental feature, if you want to use it
108+
// please open an issue with your use case, cause I suppose that
109+
// this option is excessive
110+
// $FlowFixMe
111+
return (resolveParams.beforeQuery
112+
? Promise.resolve(
113+
resolveParams.beforeQuery(resolveParams.query, resolveParams),
114+
)
115+
: resolveParams.query.exec()).then(res => {
114116
if (res.ok) {
115117
return {
116118
numAffected: res.nModified,
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { expect } from 'chai';
2+
import toMongoDottedObject from '../toMongoDottedObject';
3+
4+
describe('toMongoDottedObject()', () => {
5+
it('should dot nested objects', () => {
6+
expect(toMongoDottedObject({ a: { b: { c: 1 } } })).to.eql({ 'a.b.c': 1 });
7+
});
8+
9+
it('should not dot query operators started with $', () => {
10+
expect(toMongoDottedObject({ a: { $in: [1, 2, 3] } })).to.eql({
11+
a: { $in: [1, 2, 3] },
12+
});
13+
expect(toMongoDottedObject({ a: { b: { $in: [1, 2, 3] } } })).to.eql({
14+
'a.b': { $in: [1, 2, 3] },
15+
});
16+
});
17+
18+
it('should mix query operators started with $', () => {
19+
expect(
20+
toMongoDottedObject({ a: { $in: [1, 2, 3], $exists: true } }),
21+
).to.eql({
22+
a: { $in: [1, 2, 3], $exists: true },
23+
});
24+
});
25+
26+
it('should not mix query operators started with $ and regular fields', () => {
27+
expect(toMongoDottedObject({ a: { $exists: true, b: 3 } })).to.eql({
28+
a: { $exists: true },
29+
'a.b': 3,
30+
});
31+
});
32+
});

src/utils/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/* @flow */
2-
import toDottedObject from './toDottedObject';
2+
import toMongoDottedObject from './toMongoDottedObject';
33
import { isObject } from './is';
44

55
export {
6-
toDottedObject,
6+
toMongoDottedObject,
77
isObject,
88
};
99

src/utils/toDottedObject.js

Lines changed: 0 additions & 32 deletions
This file was deleted.

0 commit comments

Comments
 (0)