Skip to content

Commit d1a33fa

Browse files
committed
Add support for filtering data with operators $lt, $gt, ...
1 parent 368e4b9 commit d1a33fa

File tree

10 files changed

+142
-18
lines changed

10 files changed

+142
-18
lines changed

src/definition.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,13 @@ export type filterHelperArgsOpts = {
180180
isRequired?: boolean,
181181
onlyIndexed?: boolean,
182182
requiredFields?: string | string[],
183-
model?: MongooseModelT,
183+
operators?: filterOperatorsOpts | false,
184+
};
185+
186+
export type filterOperatorNames = 'gt' | 'gte' | 'lt' | 'lte' | 'ne' | 'in[]' | 'nin[]';
187+
188+
export type filterOperatorsOpts = {
189+
[fieldName: string]: filterOperatorNames[] | false,
184190
};
185191

186192
export type sortHelperArgsOpts = {

src/resolvers/count.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export default function count(
3232
name: 'count',
3333
kind: 'query',
3434
args: {
35-
...filterHelperArgs(typeComposer, {
35+
...filterHelperArgs(typeComposer, model, {
3636
filterTypeName: `Filter${typeComposer.getTypeName()}Input`,
3737
model,
3838
...(opts && opts.filter),

src/resolvers/findMany.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export default function findMany(
3636
name: 'findMany',
3737
kind: 'query',
3838
args: {
39-
...filterHelperArgs(typeComposer, {
39+
...filterHelperArgs(typeComposer, model, {
4040
filterTypeName: `FilterFindMany${typeComposer.getTypeName()}Input`,
4141
model,
4242
...(opts && opts.filter),

src/resolvers/findOne.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export default function findOne(
3535
name: 'findOne',
3636
kind: 'query',
3737
args: {
38-
...filterHelperArgs(typeComposer, {
38+
...filterHelperArgs(typeComposer, model, {
3939
filterTypeName: `FilterFindOne${typeComposer.getTypeName()}Input`,
4040
model,
4141
...(opts && opts.filter),

src/resolvers/helpers/filter.js

Lines changed: 120 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,42 @@
11
/* @flow */
22
/* eslint-disable no-use-before-define */
33

4-
import { TypeComposer } from 'graphql-compose';
5-
import { GraphQLNonNull } from 'graphql';
4+
import { TypeComposer, InputTypeComposer } from 'graphql-compose';
5+
import {
6+
GraphQLNonNull,
7+
GraphQLInputObjectType,
8+
GraphQLList,
9+
getNamedType,
10+
} from 'graphql';
611
import getIndexesFromModel from '../../utils/getIndexesFromModel';
7-
import { toDottedObject } from '../../utils';
12+
import { toDottedObject, upperFirst } from '../../utils';
813
import type {
914
GraphQLFieldConfigArgumentMap,
1015
ExtendedResolveParams,
1116
MongooseModelT,
1217
filterHelperArgsOpts,
18+
filterOperatorsOpts,
19+
filterOperatorNames,
1320
} from '../../definition';
1421

22+
export const OPERATORS_FIELDNAME = '_operators';
23+
24+
1525
export const filterHelperArgs = (
1626
typeComposer: TypeComposer,
27+
model: MongooseModelT,
1728
opts: filterHelperArgsOpts
1829
): GraphQLFieldConfigArgumentMap => {
1930
if (!(typeComposer instanceof TypeComposer)) {
2031
throw new Error('First arg for filterHelperArgs() should be instance of TypeComposer.');
2132
}
2233

34+
if (!model || !model.modelName || !model.schema) {
35+
throw new Error(
36+
'Second arg for filterHelperArgs() should be instance of Mongoose Model.'
37+
);
38+
}
39+
2340
if (!opts || !opts.filterTypeName) {
2441
throw new Error('You should provide non-empty `filterTypeName` in options.');
2542
}
@@ -34,12 +51,7 @@ export const filterHelperArgs = (
3451
}
3552

3653
if (opts.onlyIndexed) {
37-
if (!opts.model) {
38-
throw new Error('You should provide `model` in options with mongoose model '
39-
+ 'for deriving index fields.');
40-
}
41-
42-
const indexedFieldNames = getIndexedFieldNames(opts.model);
54+
const indexedFieldNames = getIndexedFieldNames(model);
4355
Object.keys(typeComposer.getFields()).forEach(fieldName => {
4456
if (indexedFieldNames.indexOf(fieldName) === -1) {
4557
removeFields.push(fieldName);
@@ -49,12 +61,23 @@ export const filterHelperArgs = (
4961

5062
const filterTypeName: string = opts.filterTypeName;
5163
const inputComposer = typeComposer.getInputTypeComposer().clone(filterTypeName);
64+
inputComposer.makeFieldsOptional(inputComposer.getFieldNames());
5265
inputComposer.removeField(removeFields);
5366

5467
if (opts.requiredFields) {
5568
inputComposer.makeFieldsRequired(opts.requiredFields);
5669
}
5770

71+
if (!opts.hasOwnProperty('operators') || opts.operators !== false) {
72+
addFieldsWithOperator(
73+
// $FlowFixMe
74+
`Operators${opts.filterTypeName}`,
75+
inputComposer,
76+
model,
77+
opts.operators || {}
78+
);
79+
}
80+
5881
return {
5982
filter: {
6083
name: 'filter',
@@ -71,7 +94,30 @@ export const filterHelperArgs = (
7194
export function filterHelper(resolveParams: ExtendedResolveParams): void {
7295
const filter = resolveParams.args && resolveParams.args.filter;
7396
if (filter && typeof filter === 'object' && Object.keys(filter).length > 0) {
74-
resolveParams.query = resolveParams.query.where(toDottedObject(filter)); // eslint-disable-line
97+
if (!filter[OPERATORS_FIELDNAME]) {
98+
resolveParams.query = resolveParams.query.where(toDottedObject(filter)); // eslint-disable-line
99+
} else {
100+
const operatorFields = Object.assign({}, filter[OPERATORS_FIELDNAME]);
101+
const simpleFields = Object.assign({}, filter);
102+
delete simpleFields[OPERATORS_FIELDNAME];
103+
104+
if (Object.keys(simpleFields).length > 0) {
105+
resolveParams.query = resolveParams.query.where(toDottedObject(simpleFields)); // eslint-disable-line
106+
}
107+
Object.keys(operatorFields).forEach(fieldName => {
108+
const fieldOperators = Object.assign({}, operatorFields[fieldName]);
109+
const criteria = {};
110+
Object.keys(fieldOperators).forEach(operatorName => {
111+
criteria[`$${operatorName}`] = fieldOperators[operatorName];
112+
});
113+
if (Object.keys(criteria).length > 0) {
114+
// $FlowFixMe
115+
resolveParams.query = resolveParams.query.find({ // eslint-disable-line
116+
[fieldName]: criteria,
117+
});
118+
}
119+
});
120+
}
75121
}
76122
}
77123

@@ -86,3 +132,67 @@ export function getIndexedFieldNames(model: MongooseModelT): string[] {
86132

87133
return fieldNames;
88134
}
135+
136+
export function addFieldsWithOperator(
137+
typeName: string,
138+
inputComposer: InputTypeComposer,
139+
model: MongooseModelT,
140+
operatorsOpts: filterOperatorsOpts
141+
) {
142+
const operatorsComposer = new InputTypeComposer(new GraphQLInputObjectType({
143+
name: typeName,
144+
fields: {},
145+
}));
146+
147+
const availableOperators: filterOperatorNames[]
148+
= ['gt', 'gte', 'lt', 'lte', 'ne', 'in[]', 'nin[]'];
149+
150+
// if `opts.resolvers.[resolverName].filter.operators` is empty and not disabled via `false`
151+
// then fill it up with indexed fields
152+
const indexedFields = getIndexedFieldNames(model);
153+
if (operatorsOpts !== false && Object.keys(operatorsOpts).length === 0) {
154+
indexedFields.forEach(fieldName => {
155+
operatorsOpts[fieldName] = availableOperators; // eslint-disable-line
156+
});
157+
}
158+
159+
const existedFields = inputComposer.getFields();
160+
Object.keys(existedFields).forEach(fieldName => {
161+
if (operatorsOpts[fieldName] && operatorsOpts[fieldName] !== false) {
162+
const fields = {};
163+
let operators;
164+
if (operatorsOpts[fieldName] && Array.isArray(operatorsOpts[fieldName])) {
165+
operators = operatorsOpts[fieldName];
166+
} else {
167+
operators = availableOperators;
168+
}
169+
operators.forEach(operatorName => {
170+
if (operatorName.slice(-2) === '[]') {
171+
fields[operatorName.slice(0, -2)] = {
172+
...existedFields[fieldName],
173+
// $FlowFixMe
174+
type: new GraphQLList(getNamedType(existedFields[fieldName].type)),
175+
};
176+
} else {
177+
fields[operatorName] = getNamedType(existedFields[fieldName]);
178+
}
179+
});
180+
if (Object.keys(fields).length > 0) {
181+
operatorsComposer.addField(fieldName, {
182+
type: new GraphQLInputObjectType({
183+
name: `${upperFirst(fieldName)}${typeName}`,
184+
fields,
185+
}),
186+
description: 'Filter value by operator(s)',
187+
});
188+
}
189+
}
190+
});
191+
192+
if (Object.keys(operatorsComposer.getFields()).length > 0) {
193+
inputComposer.addField(OPERATORS_FIELDNAME, {
194+
type: operatorsComposer.getType(),
195+
description: 'List of fields that can be filtered via operators',
196+
});
197+
}
198+
}

src/resolvers/removeMany.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export default function removeMany(
4444
},
4545
}),
4646
args: {
47-
...filterHelperArgs(typeComposer, {
47+
...filterHelperArgs(typeComposer, model, {
4848
filterTypeName: `FilterRemoveMany${typeComposer.getTypeName()}Input`,
4949
isRequired: true,
5050
model,

src/resolvers/removeOne.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export default function removeOne(
5353
},
5454
}),
5555
args: {
56-
...filterHelperArgs(typeComposer, {
56+
...filterHelperArgs(typeComposer, model, {
5757
filterTypeName: `FilterRemoveOne${typeComposer.getTypeName()}Input`,
5858
model,
5959
...(opts && opts.filter),

src/resolvers/updateMany.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export default function updateMany(
5454
isRequired: true,
5555
...(opts && opts.record),
5656
}),
57-
...filterHelperArgs(typeComposer, {
57+
...filterHelperArgs(typeComposer, model, {
5858
filterTypeName: `FilterUpdateMany${typeComposer.getTypeName()}Input`,
5959
model,
6060
...(opts && opts.filter),

src/resolvers/updateOne.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export default function updateOne(
6262
isRequired: true,
6363
...(opts && opts.record),
6464
}),
65-
...filterHelperArgs(typeComposer, {
65+
...filterHelperArgs(typeComposer, model, {
6666
filterTypeName: `FilterUpdateOne${typeComposer.getTypeName()}Input`,
6767
model,
6868
...(opts && opts.filter),

src/utils/index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,11 @@ import toDottedObject from './toDottedObject';
33
export {
44
toDottedObject,
55
};
6+
7+
export function upperFirst(string: string): string {
8+
return string.charAt(0).toUpperCase() + string.slice(1);
9+
}
10+
11+
export function isObject(value: mixed): boolean {
12+
return typeof value === 'object' && !Array.isArray(value) && value !== null;
13+
}

0 commit comments

Comments
 (0)