Skip to content

Commit c9fcd2e

Browse files
committed
feat: add AND, OR operators to filter arguments. Related #93
1 parent a60493e commit c9fcd2e

File tree

7 files changed

+291
-88
lines changed

7 files changed

+291
-88
lines changed

src/__tests__/integration-test.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,41 @@ describe('integration tests', () => {
163163
expect(Object.keys(res.data.user.rawData)).toMatchSnapshot('projection from all fields');
164164
});
165165
});
166+
167+
it('filter nested OR/AND', async () => {
168+
const UserTC = composeWithMongoose(UserModel);
169+
schemaComposer.rootQuery().addFields({
170+
user: UserTC.getResolver('findMany'),
171+
});
172+
const schema = schemaComposer.buildSchema();
173+
await UserModel.create({
174+
_id: '100000000000000000000301',
175+
name: 'User301',
176+
age: 301,
177+
});
178+
await UserModel.create({
179+
_id: '100000000000000000000302',
180+
name: 'User302',
181+
age: 302,
182+
gender: 'male',
183+
});
184+
await UserModel.create({
185+
_id: '100000000000000000000303',
186+
name: 'User303',
187+
age: 302,
188+
gender: 'female',
189+
});
190+
191+
const res = await graphql(
192+
schema,
193+
`
194+
{
195+
user(filter: { OR: [{ age: 301 }, { AND: [{ gender: male }, { age: 302 }] }] }) {
196+
name
197+
}
198+
}
199+
`
200+
);
201+
expect(res).toEqual({ data: { user: [{ name: 'User301' }, { name: 'User302' }] } });
202+
});
166203
});

src/resolvers/__tests__/findOne-test.js

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,38 +9,38 @@ import { convertModelToGraphQL } from '../../fieldsConverter';
99
beforeAll(() => UserModel.base.connect());
1010
afterAll(() => UserModel.base.disconnect());
1111

12-
describe('findOne() ->', () => {
13-
let UserTC;
14-
15-
beforeEach(() => {
16-
schemaComposer.clear();
17-
UserModel.schema._gqcTypeComposer = undefined;
18-
UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer);
19-
});
12+
let UserTC;
2013

21-
let user1;
22-
let user2;
14+
beforeEach(() => {
15+
schemaComposer.clear();
16+
UserModel.schema._gqcTypeComposer = undefined;
17+
UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer);
18+
});
2319

