Skip to content

Commit e10713b

Browse files
committed
feat: add new resolvers findByIdLean, findByIdsLean, findByManyLean, findByOneLean which returns data from DB without instantiating a full Mongoose documents. It's faster in several times but doesn't support getters & virtuals fields. See https://mongoosejs.com/docs/tutorials/lean.html#using-lean
Related #140
1 parent 84574b7 commit e10713b

16 files changed

+885
-27
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ schemaComposer.Query.addFields({
9898
userByIds: UserTC.getResolver('findByIds'),
9999
userOne: UserTC.getResolver('findOne'),
100100
userMany: UserTC.getResolver('findMany'),
101+
userByIdLean: UserTC.getResolver('findByIdLean'),
102+
userByIdsLean: UserTC.getResolver('findByIdsLean'),
103+
userOneLean: UserTC.getResolver('findOneLean'),
104+
userManyLean: UserTC.getResolver('findManyLean'),
101105
userCount: UserTC.getResolver('count'),
102106
userConnection: UserTC.getResolver('connection'),
103107
userPagination: UserTC.getResolver('pagination'),

src/composeWithMongoose.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,28 @@ export type TypeConverterResolversOpts = {
6161
limit?: LimitHelperArgsOpts | false;
6262
skip?: false;
6363
};
64+
findByIdLean?: false;
65+
findByIdsLean?:
66+
| false
67+
| {
68+
limit?: LimitHelperArgsOpts | false;
69+
sort?: SortHelperArgsOpts | false;
70+
};
71+
findOneLean?:
72+
| false
73+
| {
74+
filter?: FilterHelperArgsOpts | false;
75+
sort?: SortHelperArgsOpts | false;
76+
skip?: false;
77+
};
78+
findManyLean?:
79+
| false
80+
| {
81+
filter?: FilterHelperArgsOpts | false;
82+
sort?: SortHelperArgsOpts | false;
83+
limit?: LimitHelperArgsOpts | false;
84+
skip?: false;
85+
};
6486
updateById?:
6587
| false
6688
| {

src/discriminators/prepareChildResolvers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ export function prepareChildResolvers<TSource, TContext>(
166166

167167
case EMCResolvers.findOne:
168168
case EMCResolvers.findMany:
169+
case EMCResolvers.findOneLean:
170+
case EMCResolvers.findManyLean:
169171
case EMCResolvers.removeOne:
170172
case EMCResolvers.removeMany:
171173
case EMCResolvers.count:

src/resolvers/__tests__/findById-test.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import { Resolver, schemaComposer, ObjectTypeComposer } from 'graphql-compose';
2-
import { GraphQLNonNull } from 'graphql-compose/lib/graphql';
32
import { UserModel, IUser } from '../../__mocks__/userModel';
43
import { PostModel, IPost } from '../../__mocks__/postModel';
54
import findById from '../findById';
6-
import GraphQLMongoID from '../../types/MongoID';
75
import { convertModelToGraphQL } from '../../fieldsConverter';
86
import { ExtendedResolveParams } from '..';
97

@@ -43,10 +41,7 @@ describe('findById() ->', () => {
4341
describe('Resolver.args', () => {
4442
it('should have non-null `_id` arg', () => {
4543
const resolver = findById(UserModel, UserTC);
46-
expect(resolver.hasArg('_id')).toBe(true);
47-
const argConfig: any = resolver.getArgConfig('_id');
48-
expect(argConfig.type).toBeInstanceOf(GraphQLNonNull);
49-
expect(argConfig.type.ofType).toBe(GraphQLMongoID);
44+
expect(resolver.getArgTypeName('_id')).toBe('MongoID!');
5045
});
5146
});
5247

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { Resolver, schemaComposer, ObjectTypeComposer } from 'graphql-compose';
2+
import { UserModel, IUser } from '../../__mocks__/userModel';
3+
import { PostModel, IPost } from '../../__mocks__/postModel';
4+
import findByIdLean from '../findByIdLean';
5+
import { convertModelToGraphQL } from '../../fieldsConverter';
6+
import { ExtendedResolveParams } from '..';
7+
8+
beforeAll(() => UserModel.base.createConnection());
9+
afterAll(() => UserModel.base.disconnect());
10+
11+
describe('findByIdLean() ->', () => {
12+
let UserTC: ObjectTypeComposer;
13+
let PostTypeComposer: ObjectTypeComposer;
14+
15+
beforeEach(() => {
16+
schemaComposer.clear();
17+
UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer);
18+
PostTypeComposer = convertModelToGraphQL(PostModel, 'Post', schemaComposer);
19+
});
20+
21+
let user: IUser;
22+
let post: IPost;
23+
24+
beforeEach(async () => {
25+
await UserModel.deleteMany({});
26+
27+
user = new UserModel({ name: 'nodkz', contacts: { email: 'mail' } });
28+
await user.save();
29+
30+
await PostModel.deleteMany({});
31+
32+
post = new PostModel({ _id: 1, title: 'Post 1' });
33+
await post.save();
34+
});
35+
36+
it('should return Resolver object', () => {
37+
const resolver = findByIdLean(UserModel, UserTC);
38+
expect(resolver).toBeInstanceOf(Resolver);
39+
});
40+
41+
describe('Resolver.args', () => {
42+
it('should have non-null `_id` arg', () => {
43+
const resolver = findByIdLean(UserModel, UserTC);
44+
expect(resolver.getArgTypeName('_id')).toBe('MongoID!');
45+
});
46+
});
47+
48+
describe('Resolver.resolve():Promise', () => {
49+
it('should be fulfilled promise', async () => {
50+
const result = findByIdLean(UserModel, UserTC).resolve({});
51+
await expect(result).resolves.toBeDefined();
52+
});
53+
54+
it('should be rejected if args.id is not objectId', async () => {
55+
const result = findByIdLean(UserModel, UserTC).resolve({ args: { _id: 1 } });
56+
await expect(result).rejects.toBeDefined();
57+
});
58+
59+
it('should return null if args.id is empty', async () => {
60+
const result = await findByIdLean(UserModel, UserTC).resolve({});
61+
expect(result).toBe(null);
62+
});
63+
64+
it('should return document if provided existed id', async () => {
65+
const result = await findByIdLean(UserModel, UserTC).resolve({
66+
args: { _id: user._id },
67+
});
68+
expect(result.name).toBe(user.name);
69+
});
70+
71+
it('should return lean User object', async () => {
72+
const result = await findByIdLean(UserModel, UserTC).resolve({
73+
args: { _id: user._id },
74+
});
75+
expect(result).not.toBeInstanceOf(UserModel);
76+
// aliases should be translated `User.n` -> `User.name`
77+
expect(result).toEqual(expect.objectContaining({ name: 'nodkz' }));
78+
});
79+
80+
it('should return lean Post object', async () => {
81+
const result = await findByIdLean(PostModel, PostTypeComposer).resolve({
82+
args: { _id: 1 },
83+
});
84+
expect(result).not.toBeInstanceOf(PostModel);
85+
expect(result).toEqual({ __v: 0, _id: 1, title: 'Post 1' });
86+
});
87+
88+
it('should call `beforeQuery` method with non-executed `query` as arg', async () => {
89+
let beforeQueryCalled = false;
90+
91+
const result = await findByIdLean(PostModel, PostTypeComposer).resolve({
92+
args: { _id: 1 },
93+
beforeQuery: (query: any, rp: ExtendedResolveParams) => {
94+
expect(query).toHaveProperty('exec');
95+
expect(rp.model).toBe(PostModel);
96+
beforeQueryCalled = true;
97+
return { overrides: true };
98+
},
99+
});
100+
101+
expect(beforeQueryCalled).toBe(true);
102+
expect(result).toEqual({ overrides: true });
103+
});
104+
});
105+
});

src/resolvers/__tests__/findByIds-test.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import { Resolver, schemaComposer, ObjectTypeComposer } from 'graphql-compose';
2-
import { GraphQLNonNull, GraphQLList } from 'graphql-compose/lib/graphql';
32
import { UserModel, IUser } from '../../__mocks__/userModel';
43
import { PostModel, IPost } from '../../__mocks__/postModel';
54
import findByIds from '../findByIds';
6-
import GraphQLMongoID from '../../types/MongoID';
75
import { convertModelToGraphQL } from '../../fieldsConverter';
86
import { ExtendedResolveParams } from '..';
97

@@ -53,11 +51,7 @@ describe('findByIds() ->', () => {
5351
describe('Resolver.args', () => {
5452
it('should have non-null `_ids` arg', () => {
5553
const resolver = findByIds(UserModel, UserTC);
56-
expect(resolver.hasArg('_ids')).toBe(true);
57-
const argConfig: any = resolver.getArgConfig('_ids');
58-
expect(argConfig.type).toBeInstanceOf(GraphQLNonNull);
59-
expect(argConfig.type.ofType).toBeInstanceOf(GraphQLList);
60-
expect(argConfig.type.ofType.ofType).toBe(GraphQLMongoID);
54+
expect(resolver.getArgTypeName('_ids')).toBe('[MongoID]!');
6155
});
6256

6357
it('should have `limit` arg', () => {
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { Resolver, schemaComposer, ObjectTypeComposer } from 'graphql-compose';
2+
import { UserModel, IUser } from '../../__mocks__/userModel';
3+
import { PostModel, IPost } from '../../__mocks__/postModel';
4+
import findByIdsLean from '../findByIdsLean';
5+
import { convertModelToGraphQL } from '../../fieldsConverter';
6+
import { ExtendedResolveParams } from '..';
7+
8+
beforeAll(() => UserModel.base.createConnection());
9+
afterAll(() => UserModel.base.disconnect());
10+
11+
describe('findByIdsLean() ->', () => {
12+
let UserTC: ObjectTypeComposer;
13+
let PostTypeComposer: ObjectTypeComposer;
14+
15+
beforeEach(() => {
16+
schemaComposer.clear();
17+
UserTC = convertModelToGraphQL(UserModel, 'User', schemaComposer);
18+
PostTypeComposer = convertModelToGraphQL(PostModel, 'Post', schemaComposer);
19+
});
20+
21+
let user1: IUser;
22+
let user2: IUser;
23+
let user3: IUser;
24+
let post1: IPost;
25+
let post2: IPost;
26+
let post3: IPost;
27+
28+
beforeEach(async () => {
29+
await UserModel.deleteMany({});
30+
31+
user1 = new UserModel({ name: 'nodkz1', contacts: { email: 'mail' } });
32+
user2 = new UserModel({ name: 'nodkz2', contacts: { email: 'mail' } });
33+
user3 = new UserModel({ name: 'nodkz3', contacts: { email: 'mail' } });
34+
35+
await Promise.all([user1.save(), user2.save(), user3.save()]);
36+
37+
await PostModel.deleteMany({});
38+
39+
post1 = new PostModel({ _id: 1, title: 'Post 1' });
40+
post2 = new PostModel({ _id: 2, title: 'Post 2' });
41+
post3 = new PostModel({ _id: 3, title: 'Post 3' });
42+
43+
await Promise.all([post1.save(), post2.save(), post3.save()]);
44+
});
45+
46+
it('should return Resolver object', () => {
47+
const resolver = findByIdsLean(UserModel, UserTC);
48+
expect(resolver).toBeInstanceOf(Resolver);
49+
});
50+
51+
describe('Resolver.args', () => {
52+
it('should have non-null `_ids` arg', () => {
53+
const resolver = findByIdsLean(UserModel, UserTC);
54+
expect(resolver.getArgTypeName('_ids')).toBe('[MongoID]!');
55+
});
56+
57+
it('should have `limit` arg', () => {
58+
const resolver = findByIdsLean(UserModel, UserTC);
59+
expect(resolver.hasArg('limit')).toBe(true);
60+
});
61+
62+
it('should have `sort` arg', () => {
63+
const resolver = findByIdsLean(UserModel, UserTC);
64+
expect(resolver.hasArg('sort')).toBe(true);
65+
});
66+
});
67+
68+
describe('Resolver.resolve():Promise', () => {
69+
it('should be fulfilled promise', async () => {
70+
const result = findByIdsLean(UserModel, UserTC).resolve({});
71+
await expect(result).resolves.toBeDefined();
72+
});
73+
74+
it('should return empty array if args._ids is empty', async () => {
75+
const result = await findByIdsLean(UserModel, UserTC).resolve({});
76+
expect(result).toBeInstanceOf(Array);
77+
expect(Object.keys(result)).toHaveLength(0);
78+
});
79+
80+
it('should return array of documents', async () => {
81+
const result = await findByIdsLean(UserModel, UserTC).resolve({
82+
args: { _ids: [user1._id, user2._id, user3._id] },
83+
});
84+
85+
expect(result).toBeInstanceOf(Array);
86+
expect(result).toHaveLength(3);
87+
expect(result.map((d: any) => d.name)).toEqual(
88+
expect.arrayContaining([user1.name, user2.name, user3.name])
89+
);
90+
});
91+
92+
it('should return array of documents if object id is string', async () => {
93+
const stringId = `${user1._id}`;
94+
const result = await findByIdsLean(UserModel, UserTC).resolve({
95+
args: { _ids: [stringId] },
96+
});
97+
98+
expect(result).toBeInstanceOf(Array);
99+
expect(result).toHaveLength(1);
100+
});
101+
102+
it('should return array of documents if args._ids are integers', async () => {
103+
const result = await findByIdsLean(PostModel, PostTypeComposer).resolve({
104+
args: { _ids: [1, 2, 3] },
105+
});
106+
expect(result).toBeInstanceOf(Array);
107+
expect(result).toHaveLength(3);
108+
});
109+
110+
it('should return mongoose documents', async () => {
111+
const result = await findByIdsLean(UserModel, UserTC).resolve({
112+
args: { _ids: [user1._id, user2._id] },
113+
});
114+
expect(result[0]).not.toBeInstanceOf(UserModel);
115+
expect(result[1]).not.toBeInstanceOf(UserModel);
116+
// should translate aliases fields
117+
expect(result).toEqual([
118+
expect.objectContaining({ name: 'nodkz1' }),
119+
expect.objectContaining({ name: 'nodkz2' }),
120+
]);
121+
});
122+
123+
it('should call `beforeQuery` method with non-executed `query` as arg', async () => {
124+
const result = await findByIdsLean(UserModel, UserTC).resolve({
125+
args: { _ids: [user1._id, user2._id] },
126+
beforeQuery(query: any, rp: ExtendedResolveParams) {
127+
expect(rp.model).toBe(UserModel);
128+
expect(rp.query).toHaveProperty('exec');
129+
return query.where({ _id: user1._id });
130+
},
131+
});
132+
133+
expect(result).toHaveLength(1);
134+
});
135+
});
136+
});

0 commit comments

Comments
 (0)