Skip to content

Commit 05a3355

Browse files
committed
feat: add new resolvers dataLoader, dataLoaderLean, dataLoaderMany, dataLoaderLeanMany. These resolvers are quite helpful for relations construction between Entities for avoiding the N+1 Problem.
@see https://github.com/graphql/dataloader
1 parent 9ada140 commit 05a3355

15 files changed

+979
-9
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,14 @@ schemaComposer.Query.addFields({
9898
userByIds: UserTC.getResolver('findByIds'),
9999
userOne: UserTC.getResolver('findOne'),
100100
userMany: UserTC.getResolver('findMany'),
101+
userDataLoader: UserTC.getResolver('dataLoader'),
102+
userDataLoaderMany: UserTC.getResolver('dataLoaderMany'),
101103
userByIdLean: UserTC.getResolver('findByIdLean'),
102104
userByIdsLean: UserTC.getResolver('findByIdsLean'),
103105
userOneLean: UserTC.getResolver('findOneLean'),
104106
userManyLean: UserTC.getResolver('findManyLean'),
107+
userDataLoaderLean: UserTC.getResolver('dataLoaderLean'),
108+
userDataLoaderManyLean: UserTC.getResolver('dataLoaderManyLean'),
105109
userCount: UserTC.getResolver('count'),
106110
userConnection: UserTC.getResolver('connection'),
107111
userPagination: UserTC.getResolver('pagination'),

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
"url": "https://github.com/graphql-compose/graphql-compose-mongoose/issues"
2424
},
2525
"homepage": "https://github.com/graphql-compose/graphql-compose-mongoose",
26-
"dependencies": {},
26+
"dependencies": {
27+
"dataloader": "^2.0.0"
28+
},
2729
"optionalDependencies": {
2830
"graphql-compose-connection": "^7.0.0",
2931
"graphql-compose-pagination": "^7.0.0"
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { Resolver, schemaComposer, ObjectTypeComposer } from 'graphql-compose';
2+
import { UserModel, IUser } from '../../__mocks__/userModel';
3+
import { PostModel, IPost } from '../../__mocks__/postModel';
4+
import { mongoose } from '../../__mocks__/mongooseCommon';
5+
import dataLoader from '../dataLoader';
6+
import { convertModelToGraphQL } from '../../fieldsConverter';
7+
import { ExtendedResolveParams } from '..';
8+
import { GraphQLResolveInfo } from 'graphql';
9+
10+
beforeAll(() => UserModel.base.createConnection());
11+
afterAll(() => UserModel.base.disconnect());
12+
13+
// mock GraphQLResolveInfo
14+
const info = { fieldNodes: {} } as GraphQLResolveInfo;
15+
16+
describe('dataLoader() ->', () => {
17+
let UserTC: ObjectTypeComposer;
18+
let PostTypeComposer: ObjectTypeComposer;
19+
20+
beforeEach(() => {
21+
schemaComposer.clear();
22+
UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer);
23+
PostTypeComposer = convertModelToGraphQL(PostModel, 'Post', schemaComposer);
24+
});
25+
26+
let user: IUser;
27+
let user2: IUser;
28+
let post: IPost;
29+
30+
beforeEach(async () => {
31+
await UserModel.deleteMany({});
32+
33+
user = new UserModel({ name: 'nodkz', contacts: { email: 'mail' } });
34+
await user.save();
35+
36+
user2 = new UserModel({ name: 'user2', contacts: { email: 'mail2' } });
37+
await user2.save();
38+
39+
await PostModel.deleteMany({});
40+
41+
post = new PostModel({ _id: 1, title: 'Post 1' });
42+
await post.save();
43+
});
44+
45+
it('should return Resolver object', () => {
46+
const resolver = dataLoader(UserModel, UserTC);
47+
expect(resolver).toBeInstanceOf(Resolver);
48+
});
49+
50+
describe('Resolver.args', () => {
51+
it('should have non-null `_id` arg', () => {
52+
const resolver = dataLoader(UserModel, UserTC);
53+
expect(resolver.getArgTypeName('_id')).toBe('MongoID!');
54+
});
55+
});
56+
57+
describe('Resolver.resolve():Promise', () => {
58+
it('should be fulfilled promise', async () => {
59+
const result = dataLoader(UserModel, UserTC).resolve({});
60+
await expect(result).resolves.toBeDefined();
61+
});
62+
63+
it('should be rejected if args.id is not objectId', async () => {
64+
const result = dataLoader(UserModel, UserTC).resolve({
65+
args: { _id: 1 },
66+
context: {},
67+
info,
68+
});
69+
await expect(result).rejects.toBeDefined();
70+
});
71+
72+
it('should return null if args.id is empty', async () => {
73+
const result = await dataLoader(UserModel, UserTC).resolve({});
74+
expect(result).toBe(null);
75+
});
76+
77+
it('should return document if provided existed id', async () => {
78+
const result = await dataLoader(UserModel, UserTC).resolve({
79+
args: { _id: user._id },
80+
context: {},
81+
info,
82+
});
83+
expect(result.name).toBe(user.name);
84+
});
85+
86+
it('should return mongoose document', async () => {
87+
const result = await dataLoader(UserModel, UserTC).resolve({
88+
args: { _id: user._id },
89+
context: {},
90+
info,
91+
});
92+
expect(result).toBeInstanceOf(UserModel);
93+
});
94+
95+
it('should return mongoose Post document', async () => {
96+
const result = await dataLoader(PostModel, PostTypeComposer).resolve({
97+
args: { _id: 1 },
98+
context: {},
99+
info,
100+
});
101+
expect(result).toBeInstanceOf(PostModel);
102+
});
103+
104+
it('should call `beforeQuery` method with non-executed `query` as arg', async () => {
105+
const result = await dataLoader(PostModel, PostTypeComposer).resolve({
106+
args: { _id: 1 },
107+
context: {},
108+
info,
109+
beforeQuery: (query: any, rp: ExtendedResolveParams) => {
110+
expect(query).toHaveProperty('exec');
111+
expect(rp.model).toBe(PostModel);
112+
return [{ _id: 1, overridden: true }];
113+
},
114+
});
115+
expect(result).toEqual({ _id: 1, overridden: true });
116+
});
117+
});
118+
119+
it('check DataLoader batch logic', async () => {
120+
let conditions;
121+
const resolveParams = {
122+
context: {},
123+
info,
124+
beforeQuery: (query: any) => {
125+
conditions = query._conditions;
126+
},
127+
};
128+
const resolver = dataLoader(UserModel, UserTC);
129+
const resultPromise1 = resolver.resolve({
130+
args: { _id: user2._id },
131+
...resolveParams,
132+
});
133+
const resultPromise2 = resolver.resolve({
134+
args: { _id: user._id },
135+
...resolveParams,
136+
});
137+
const resultPromise3 = resolver.resolve({
138+
args: { _id: user._id },
139+
...resolveParams,
140+
});
141+
const resultPromise4 = resolver.resolve({
142+
args: { _id: user._id },
143+
...resolveParams,
144+
});
145+
146+
const nonExistedId = mongoose.Types.ObjectId('5cefda4616156200084e5170');
147+
const resultPromise5 = resolver.resolve({
148+
args: { _id: nonExistedId },
149+
...resolveParams,
150+
});
151+
152+
// should return correct results
153+
expect((await resultPromise1).name).toEqual('user2');
154+
expect((await resultPromise2).name).toEqual('nodkz');
155+
expect((await resultPromise3).name).toEqual('nodkz');
156+
expect((await resultPromise4).name).toEqual('nodkz');
157+
158+
// should use $in operator and combine duplicated ids
159+
expect(conditions).toEqual({
160+
_id: { $in: [user2._id, user._id, nonExistedId] },
161+
});
162+
163+
// return undefined for not found record
164+
expect(await resultPromise5).toBe(undefined);
165+
});
166+
});
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { Resolver, schemaComposer, ObjectTypeComposer } from 'graphql-compose';
2+
import { UserModel, IUser } from '../../__mocks__/userModel';
3+
import { PostModel, IPost } from '../../__mocks__/postModel';
4+
import { mongoose } from '../../__mocks__/mongooseCommon';
5+
import dataLoaderLean from '../dataLoaderLean';
6+
import { convertModelToGraphQL } from '../../fieldsConverter';
7+
import { ExtendedResolveParams } from '..';
8+
import { GraphQLResolveInfo } from 'graphql';
9+
10+
beforeAll(() => UserModel.base.createConnection());
11+
afterAll(() => UserModel.base.disconnect());
12+
13+
// mock GraphQLResolveInfo
14+
const info = { fieldNodes: {} } as GraphQLResolveInfo;
15+
16+
describe('dataLoaderLean() ->', () => {
17+
let UserTC: ObjectTypeComposer;
18+
let PostTypeComposer: ObjectTypeComposer;
19+
20+
beforeEach(() => {
21+
schemaComposer.clear();
22+
UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer);
23+
PostTypeComposer = convertModelToGraphQL(PostModel, 'Post', schemaComposer);
24+
});
25+
26+
let user: IUser;
27+
let user2: IUser;
28+
let post: IPost;
29+
30+
beforeEach(async () => {
31+
await UserModel.deleteMany({});
32+
33+
user = new UserModel({ name: 'nodkz', contacts: { email: 'mail' } });
34+
await user.save();
35+
36+
user2 = new UserModel({ name: 'user2', contacts: { email: 'mail2' } });
37+
await user2.save();
38+
39+
await PostModel.deleteMany({});
40+
41+
post = new PostModel({ _id: 1, title: 'Post 1' });
42+
await post.save();
43+
});
44+
45+
it('should return Resolver object', () => {
46+
const resolver = dataLoaderLean(UserModel, UserTC);
47+
expect(resolver).toBeInstanceOf(Resolver);
48+
});
49+
50+
describe('Resolver.args', () => {
51+
it('should have non-null `_id` arg', () => {
52+
const resolver = dataLoaderLean(UserModel, UserTC);
53+
expect(resolver.getArgTypeName('_id')).toBe('MongoID!');
54+
});
55+
});
56+
57+
describe('Resolver.resolve():Promise', () => {
58+
it('should be fulfilled promise', async () => {
59+
const result = dataLoaderLean(UserModel, UserTC).resolve({});
60+
await expect(result).resolves.toBeDefined();
61+
});
62+
63+
it('should be rejected if args.id is not objectId', async () => {
64+
const result = dataLoaderLean(UserModel, UserTC).resolve({
65+
args: { _id: 1 },
66+
context: {},
67+
info,
68+
});
69+
await expect(result).rejects.toBeDefined();
70+
});
71+
72+
it('should return null if args.id is empty', async () => {
73+
const result = await dataLoaderLean(UserModel, UserTC).resolve({});
74+
expect(result).toBe(null);
75+
});
76+
77+
it('should return document if provided existed id', async () => {
78+
const result = await dataLoaderLean(UserModel, UserTC).resolve({
79+
args: { _id: user._id },
80+
context: {},
81+
info,
82+
});
83+
expect(result.name).toBe(user.name);
84+
});
85+
86+
it('should return lean object from DB', async () => {
87+
const result = await dataLoaderLean(UserModel, UserTC).resolve({
88+
args: { _id: user._id },
89+
context: {},
90+
info,
91+
});
92+
expect(result).not.toBeInstanceOf(UserModel);
93+
});
94+
95+
it('should return lean Post object directly from DB', async () => {
96+
const result = await dataLoaderLean(PostModel, PostTypeComposer).resolve({
97+
args: { _id: 1 },
98+
context: {},
99+
info,
100+
});
101+
expect(result).not.toBeInstanceOf(PostModel);
102+
});
103+
104+
it('should call `beforeQuery` method with non-executed `query` as arg', async () => {
105+
const result = await dataLoaderLean(PostModel, PostTypeComposer).resolve({
106+
args: { _id: 1 },
107+
context: {},
108+
info,
109+
beforeQuery: (query: any, rp: ExtendedResolveParams) => {
110+
expect(query).toHaveProperty('exec');
111+
expect(rp.model).toBe(PostModel);
112+
return [{ _id: 1, overridden: true }];
113+
},
114+
});
115+
expect(result).toEqual({ _id: 1, overridden: true });
116+
});
117+
});
118+
119+
it('check DataLoader batch logic', async () => {
120+
let conditions;
121+
const resolveParams = {
122+
context: {},
123+
info,
124+
beforeQuery: (query: any) => {
125+
conditions = query._conditions;
126+
},
127+
};
128+
const resolver = dataLoaderLean(UserModel, UserTC);
129+
const resultPromise1 = resolver.resolve({
130+
args: { _id: user2._id },
131+
...resolveParams,
132+
});
133+
const resultPromise2 = resolver.resolve({
134+
args: { _id: user._id },
135+
...resolveParams,
136+
});
137+
const resultPromise3 = resolver.resolve({
138+
args: { _id: user._id },
139+
...resolveParams,
140+
});
141+
const resultPromise4 = resolver.resolve({
142+
args: { _id: user._id },
143+
...resolveParams,
144+
});
145+
146+
const nonExistedId = mongoose.Types.ObjectId('5cefda4616156200084e5170');
147+
const resultPromise5 = resolver.resolve({
148+
args: { _id: nonExistedId },
149+
...resolveParams,
150+
});
151+
152+
// should return correct results
153+
expect((await resultPromise1).name).toEqual('user2');
154+
expect((await resultPromise2).name).toEqual('nodkz');
155+
expect((await resultPromise3).name).toEqual('nodkz');
156+
expect((await resultPromise4).name).toEqual('nodkz');
157+
158+
// should use $in operator and combine duplicated ids
159+
expect(conditions).toEqual({
160+
_id: { $in: [user2._id, user._id, nonExistedId] },
161+
});
162+
163+
// return undefined for not found record
164+
expect(await resultPromise5).toBe(undefined);
165+
});
166+
});

0 commit comments

Comments
 (0)