diff --git a/src/resolvers/__tests__/connection-test.js b/src/resolvers/__tests__/connection-test.js index cf59c3fe..8453ef5b 100644 --- a/src/resolvers/__tests__/connection-test.js +++ b/src/resolvers/__tests__/connection-test.js @@ -1,6 +1,7 @@ /* @flow */ import { Resolver, schemaComposer } from 'graphql-compose'; +import { Query } from 'mongoose'; import { UserModel } from '../../__mocks__/userModel'; import connection, { prepareCursorQuery } from '../connection'; import findMany from '../findMany'; @@ -217,6 +218,63 @@ describe('connection() resolver', () => { expect(result.edges[0].node).toBeInstanceOf(UserModel); expect(result.edges[1].node).toBeInstanceOf(UserModel); }); + + it('should call `beforeQuery` method with non-executed `query` as arg', async () => { + const mongooseActions = []; + + UserModel.base.set('debug', function debugMongoose(...args) { + mongooseActions.push(args); + }); + + const resolver = connection(UserModel, UserTC); + + if (!resolver) { + throw new Error('resolver is undefined'); + } + + const result = await resolver.resolve({ + args: {}, + beforeQuery: (query, rp) => { + expect(query).toBeInstanceOf(Query); + expect(rp.model).toBe(UserModel); + // modify query before execution + return query.where({ _id: user1.id }).limit(1989); + }, + }); + + expect(mongooseActions).toEqual([ + [ + 'users', + 'find', + { _id: user1._id }, + { + limit: 1989, + projection: {}, + }, + ], + ]); + + expect(result.edges).toHaveLength(1); + }); + + it('should override result with `beforeQuery`', async () => { + const resolver = connection(UserModel, UserTC); + + if (!resolver) { + throw new Error('resolver is undefined'); + } + + const result = await resolver.resolve({ + args: {}, + beforeQuery: (query, rp) => { + expect(query).toBeInstanceOf(Query); + expect(rp.model).toBe(UserModel); + return [{ overrides: true }]; + }, + }); + + expect(result).toHaveProperty('edges.0.node', { overrides: true }); + }); }); }); }); diff --git a/src/resolvers/__tests__/count-test.js b/src/resolvers/__tests__/count-test.js index 56373350..5a8b1262 100644 --- a/src/resolvers/__tests__/count-test.js +++ b/src/resolvers/__tests__/count-test.js @@ -70,6 +70,50 @@ describe('count() ->', () => { }); expect(result).toBe(1); }); + + it('should call `beforeQuery` method with non-executed `query` as arg', async () => { + const mongooseActions = []; + + UserModel.base.set('debug', function debugMongoose(...args) { + mongooseActions.push(args); + }); + + const result = await count(UserModel, UserTC).resolve({ + args: {}, + beforeQuery: (query, rp) => { + expect(query).toHaveProperty('exec'); + expect(rp.model).toBe(UserModel); + + // modify query before execution + return query.limit(1); + }, + }); + + expect(mongooseActions).toEqual([ + [ + 'users', + 'countDocuments', + {}, + { + limit: 1, + }, + ], + ]); + + expect(result).toBe(1); + }); + + it('should override result with `beforeQuery`', async () => { + const result = await count(UserModel, UserTC).resolve({ + args: {}, + beforeQuery: (query, rp) => { + expect(query).toHaveProperty('exec'); + expect(rp.model).toBe(UserModel); + return 1989; + }, + }); + expect(result).toBe(1989); + }); }); describe('Resolver.getType()', () => { diff --git a/src/resolvers/__tests__/findById-test.js b/src/resolvers/__tests__/findById-test.js index 5f33cb7b..a6215ea8 100644 --- a/src/resolvers/__tests__/findById-test.js +++ b/src/resolvers/__tests__/findById-test.js @@ -87,5 +87,22 @@ describe('findById() ->', () => { }); expect(result).toBeInstanceOf(PostModel); }); + + it('should call `beforeQuery` method with non-executed `query` as arg', async () => { + let beforeQueryCalled = false; + + const result = await findById(PostModel, PostTypeComposer).resolve({ + args: { _id: 1 }, + beforeQuery: (query, rp) => { + expect(query).toHaveProperty('exec'); + expect(rp.model).toBe(PostModel); + beforeQueryCalled = true; + return { overrides: true }; + }, + }); + + expect(beforeQueryCalled).toBe(true); + expect(result).toEqual({ overrides: true }); + }); }); }); diff --git a/src/resolvers/__tests__/findByIds-test.js b/src/resolvers/__tests__/findByIds-test.js index 0d018463..73c2e579 100644 --- a/src/resolvers/__tests__/findByIds-test.js +++ b/src/resolvers/__tests__/findByIds-test.js @@ -121,5 +121,18 @@ describe('findByIds() ->', () => { expect(result[0]).toBeInstanceOf(UserModel); expect(result[1]).toBeInstanceOf(UserModel); }); + + it('should call `beforeQuery` method with non-executed `query` as arg', async () => { + const result = await findByIds(UserModel, UserTC).resolve({ + args: { _ids: [user1._id, user2._id] }, + beforeQuery(query, rp) { + expect(rp.model).toBe(UserModel); + expect(rp.query).toHaveProperty('exec'); + return query.where({ _id: user1._id }); + }, + }); + + expect(result).toHaveLength(1); + }); }); }); diff --git a/src/resolvers/__tests__/findMany-test.js b/src/resolvers/__tests__/findMany-test.js index 33b6b0e8..3db8cb63 100644 --- a/src/resolvers/__tests__/findMany-test.js +++ b/src/resolvers/__tests__/findMany-test.js @@ -110,5 +110,18 @@ describe('findMany() ->', () => { expect(result[0]).toBeInstanceOf(UserModel); expect(result[1]).toBeInstanceOf(UserModel); }); + + it('should call `beforeQuery` method with non-executed `query` as arg', async () => { + const result = await findMany(UserModel, UserTC).resolve({ + args: { limit: 2 }, + beforeQuery(query, rp) { + expect(rp.model).toBe(UserModel); + expect(rp.query).toHaveProperty('exec'); + return [{ overridden: true }]; + }, + }); + + expect(result).toEqual([{ overridden: true }]); + }); }); }); diff --git a/src/resolvers/__tests__/findOne-test.js b/src/resolvers/__tests__/findOne-test.js index 80222938..4e503691 100644 --- a/src/resolvers/__tests__/findOne-test.js +++ b/src/resolvers/__tests__/findOne-test.js @@ -118,6 +118,19 @@ describe('findOne() ->', () => { }); expect(result).toBeInstanceOf(UserModel); }); + + it('should call `beforeQuery` method with non-executed `query` as arg', async () => { + const result = await findOne(UserModel, UserTC).resolve({ + args: { _id: user1._id }, + beforeQuery(query, rp) { + expect(rp.model).toBe(UserModel); + expect(rp.query).toHaveProperty('exec'); + return query.where({ _id: user2._id }); + }, + }); + + expect(result._id).toEqual(user2._id); + }); }); describe('Resolver.getType()', () => { diff --git a/src/resolvers/__tests__/pagination-test.js b/src/resolvers/__tests__/pagination-test.js index 22b54bb4..1c90290a 100644 --- a/src/resolvers/__tests__/pagination-test.js +++ b/src/resolvers/__tests__/pagination-test.js @@ -144,5 +144,21 @@ describe('pagination() ->', () => { expect(result.items[0]).toBeInstanceOf(UserModel); expect(result.items[1]).toBeInstanceOf(UserModel); }); + + it('should call `beforeQuery` method with non-executed `query` as arg', async () => { + const resolver = pagination(UserModel, UserTC); + if (!resolver) throw new Error('Pagination resolver is undefined'); + + const result = await resolver.resolve({ + args: { page: 1, perPage: 20 }, + beforeQuery(query, rp) { + expect(rp.model).toBe(UserModel); + expect(rp.query).toHaveProperty('exec'); + return [{ overrides: true }]; + }, + }); + + expect(result.items).toEqual([{ overrides: true }]); + }); }); }); diff --git a/src/resolvers/__tests__/removeById-test.js b/src/resolvers/__tests__/removeById-test.js index 93aed514..8456b1f7 100644 --- a/src/resolvers/__tests__/removeById-test.js +++ b/src/resolvers/__tests__/removeById-test.js @@ -147,6 +147,32 @@ describe('removeById() ->', () => { const exist = await UserModel.collection.findOne({ _id: user._id }); expect(exist.name).toBe(user.name); }); + + it('should call `beforeQuery` method with non-executed `query` as arg', async () => { + const mongooseActions = []; + + UserModel.base.set('debug', function debugMongoose(...args) { + mongooseActions.push(args); + }); + + const resolveParams = { + args: { _id: 'INVALID_ID' }, + context: { ip: '1.1.1.1' }, + beforeQuery(query, rp) { + expect(rp.model).toBe(UserModel); + expect(rp.query).toHaveProperty('exec'); + return query.where({ _id: user._id, canDelete: false }); + }, + }; + + const result = await removeById(UserModel, UserTC).resolve(resolveParams); + + expect(mongooseActions).toEqual([ + ['users', 'findOne', { _id: user._id, canDelete: false }, { projection: {} }], + ]); + + expect(result).toBeNull(); + }); }); describe('Resolver.getType()', () => { diff --git a/src/resolvers/__tests__/removeMany-test.js b/src/resolvers/__tests__/removeMany-test.js index bfa74135..1643cd3c 100644 --- a/src/resolvers/__tests__/removeMany-test.js +++ b/src/resolvers/__tests__/removeMany-test.js @@ -109,8 +109,9 @@ describe('removeMany() ->', () => { args: { filter: { gender: 'female' }, }, - beforeQuery: query => { + beforeQuery: (query, rp) => { expect(query).toBeInstanceOf(Query); + expect(rp.model).toBe(UserModel); beforeQueryCalled = true; return query; }, diff --git a/src/resolvers/__tests__/removeOne-test.js b/src/resolvers/__tests__/removeOne-test.js index ba7cd4b3..f8e0723d 100644 --- a/src/resolvers/__tests__/removeOne-test.js +++ b/src/resolvers/__tests__/removeOne-test.js @@ -189,6 +189,25 @@ describe('removeOne() ->', () => { const exist = await UserModel.collection.findOne({ _id: user1._id }); expect(exist.name).toBe(user1.name); }); + + it('should call `beforeQuery` method with non-executed `query` as arg', async () => { + let beforeQueryCalled = false; + + const result = await removeOne(UserModel, UserTC).resolve({ + args: { filter: { _id: 'INVALID_ID' } }, + beforeQuery: (query, rp) => { + expect(query).toHaveProperty('exec'); + expect(rp.model).toBe(UserModel); + + beforeQueryCalled = true; + // modify query before execution + return query.where({ _id: user1.id }); + }, + }); + + expect(result).toHaveProperty('record._id', user1._id); + expect(beforeQueryCalled).toBe(true); + }); }); describe('Resolver.getType()', () => { diff --git a/src/resolvers/__tests__/updateById-test.js b/src/resolvers/__tests__/updateById-test.js index a41046f6..560331a2 100644 --- a/src/resolvers/__tests__/updateById-test.js +++ b/src/resolvers/__tests__/updateById-test.js @@ -187,6 +187,25 @@ describe('updateById() ->', () => { const exist = await UserModel.collection.findOne({ _id: user1._id }); expect(exist.name).toBe(user1.name); }); + + it('should call `beforeQuery` method with non-executed `query` as arg', async () => { + let beforeQueryCalled = false; + + const result = await updateById(UserModel, UserTC).resolve({ + args: { record: { _id: user1.id, name: 'new name' } }, + beforeQuery: (query, rp) => { + expect(query).toHaveProperty('exec'); + expect(rp.model).toBe(UserModel); + + beforeQueryCalled = true; + // modify query before execution + return query.where({ _id: user2.id }); + }, + }); + + expect(result).toHaveProperty('record._id', user2._id); + expect(beforeQueryCalled).toBe(true); + }); }); describe('Resolver.getType()', () => { diff --git a/src/resolvers/__tests__/updateMany-test.js b/src/resolvers/__tests__/updateMany-test.js index 15f8e7ae..c6d77141 100644 --- a/src/resolvers/__tests__/updateMany-test.js +++ b/src/resolvers/__tests__/updateMany-test.js @@ -117,8 +117,9 @@ describe('updateMany() ->', () => { args: { record: { gender: 'female' }, }, - beforeQuery: query => { + beforeQuery: (query, rp) => { expect(query).toBeInstanceOf(Query); + expect(rp.model).toBe(UserModel); beforeQueryCalled = true; // modify query before execution return query.where({ _id: user1.id }); diff --git a/src/resolvers/__tests__/updateOne-test.js b/src/resolvers/__tests__/updateOne-test.js index 293aa55a..29bb6932 100644 --- a/src/resolvers/__tests__/updateOne-test.js +++ b/src/resolvers/__tests__/updateOne-test.js @@ -211,6 +211,25 @@ describe('updateOne() ->', () => { const exist = await UserModel.collection.findOne({ _id: user1._id }); expect(exist.name).toBe(user1.name); }); + + it('should call `beforeQuery` method with non-executed `query` as arg', async () => { + let beforeQueryCalled = false; + + const result = await updateOne(UserModel, UserTC).resolve({ + args: { filter: { _id: user1.id }, record: { name: 'new name' } }, + beforeQuery: (query, rp) => { + expect(query).toHaveProperty('exec'); + expect(rp.model).toBe(UserModel); + + beforeQueryCalled = true; + // modify query before execution + return query.where({ _id: user2.id }); + }, + }); + + expect(result).toHaveProperty('record._id', user2._id); + expect(beforeQueryCalled).toBe(true); + }); }); describe('Resolver.getType()', () => { diff --git a/src/resolvers/count.js b/src/resolvers/count.js index a780e64d..b72e6dbd 100644 --- a/src/resolvers/count.js +++ b/src/resolvers/count.js @@ -5,6 +5,7 @@ import type { Resolver, ObjectTypeComposer } from 'graphql-compose'; import type { MongooseDocument } from 'mongoose'; import { filterHelper, filterHelperArgs } from './helpers'; import type { ExtendedResolveParams, GenResolverOpts } from './index'; +import { beforeQueryHelper } from './helpers/beforeQueryHelper'; export default function count( model: Class, // === MongooseModel @@ -32,13 +33,16 @@ export default function count( }, resolve: (resolveParams: ExtendedResolveParams) => { resolveParams.query = model.find(); + resolveParams.model = model; filterHelper(resolveParams); if (resolveParams.query.countDocuments) { // mongoose 5.2.0 and above - return resolveParams.query.countDocuments().exec(); + resolveParams.query.countDocuments(); + return beforeQueryHelper(resolveParams); } else { // mongoose 5 and below - return resolveParams.query.count().exec(); + resolveParams.query.count(); + return beforeQueryHelper(resolveParams); } }, }); diff --git a/src/resolvers/findById.js b/src/resolvers/findById.js index dfd4de64..4d6679dd 100644 --- a/src/resolvers/findById.js +++ b/src/resolvers/findById.js @@ -4,6 +4,7 @@ import type { Resolver, ObjectTypeComposer } from 'graphql-compose'; import type { MongooseDocument } from 'mongoose'; import { projectionHelper } from './helpers'; import type { ExtendedResolveParams, GenResolverOpts } from './index'; +import { beforeQueryHelper } from './helpers/beforeQueryHelper'; export default function findById( model: Class, // === MongooseModel @@ -30,8 +31,9 @@ export default function findById( if (args._id) { resolveParams.query = model.findById(args._id); // eslint-disable-line + resolveParams.model = model; // eslint-disable-line projectionHelper(resolveParams); - return resolveParams.query.exec(); + return beforeQueryHelper(resolveParams); } return Promise.resolve(null); }, diff --git a/src/resolvers/findByIds.js b/src/resolvers/findByIds.js index cc906c60..316d2f63 100644 --- a/src/resolvers/findByIds.js +++ b/src/resolvers/findByIds.js @@ -10,6 +10,7 @@ import { projectionHelper, } from './helpers'; import type { ExtendedResolveParams, GenResolverOpts } from './index'; +import { beforeQueryHelper } from './helpers/beforeQueryHelper'; export default function findByIds( model: Class, // === MongooseModel @@ -52,10 +53,11 @@ export default function findByIds( }; resolveParams.query = model.find(selector); // eslint-disable-line + resolveParams.model = model; // eslint-disable-line projectionHelper(resolveParams); limitHelper(resolveParams); sortHelper(resolveParams); - return resolveParams.query.exec(); + return beforeQueryHelper(resolveParams); }, }); } diff --git a/src/resolvers/findMany.js b/src/resolvers/findMany.js index 30fa36bd..0ecc1668 100644 --- a/src/resolvers/findMany.js +++ b/src/resolvers/findMany.js @@ -15,6 +15,7 @@ import { projectionHelper, } from './helpers'; import type { ExtendedResolveParams, GenResolverOpts } from './index'; +import { beforeQueryHelper } from './helpers/beforeQueryHelper'; export default function findMany( model: Class, // === MongooseModel @@ -50,12 +51,13 @@ export default function findMany( }, resolve: (resolveParams: ExtendedResolveParams) => { resolveParams.query = model.find(); + resolveParams.model = model; filterHelper(resolveParams); skipHelper(resolveParams); limitHelper(resolveParams); sortHelper(resolveParams); projectionHelper(resolveParams); - return resolveParams.query.exec(); + return beforeQueryHelper(resolveParams); }, }); } diff --git a/src/resolvers/findOne.js b/src/resolvers/findOne.js index c875fd05..66b4934c 100644 --- a/src/resolvers/findOne.js +++ b/src/resolvers/findOne.js @@ -13,6 +13,7 @@ import { projectionHelper, } from './helpers'; import type { ExtendedResolveParams, GenResolverOpts } from './index'; +import { beforeQueryHelper } from './helpers/beforeQueryHelper'; export default function findOne( model: Class, // === MongooseModel @@ -45,12 +46,12 @@ export default function findOne( }, resolve: (resolveParams: ExtendedResolveParams) => { resolveParams.query = model.findOne({}); // eslint-disable-line + resolveParams.model = model; filterHelper(resolveParams); skipHelper(resolveParams); sortHelper(resolveParams); projectionHelper(resolveParams); - - return resolveParams.query.exec(); + return beforeQueryHelper(resolveParams); }, }); } diff --git a/src/resolvers/helpers/__tests__/beforeQueryHelper-test.js b/src/resolvers/helpers/__tests__/beforeQueryHelper-test.js new file mode 100644 index 00000000..ed7ea11a --- /dev/null +++ b/src/resolvers/helpers/__tests__/beforeQueryHelper-test.js @@ -0,0 +1,47 @@ +/* @flow */ + +import { beforeQueryHelper } from '../beforeQueryHelper'; +import { UserModel } from '../../../__mocks__/userModel'; + +describe('Resolver helper `beforeQueryHelper` ->', () => { + let spyExec; + let spyWhere; + let resolveParams: any; + + beforeEach(() => { + spyWhere = jest.fn(); + spyExec = jest.fn(() => Promise.resolve('EXEC_RETURN')); + + resolveParams = { + query: { + exec: spyExec, + where: spyWhere, + }, + model: UserModel, + }; + }); + + it('should return query.exec() if `resolveParams.beforeQuery` is empty', async () => { + const result = await beforeQueryHelper(resolveParams); + expect(result).toBe('EXEC_RETURN'); + }); + + it('should call the `exec` method of `beforeQuery` return', async () => { + resolveParams.beforeQuery = function beforeQuery() { + return { + exec: () => Promise.resolve('changed'), + }; + }; + + const result = await beforeQueryHelper(resolveParams); + expect(result).toBe('changed'); + }); + + it('should return the complete payload if not a Query', async () => { + resolveParams.beforeQuery = function beforeQuery() { + return 'NOT_A_QUERY'; + }; + + expect(await beforeQueryHelper(resolveParams)).toBe('NOT_A_QUERY'); + }); +}); diff --git a/src/resolvers/helpers/beforeQueryHelper.js b/src/resolvers/helpers/beforeQueryHelper.js new file mode 100644 index 00000000..187e06e7 --- /dev/null +++ b/src/resolvers/helpers/beforeQueryHelper.js @@ -0,0 +1,23 @@ +import type { ExtendedResolveParams } from '../index'; + +export async function beforeQueryHelper(resolveParams: ExtendedResolveParams): Promise { + if (!resolveParams.beforeQuery) { + return resolveParams.query.exec(); + } + + if (!resolveParams.query || typeof resolveParams.query.exec !== 'function') { + throw new Error('beforeQueryHelper: expected resolveParams.query to be intance of Query'); + } + + if (!resolveParams.model || !resolveParams.model.modelName || !resolveParams.model.schema) { + throw new Error('beforeQueryHelper: resolveParams.model should be instance of Mongoose Model.'); + } + + const result = await resolveParams.beforeQuery(resolveParams.query, resolveParams); + + if (result && typeof result.exec === 'function') { + return result.exec(); + } + + return result; +} diff --git a/src/resolvers/index.d.ts b/src/resolvers/index.d.ts index 12c37ee1..1ff96490 100644 --- a/src/resolvers/index.d.ts +++ b/src/resolvers/index.d.ts @@ -1,5 +1,5 @@ import { ResolverResolveParams } from 'graphql-compose'; -import { DocumentQuery } from 'mongoose'; +import { DocumentQuery, Model, Query } from 'mongoose'; import connection from './connection'; import count from './count'; @@ -41,6 +41,7 @@ export type ExtendedResolveParams = ResolverResolveParams & { rawQuery: { [optName: string]: any }; beforeQuery?: (query: any, rp: ExtendedResolveParams) => Promise; beforeRecordMutate?: (record: any, rp: ExtendedResolveParams) => Promise; + model: Model; }; export { diff --git a/src/resolvers/removeMany.js b/src/resolvers/removeMany.js index 3926b193..41e6311e 100644 --- a/src/resolvers/removeMany.js +++ b/src/resolvers/removeMany.js @@ -5,6 +5,7 @@ import type { Resolver, ObjectTypeComposer } from 'graphql-compose'; import type { MongooseDocument } from 'mongoose'; import { filterHelperArgs, filterHelper } from './helpers'; import type { ExtendedResolveParams, GenResolverOpts } from './index'; +import { beforeQueryHelper } from './helpers/beforeQueryHelper'; export default function removeMany( model: Class, // === MongooseModel @@ -57,6 +58,7 @@ export default function removeMany( } resolveParams.query = model.find(); + resolveParams.model = model; filterHelper(resolveParams); if (resolveParams.query.deleteMany) { @@ -66,16 +68,7 @@ export default function removeMany( resolveParams.query = resolveParams.query.remove(); } - let res; - - // `beforeQuery` is experemental feature, if you want to use it - // please open an issue with your use case, cause I suppose that - // this option is excessive - if (resolveParams.beforeQuery) { - res = await resolveParams.beforeQuery(resolveParams.query, resolveParams); - } else { - res = await resolveParams.query.exec(); - } + const res = await beforeQueryHelper(resolveParams); if (res.ok) { // mongoose 5 diff --git a/src/resolvers/updateMany.js b/src/resolvers/updateMany.js index c238f70e..a3810d12 100644 --- a/src/resolvers/updateMany.js +++ b/src/resolvers/updateMany.js @@ -16,6 +16,7 @@ import { } from './helpers'; import { toMongoDottedObject } from '../utils/toMongoDottedObject'; import type { ExtendedResolveParams, GenResolverOpts } from './index'; +import { beforeQueryHelper } from './helpers/beforeQueryHelper'; export default function updateMany( model: Class, // === MongooseModel @@ -83,6 +84,7 @@ export default function updateMany( } resolveParams.query = model.find(); + resolveParams.model = model; filterHelper(resolveParams); skipHelper(resolveParams); sortHelper(resolveParams); @@ -97,17 +99,7 @@ export default function updateMany( resolveParams.query.update({ $set: toMongoDottedObject(recordData) }); } - let res; - - // `beforeQuery` is experemental feature, if you want to use it - // please open an issue with your use case, cause I suppose that - // this option is excessive - - if (resolveParams.beforeQuery) { - res = await resolveParams.beforeQuery(resolveParams.query, resolveParams); - } else { - res = await resolveParams.query.exec(); - } + const res = await beforeQueryHelper(resolveParams); if (res.ok) { return {