24-
beforeEach(async () => {
25-
await UserModel.remove({});
20+
let user1;
21+
let user2;
2622

27-
user1 = new UserModel({
28-
name: 'userName1',
29-
skills: ['js', 'ruby', 'php', 'python'],
30-
gender: 'male',
31-
relocation: true,
32-
});
23+
beforeEach(async () => {
24+
await UserModel.remove({});
3325

34-
user2 = new UserModel({
35-
name: 'userName2',
36-
skills: ['go', 'erlang'],
37-
gender: 'female',
38-
relocation: false,
39-
});
26+
user1 = new UserModel({
27+
name: 'userName1',
28+
skills: ['js', 'ruby', 'php', 'python'],
29+
gender: 'male',
30+
relocation: true,
31+
});
4032

41-
await Promise.all([user1.save(), user2.save()]);
33+
user2 = new UserModel({
34+
name: 'userName2',
35+
skills: ['go', 'erlang'],
36+
gender: 'female',
37+
relocation: false,
4238
});
4339

40+
await Promise.all([user1.save(), user2.save()]);
41+
});
42+
43+
describe('findOne() ->', () => {
4444
it('should return Resolver object', () => {
4545
const resolver = findOne(UserModel, UserTC);
4646
expect(resolver).toBeInstanceOf(Resolver);

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

Lines changed: 134 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,39 @@
22

33
import { schemaComposer, InputTypeComposer } from 'graphql-compose';
44
import {
5-
addFieldWithOperators,
5+
_createOperatorsField,
6+
addFilterOperators,
67
processFilterOperators,
78
OPERATORS_FIELDNAME,
89
} from '../filterOperators';
910
import { UserModel } from '../../../__mocks__/userModel';
1011

11-
describe('Resolver helper `filter` ->', () => {
12-
beforeEach(() => {
13-
schemaComposer.clear();
14-
});
15-
16-
describe('addFieldWithOperators()', () => {
17-
let itc: InputTypeComposer;
12+
let itc: InputTypeComposer;
1813

19-
beforeEach(() => {
20-
itc = InputTypeComposer.create({
21-
name: 'UserFilterInput',
22-
fields: {
23-
_id: 'String',
24-
employment: 'String',
25-
name: 'String',
26-
age: 'Int',
27-
skills: ['String'],
28-
},
29-
});
30-
});
14+
beforeEach(() => {
15+
schemaComposer.clear();
16+
itc = InputTypeComposer.create({
17+
name: 'UserFilterInput',
18+
fields: {
19+
_id: 'String',
20+
employment: 'String',
21+
name: 'String',
22+
age: 'Int',
23+
skills: ['String'],
24+
},
25+
});
26+
});
3127

28+
describe('Resolver helper `filter` ->', () => {
29+
describe('_createOperatorsField()', () => {
3230
it('should add OPERATORS_FIELDNAME to filterType', () => {
33-
addFieldWithOperators(itc, 'OperatorsTypeName', UserModel, {});
31+
_createOperatorsField(itc, 'OperatorsTypeName', UserModel, {});
3432
expect(itc.hasField(OPERATORS_FIELDNAME)).toBe(true);
3533
expect(itc.getFieldTC(OPERATORS_FIELDNAME).getTypeName()).toBe('OperatorsTypeName');
3634
});
3735

3836
it('should by default have only indexed fields', () => {
39-
addFieldWithOperators(itc, 'OperatorsTypeName', UserModel, {});
37+
_createOperatorsField(itc, 'OperatorsTypeName', UserModel, {});
4038
const operatorsTC = itc.getFieldTC(OPERATORS_FIELDNAME);
4139
expect(operatorsTC.getFieldNames()).toEqual(
4240
expect.arrayContaining(['name', '_id', 'employment'])
@@ -45,25 +43,47 @@ describe('Resolver helper `filter` ->', () => {
4543
});
4644

4745
it('should have only provided fields via options', () => {
48-
addFieldWithOperators(itc, 'OperatorsTypeName', UserModel, { age: ['lt'] });
46+
_createOperatorsField(itc, 'OperatorsTypeName', UserModel, { age: ['lt'] });
4947
const operatorsTC = itc.getFieldTC(OPERATORS_FIELDNAME);
5048
expect(operatorsTC.hasField('age')).toBe(true);
5149
});
5250

5351
it('should have only provided operators via options for field', () => {
54-
addFieldWithOperators(itc, 'OperatorsTypeName', UserModel, { age: ['lt', 'gte'] });
52+
_createOperatorsField(itc, 'OperatorsTypeName', UserModel, { age: ['lt', 'gte'] });
5553
const operatorsTC = itc.getFieldTC(OPERATORS_FIELDNAME);
5654
const ageTC = operatorsTC.getFieldTC('age');
5755
expect(ageTC.getFieldNames()).toEqual(expect.arrayContaining(['lt', 'gte']));
5856
});
5957

6058
it('should reuse existed operatorsType', () => {
6159
const existedITC = itc.constructor.schemaComposer.getOrCreateITC('ExistedType');
62-
addFieldWithOperators(itc, 'ExistedType', UserModel, {});
60+
_createOperatorsField(itc, 'ExistedType', UserModel, {});
6361
expect(itc.getFieldType(OPERATORS_FIELDNAME)).toBe(existedITC.getType());
6462
});
6563
});
6664

65+
describe('addFilterOperators()', () => {
66+
it('should add OPERATORS_FIELDNAME via _createOperatorsField()', () => {
67+
addFilterOperators(itc, UserModel, {});
68+
expect(itc.hasField(OPERATORS_FIELDNAME)).toBe(true);
69+
expect(itc.getFieldTC(OPERATORS_FIELDNAME).getTypeName()).toBe('Operators');
70+
});
71+
72+
it('should add OR field', () => {
73+
addFilterOperators(itc, UserModel, {});
74+
const fields = itc.getFieldNames();
75+
expect(fields).toEqual(expect.arrayContaining(['OR', 'name', 'age']));
76+
expect(itc.getFieldTC('OR').getType()).toBe(itc.getType());
77+
});
78+
79+
it('should add AND field', () => {
80+
addFilterOperators(itc, UserModel, {});
81+
const fields = itc.getFieldNames();
82+
expect(fields).toEqual(expect.arrayContaining(['AND', 'name', 'age']));
83+
expect(itc.getFieldTC('AND').getType()).toBe(itc.getType());
84+
});
85+
});
86+
6787
describe('processFilterOperators()', () => {
6888
it('should call query.find if args.filter.OPERATORS_FIELDNAME is provided', () => {
6989
const filter = {
@@ -74,5 +94,94 @@ describe('Resolver helper `filter` ->', () => {
7494
processFilterOperators(filter, resolveParams);
7595
expect(spyWhereFn).toBeCalledWith({ age: { $gt: 10, $lt: 20 } });
7696
});
97+
98+
it('should convert OR query', () => {
99+
const filter = {
100+
OR: [
101+
{
102+
name: {
103+
first: 'Pavel',
104+
},
105+
age: 30,
106+
},
107+
{
108+
age: 40,
109+
},
110+
],
111+
};
112+
const spyWhereFn = jest.fn();
113+
const resolveParams: any = { query: { where: spyWhereFn } };
114+
processFilterOperators(filter, resolveParams);
115+
expect(spyWhereFn).toBeCalledWith({
116+
$or: [
117+
{ age: 30, 'name.first': 'Pavel' },
118+
{
119+
age: 40,
120+
},
121+
],
122+
});
123+
});
124+
125+
it('should convert AND query', () => {
126+
const filter = {
127+
AND: [
128+
{
129+
name: {
130+
first: 'Pavel',
131+
},
132+
},
133+
{
134+
age: 40,
135+
},
136+
],
137+
};
138+
const spyWhereFn = jest.fn();
139+
const resolveParams: any = { query: { where: spyWhereFn } };
140+
processFilterOperators(filter, resolveParams);
141+
expect(spyWhereFn).toBeCalledWith({
142+
$and: [
143+
{ 'name.first': 'Pavel' },
144+
{
145+
age: 40,
146+
},
147+
],
148+
});
149+
});
150+
151+
it('should convert nested AND/OR query', () => {
152+
const filter = {
153+
OR: [
154+
{
155+
AND: [
156+
{ name: { first: 'Pavel' } },
157+
{
158+
OR: [{ age: 30 }, { age: 35 }],
159+
},
160+
],
161+
},
162+
{
163+
age: 40,
164+
},
165+
],
166+
};
167+
const spyWhereFn = jest.fn();
168+
const resolveParams: any = { query: { where: spyWhereFn } };
169+
processFilterOperators(filter, resolveParams);
170+
expect(spyWhereFn).toBeCalledWith({
171+
$or: [
172+
{
173+
$and: [
174+
{ 'name.first': 'Pavel' },
175+
{
176+
$or: [{ age: 30 }, { age: 35 }],
177+
},
178+
],
179+
},
180+
{
181+
age: 40,
182+
},
183+
],
184+
});
185+
});
77186
});
78187
});

src/resolvers/helpers/filter.js

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { isObject, toMongoDottedObject, getIndexedFieldNamesForGraphQL } from '.
88
import type { ExtendedResolveParams } from '../index';
99
import {
1010
type FilterOperatorsOpts,
11-
addFieldWithOperators,
11+
addFilterOperators,
1212
processFilterOperators,
1313
} from './filterOperators';
1414

@@ -69,19 +69,12 @@ export const filterHelperArgs = (
6969
itc.makeRequired(opts.requiredFields);
7070
}
7171

72-
if (!{}.hasOwnProperty.call(opts, 'operators') || opts.operators !== false) {
73-
addFieldWithOperators(
74-
itc,
75-
`Operators${opts.filterTypeName || ''}`,
76-
model,
77-
opts.operators || {}
78-
);
79-
}
80-
8172
if (itc.getFieldNames().length === 0) {
8273
return {};
8374
}
8475

76+
addFilterOperators(itc, model, opts);
77+
8578
return {
8679
filter: {
8780
type: opts.isRequired ? itc.getTypeAsRequired() : itc.getType(),
@@ -117,8 +110,6 @@ export function filterHelper(resolveParams: ExtendedResolveParams): void {
117110

118111
if (isObject(resolveParams.rawQuery)) {
119112
// eslint-disable-next-line
120-
resolveParams.query = resolveParams.query.where(
121-
resolveParams.rawQuery
122-
);
113+
resolveParams.query = resolveParams.query.where(resolveParams.rawQuery);
123114
}
124115
}

0 commit comments

Comments
 (0)