Skip to content

Commit 4030e04

Browse files
committed
Cover with tests
1 parent 0b0f809 commit 4030e04

16 files changed

+1132
-192
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ Requirements
6161
============
6262
Types should have following resolvers:
6363
* `count` - for counting records
64-
* `findMany` - for filtering records. Also required that this resolver supports search with operators (lt, gt), which used in `directionFilter` option. Resolver `findMany` should have `filter` argument, which will be copied to connection.
64+
* `findMany` - for filtering records. Also required that this resolver supports search with operators (lt, gt), which used in `directionFilter` option. Resolver `findMany` should have `filter` argument, which will be copied to connection. Also should have `limit` and `skip` args.
6565

6666
Used in plugins
6767
===============
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { TypeComposer } from 'graphql-compose';
2+
import {
3+
GraphQLObjectType,
4+
} from 'graphql';
5+
6+
const RootQuery = new GraphQLObjectType({
7+
name: 'RootQuery',
8+
fields: {
9+
},
10+
});
11+
12+
export const rootQueryTypeComposer = new TypeComposer(RootQuery);

src/__mocks__/userTypeComposer.js

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { TypeComposer, Resolver } from 'graphql-compose';
2+
import {
3+
GraphQLString,
4+
GraphQLObjectType,
5+
GraphQLInputObjectType,
6+
GraphQLEnumType,
7+
GraphQLInt,
8+
} from 'graphql';
9+
10+
export const UserType = new GraphQLObjectType({
11+
name: 'User',
12+
fields: {
13+
id: {
14+
type: GraphQLInt,
15+
},
16+
name: {
17+
type: GraphQLString,
18+
},
19+
age: {
20+
type: GraphQLInt,
21+
},
22+
gender: {
23+
type: GraphQLString,
24+
},
25+
},
26+
});
27+
28+
export const userTypeComposer = new TypeComposer(UserType);
29+
30+
export const userList = [
31+
{ id: 1, name: 'user1', age: 11, gender: 'm' },
32+
{ id: 2, name: 'user2', age: 12, gender: 'm' },
33+
{ id: 3, name: 'user3', age: 13, gender: 'f' },
34+
{ id: 4, name: 'user4', age: 14, gender: 'm' },
35+
{ id: 5, name: 'user5', age: 15, gender: 'f' },
36+
{ id: 6, name: 'user6', age: 16, gender: 'f' },
37+
{ id: 7, name: 'user7', age: 17, gender: 'f' },
38+
{ id: 8, name: 'user8', age: 18, gender: 'm' },
39+
{ id: 9, name: 'user9', age: 19, gender: 'm' },
40+
{ id: 10, name: 'user10', age: 49, gender: 'f' },
41+
{ id: 11, name: 'user11', age: 49, gender: 'm' },
42+
{ id: 12, name: 'user12', age: 47, gender: 'f' },
43+
{ id: 15, name: 'user15', age: 45, gender: 'm' },
44+
{ id: 14, name: 'user14', age: 45, gender: 'm' },
45+
{ id: 13, name: 'user13', age: 45, gender: 'f' },
46+
];
47+
48+
const filterArgConfig = {
49+
name: 'filter',
50+
type: new GraphQLInputObjectType({
51+
name: 'FilterUserInput',
52+
fields: {
53+
gender: {
54+
type: GraphQLString,
55+
},
56+
_operators: {
57+
type: new GraphQLInputObjectType({
58+
name: 'OperatorsFilterUserInput',
59+
fields: {
60+
id: {
61+
type: new GraphQLInputObjectType({
62+
name: 'IdOperatorsFilterUserInput',
63+
fields: {
64+
lt: { type: GraphQLInt },
65+
gt: { type: GraphQLInt },
66+
},
67+
}),
68+
},
69+
age: {
70+
type: new GraphQLInputObjectType({
71+
name: 'AgeOperatorsFilterUserInput',
72+
fields: {
73+
lt: { type: GraphQLInt },
74+
gt: { type: GraphQLInt },
75+
},
76+
}),
77+
},
78+
},
79+
}),
80+
},
81+
},
82+
}),
83+
};
84+
85+
function filteredUserList(list, filter = {}) {
86+
let result = list.slice();
87+
if (filter.gender) {
88+
result = result.filter(o => o.gender === filter.gender);
89+
}
90+
91+
if (filter._operators) {
92+
if (filter._operators.id) {
93+
if (filter._operators.id.lt) {
94+
result = result.filter(o => o.id < filter._operators.id.lt);
95+
}
96+
if (filter._operators.id.gt) {
97+
result = result.filter(o => o.id > filter._operators.id.gt);
98+
}
99+
}
100+
if (filter._operators.age) {
101+
if (filter._operators.age.lt) {
102+
result = result.filter(o => o.age < filter._operators.age.lt);
103+
}
104+
if (filter._operators.age.gt) {
105+
result = result.filter(o => o.age > filter._operators.age.gt);
106+
}
107+
}
108+
}
109+
110+
return result;
111+
}
112+
113+
function sortUserList(list, sortValue = {}) {
114+
const fields = Object.keys(sortValue);
115+
list.sort((a, b) => {
116+
let result = 0;
117+
fields.forEach(field => {
118+
if (result === 0) {
119+
if (a[field] < b[field]) {
120+
result = sortValue[field] * -1;
121+
} else if (a[field] > b[field]) {
122+
result = sortValue[field];
123+
}
124+
}
125+
});
126+
return result;
127+
});
128+
return list;
129+
}
130+
131+
export const findManyResolver = new Resolver(userTypeComposer, {
132+
name: 'findMany',
133+
kind: 'query',
134+
outputType: UserType,
135+
args: {
136+
filter: filterArgConfig,
137+
sort: new GraphQLEnumType({
138+
name: 'SortUserInput',
139+
values: {
140+
ID_ASC: { name: 'ID_ASC', value: { id: 1 } },
141+
ID_DESC: { name: 'ID_DESC', value: { id: -1 } },
142+
AGE_ASC: { name: 'AGE_ASC', value: { age: 1 } },
143+
AGE_DESC: { name: 'AGE_DESC', value: { age: -1 } },
144+
},
145+
}),
146+
limit: GraphQLInt,
147+
skip: GraphQLInt,
148+
},
149+
resolve: (resolveParams) => {
150+
const args = resolveParams.args || {};
151+
const { filter, sort, limit, skip } = args;
152+
153+
let list = userList.slice();
154+
list = sortUserList(list, sort);
155+
list = filteredUserList(list, filter);
156+
157+
if (skip) {
158+
list = list.slice(skip);
159+
}
160+
161+
if (limit) {
162+
list = list.slice(0, limit);
163+
}
164+
165+
return Promise.resolve(list);
166+
},
167+
});
168+
169+
export const countResolver = new Resolver(userTypeComposer, {
170+
name: 'count',
171+
kind: 'query',
172+
outputType: GraphQLInt,
173+
args: {
174+
filter: filterArgConfig,
175+
},
176+
resolve: (resolveParams) => {
177+
return Promise.resolve(
178+
filteredUserList(
179+
userList,
180+
resolveParams.args && resolveParams.args.filter
181+
).length
182+
);
183+
},
184+
});
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/* eslint-disable no-param-reassign */
2+
3+
import { expect } from 'chai';
4+
import { TypeComposer } from 'graphql-compose';
5+
import { composeWithConnection } from '../composeWithConnection';
6+
import { userTypeComposer } from '../__mocks__/userTypeComposer';
7+
import { rootQueryTypeComposer } from '../__mocks__/rootQueryTypeComposer';
8+
import {
9+
graphql,
10+
GraphQLSchema,
11+
} from 'graphql';
12+
13+
describe('composeWithRelay', () => {
14+
const userComposer = composeWithConnection(userTypeComposer, {
15+
countResolverName: 'count',
16+
findResolverName: 'findMany',
17+
sort: {
18+
ID_ASC: {
19+
uniqueFields: ['id'],
20+
sortValue: { id: 1 },
21+
directionFilter: (filter, cursorData, isBefore) => {
22+
filter._operators = filter._operators || {};
23+
filter._operators.id = filter._operators.id || {};
24+
if (isBefore) {
25+
filter._operators.id.lt = cursorData.id;
26+
} else {
27+
filter._operators.id.gt = cursorData.id;
28+
}
29+
return filter;
30+
},
31+
},
32+
AGE_ID_DESC: {
33+
uniqueFields: ['age', 'id'],
34+
sortValue: { age: -1, id: -1 },
35+
directionFilter: (filter, cursorData, isBefore) => {
36+
filter._operators = filter._operators || {};
37+
filter._operators.id = filter._operators.id || {};
38+
filter._operators.age = filter._operators.age || {};
39+
if (isBefore) {
40+
filter._operators.age.gt = cursorData.age;
41+
filter._operators.id.gt = cursorData.id;
42+
} else {
43+
filter._operators.age.lt = cursorData.age;
44+
filter._operators.id.lt = cursorData.id;
45+
}
46+
return filter;
47+
},
48+
},
49+
},
50+
});
51+
52+
describe('basic checks', () => {
53+
it('should return TypeComposer', () => {
54+
expect(userComposer).instanceof(TypeComposer);
55+
});
56+
57+
it('should throw error if first arg is not TypeComposer', () => {
58+
expect(() => composeWithConnection(123)).to.throw('should provide TypeComposer instance');
59+
});
60+
61+
it('should throw error if options are empty', () => {
62+
expect(() => composeWithConnection(userTypeComposer))
63+
.to.throw('should provide non-empty options');
64+
});
65+
});
66+
67+
it('should apply first sort ID_ASC by default', async () => {
68+
rootQueryTypeComposer.addField('userConnection',
69+
userTypeComposer.getResolver('connection').getFieldConfig()
70+
);
71+
const schema = new GraphQLSchema({
72+
query: rootQueryTypeComposer.getType(),
73+
});
74+
const query = `{
75+
userConnection(last: 3) {
76+
count,
77+
pageInfo {
78+
startCursor
79+
endCursor
80+
hasPreviousPage
81+
hasNextPage
82+
}
83+
edges {
84+
cursor
85+
node {
86+
id
87+
name
88+
}
89+
}
90+
}
91+
}`;
92+
const result = await graphql(schema, query);
93+
expect(result)
94+
.deep.property('data.userConnection')
95+
.deep.equals({
96+
count: 15,
97+
pageInfo:
98+
{ startCursor: 'eyJpZCI6MTN9',
99+
endCursor: 'eyJpZCI6MTV9',
100+
hasPreviousPage: true,
101+
hasNextPage: false },
102+
edges: [
103+
{
104+
cursor: 'eyJpZCI6MTN9',
105+
node: { id: 13, name: 'user13' },
106+
},
107+
{
108+
cursor: 'eyJpZCI6MTR9',
109+
node: { id: 14, name: 'user14' },
110+
},
111+
{
112+
cursor: 'eyJpZCI6MTV9',
113+
node: { id: 15, name: 'user15' },
114+
},
115+
],
116+
});
117+
});
118+
119+
it('should able to change `sort` on AGE_ID_DESC', async () => {
120+
rootQueryTypeComposer.addField('userConnection',
121+
userTypeComposer.getResolver('connection').getFieldConfig()
122+
);
123+
const schema = new GraphQLSchema({
124+
query: rootQueryTypeComposer.getType(),
125+
});
126+
const query = `{
127+
userConnection(first: 3, sort: AGE_ID_DESC) {
128+
count,
129+
pageInfo {
130+
startCursor
131+
endCursor
132+
hasPreviousPage
133+
hasNextPage
134+
}
135+
edges {
136+
cursor
137+
node {
138+
id
139+
name
140+
age
141+
}
142+
}
143+
}
144+
}`;
145+
const result = await graphql(schema, query);
146+
expect(result)
147+
.deep.property('data.userConnection')
148+
.deep.equals({
149+
count: 0, // TODO fix projection in graphql-compose, should be 15
150+
pageInfo:
151+
{ startCursor: 'eyJhZ2UiOjQ5LCJpZCI6MTF9',
152+
endCursor: 'eyJhZ2UiOjQ3LCJpZCI6MTJ9',
153+
hasPreviousPage: false,
154+
hasNextPage: true },
155+
edges: [
156+
{
157+
cursor: 'eyJhZ2UiOjQ5LCJpZCI6MTF9',
158+
node: { id: 11, name: 'user11', age: 49 },
159+
},
160+
{
161+
cursor: 'eyJhZ2UiOjQ5LCJpZCI6MTB9',
162+
node: { id: 10, name: 'user10', age: 49 },
163+
},
164+
{
165+
cursor: 'eyJhZ2UiOjQ3LCJpZCI6MTJ9',
166+
node: { id: 12, name: 'user12', age: 47 },
167+
},
168+
],
169+
});
170+
});
171+
});

src/__tests__/cursor-test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { expect } from 'chai';
2+
import { cursorToData, dataToCursor } from '../cursor';
3+
4+
describe('cursor behavior', () => {
5+
it('should encode object to base64', () => {
6+
expect(dataToCursor({ id: 1, age: 30 }))
7+
.to.equal('eyJpZCI6MSwiYWdlIjozMH0=');
8+
});
9+
10+
it('should decode object from base64', () => {
11+
expect(cursorToData('eyJpZCI6MSwiYWdlIjozMH0='))
12+
.to.deep.equal({ id: 1, age: 30 });
13+
});
14+
15+
it('should return null if cursor is invalid', () => {
16+
expect(cursorToData('invalid_base64')).to.be.null;
17+
});
18+
});

0 commit comments

Comments
 (0)