From a8224595ad2f17094954d8f4be0b3b9c3c8d7cf6 Mon Sep 17 00:00:00 2001 From: Andras Toth Date: Fri, 13 Nov 2015 10:38:36 +0100 Subject: [PATCH] feat(hooks): support query, mutation hooks --- README.md | 35 ++++++++++++++++ example/app.js | 15 ++++++- src/e2e.spec.js | 96 +++++++++++++++++++++++++++++++++++++++++++- src/model/README.md | 4 ++ src/schema/schema.js | 50 +++++++++++++---------- src/type/type.js | 12 +----- src/utils/index.js | 13 +++++- 7 files changed, 189 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 2227e5b..ab30816 100644 --- a/README.md +++ b/README.md @@ -182,7 +182,41 @@ mutation deleteX { You can specify pre- and post-resolve hooks on fields in order to manipulate arguments and data passed in to the database resolve function, and returned by the GraphQL resolve function. +You can add hooks to type fields and query fields (singular & plural queries, mutations) too. + Examples: +- Query, mutation hooks (`viewer`, `singular`, `plural`, `mutation`) +```javascript +const hooks = { + viewer: { + pre: (next, root, args, {rootValue}) => { + const {request} = rootValue; + // authorize the logged in user based on the request + authorize(request); + next(); + }, + post: (next, value) => { + console.log(value); + next(); + } + }, + // singular: { + // pre: (next, root, args, {rootValue}) => next(), + // post: (next, value) => next() + // }, + // plural: { + // pre: (next, root, args, {rootValue}) => next(), + // post: (next, value) => next() + // }, + // mutation: { + // pre: (next, args, {rootValue}) => next(), + // post: (next, value) => next() + // } +}; +const schema = getSchema([User], {hooks}); +``` + +- Field hooks ```javascript const UserSchema = new mongoose.Schema({ name: { @@ -190,6 +224,7 @@ const UserSchema = new mongoose.Schema({ hooks: { pre: (next, root, args, {rootValue}) => { const {request} = rootValue; + // authorize the logged in user based on the request // throws error if the user has no right to request the user names authorize(request); diff --git a/example/app.js b/example/app.js index 36b8790..1114cf5 100644 --- a/example/app.js +++ b/example/app.js @@ -31,7 +31,20 @@ User.remove().then(() => { User.create(users); }); -const schema = getSchema([User]); +const hooks = { + viewer: { + pre: (next, root, args, {rootValue}) => { + const {request} = rootValue; + console.log(request); + next(); + }, + post: (next, value) => { + console.log(value); + next(); + } + } +}; +const schema = getSchema([User], {hooks}); // Set up example server const app = koa(); diff --git a/src/e2e.spec.js b/src/e2e.spec.js index fc09830..1f70233 100644 --- a/src/e2e.spec.js +++ b/src/e2e.spec.js @@ -1,4 +1,5 @@ import {expect} from 'chai'; +import {spy} from 'sinon'; import {getSchema, graphql} from './'; import User from '../fixture/user'; @@ -8,7 +9,26 @@ describe('e2e', () => { let motherUser; let user1; let user2; - const schema = getSchema([User]); + const hooks = { + viewer: { + pre: spy(), + post: spy() + }, + singular: { + pre: spy(), + post: spy() + }, + plural: { + pre: spy(), + post: spy() + }, + mutation: { + pre: spy(), + post: spy() + } + }; + const options = {hooks}; + const schema = getSchema([User], options); beforeEach(async function BeforeEach() { motherUser = new User({ @@ -338,5 +358,79 @@ describe('e2e', () => { }); }); }); + + describe('hooks', () => { + it('should call viewer hooks on a viewer query', async () => { + const {pre, post} = hooks.viewer; + pre.reset(); + post.reset(); + + expect(pre.called).to.be.false; // eslint-disable-line + expect(post.called).to.be.false; // eslint-disable-line + await graphql(schema, `{ + viewer { + users { + count + } + } + }`); + expect(pre.called).to.be.true; // eslint-disable-line + expect(post.called).to.be.true; // eslint-disable-line + }); + + it('should call singular hooks on a singular query', async () => { + const {pre, post} = hooks.singular; + pre.reset(); + post.reset(); + + expect(pre.called).to.be.false; // eslint-disable-line + expect(post.called).to.be.false; // eslint-disable-line + await graphql(schema, `{ + user(id: "${user2._id}") { + _id + } + }`); + expect(pre.called).to.be.true; // eslint-disable-line + expect(post.called).to.be.true; // eslint-disable-line + }); + + it('should call plural hooks on a plural query', async () => { + const {pre, post} = hooks.plural; + pre.reset(); + post.reset(); + + expect(pre.called).to.be.false; // eslint-disable-line + expect(post.called).to.be.false; // eslint-disable-line + await graphql(schema, `{ + users(age: 28) { + _id + } + }`); + expect(pre.called).to.be.true; // eslint-disable-line + expect(post.called).to.be.true; // eslint-disable-line + }); + + it('should call mutation hooks on a mutation', async () => { + const {pre, post} = hooks.mutation; + pre.reset(); + post.reset(); + + expect(pre.called).to.be.false; // eslint-disable-line + expect(post.called).to.be.false; // eslint-disable-line + await graphql(schema, ` + mutation addUserMutation { + addUser(input: {name: "Test User", clientMutationId: "1"}) { + changedUserEdge { + node { + id + } + } + } + } + `); + expect(pre.called).to.be.true; // eslint-disable-line + expect(post.called).to.be.true; // eslint-disable-line + }); + }); }); }); diff --git a/src/model/README.md b/src/model/README.md index b7cb8a3..3447ae7 100644 --- a/src/model/README.md +++ b/src/model/README.md @@ -11,6 +11,10 @@ description: String?, nonNull: Boolean?, // required? hidden: Boolean? // included in the GraphQL schema + hooks: { + pre: (Function|Array)? + post: (Function|Array)? + }? type: String('String'|'Number'|'Date'|'Buffer'|'Boolean'|'ObjectID'|'Object'|'Array'), // if type == Array subtype: String('String'|'Number'|'Date'|'Buffer'|'Boolean'|'ObjectID'|'Object'|'Array'), diff --git a/src/schema/schema.js b/src/schema/schema.js index 0aa67e2..2f4708a 100644 --- a/src/schema/schema.js +++ b/src/schema/schema.js @@ -32,15 +32,17 @@ import { getDeleteOneMutateHandler, connectionFromModel } from './../query'; -import viewer from '../model/viewer'; +import {addHooks} from '../utils'; +import viewerInstance from '../model/viewer'; const idField = { name: 'id', type: new GraphQLNonNull(GraphQLID) }; -function getSingularQueryField(graffitiModel, type) { +function getSingularQueryField(graffitiModel, type, hooks = {}) { const {name} = type; + const {singular} = hooks; const singularName = name.toLowerCase(); return { @@ -49,13 +51,14 @@ function getSingularQueryField(graffitiModel, type) { args: { id: idField }, - resolve: getOneResolver(graffitiModel) + resolve: addHooks(getOneResolver(graffitiModel), singular) } }; } -function getPluralQueryField(graffitiModel, type) { +function getPluralQueryField(graffitiModel, type, hooks = {}) { const {name} = type; + const {plural} = hooks; const pluralName = `${name.toLowerCase()}s`; return { @@ -71,20 +74,21 @@ function getPluralQueryField(graffitiModel, type) { description: `The ID of a ${name}` } }), - resolve: getListResolver(graffitiModel) + resolve: addHooks(getListResolver(graffitiModel), plural) } }; } -function getQueryField(graffitiModel, type) { +function getQueryField(graffitiModel, type, hooks) { return { - ...getSingularQueryField(graffitiModel, type), - ...getPluralQueryField(graffitiModel, type) + ...getSingularQueryField(graffitiModel, type, hooks), + ...getPluralQueryField(graffitiModel, type, hooks) }; } -function getConnectionField(graffitiModel, type) { +function getConnectionField(graffitiModel, type, hooks = {}) { const {name} = type; + const {plural} = hooks; const pluralName = `${name.toLowerCase()}s`; const {connectionType} = connectionDefinitions({name: name, nodeType: type, connectionFields: { count: { @@ -97,13 +101,14 @@ function getConnectionField(graffitiModel, type) { [pluralName]: { args: getArguments(type, connectionArgs), type: connectionType, - resolve: (rootValue, args, info) => connectionFromModel(graffitiModel, args, info) + resolve: addHooks((rootValue, args, info) => connectionFromModel(graffitiModel, args, info), plural) } }; } -function getMutationField(graffitiModel, type, viewer) { +function getMutationField(graffitiModel, type, viewer, hooks = {}) { const {name} = type; + const {mutation} = hooks; const fields = getTypeFields(type); const inputFields = reduce(fields, (inputFields, field) => { @@ -154,7 +159,7 @@ function getMutationField(graffitiModel, type, viewer) { }) } }, - mutateAndGetPayload: getAddOneMutateHandler(graffitiModel) + mutateAndGetPayload: addHooks(getAddOneMutateHandler(graffitiModel), mutation) }), [updateName]: mutationWithClientMutationId({ name: updateName, @@ -171,7 +176,7 @@ function getMutationField(graffitiModel, type, viewer) { resolve: (node) => node } }, - mutateAndGetPayload: getUpdateOneMutateHandler(graffitiModel) + mutateAndGetPayload: addHooks(getUpdateOneMutateHandler(graffitiModel), mutation) }), [deleteName]: mutationWithClientMutationId({ name: deleteName, @@ -185,30 +190,31 @@ function getMutationField(graffitiModel, type, viewer) { }, id: idField }, - mutateAndGetPayload: getDeleteOneMutateHandler(graffitiModel) + mutateAndGetPayload: addHooks(getDeleteOneMutateHandler(graffitiModel), mutation) }) }; } -function getFields(graffitiModels, {mutation = true} = {}) { +function getFields(graffitiModels, {hooks = {}, mutation = true} = {}) { const types = getTypes(graffitiModels); + const {viewer, singular} = hooks; GraphQLViewer._typeConfig.fields = reduce(types, (fields, type, key) => { type.name = type.name || key; const graffitiModel = graffitiModels[type.name]; return { ...fields, - ...getConnectionField(graffitiModel, type), - ...getSingularQueryField(graffitiModel, type) + ...getConnectionField(graffitiModel, type, hooks), + ...getSingularQueryField(graffitiModel, type, hooks) }; }, { id: globalIdField('Viewer') }); const viewerField = { - name: viewer, + name: 'Viewer', type: GraphQLViewer, - resolve: () => viewer + resolve: addHooks(() => viewerInstance, viewer) }; const {queries, mutations} = reduce(types, ({queries, mutations}, type, key) => { @@ -217,11 +223,11 @@ function getFields(graffitiModels, {mutation = true} = {}) { return { queries: { ...queries, - ...getQueryField(graffitiModel, type) + ...getQueryField(graffitiModel, type, hooks) }, mutations: { ...mutations, - ...getMutationField(graffitiModel, type, viewerField) + ...getMutationField(graffitiModel, type, viewerField, hooks) } }; }, { @@ -243,7 +249,7 @@ function getFields(graffitiModels, {mutation = true} = {}) { description: 'The ID of an object' } }, - resolve: getIdFetcher(graffitiModels) + resolve: addHooks(getIdFetcher(graffitiModels), singular) }, ...queries } diff --git a/src/type/type.js b/src/type/type.js index 0d630b8..473e202 100644 --- a/src/type/type.js +++ b/src/type/type.js @@ -19,7 +19,7 @@ import { GraphQLNonNull, GraphQLScalarType } from 'graphql/type'; -import {Middleware} from '../utils'; +import {addHooks} from '../utils'; import GraphQLDate from './custom/date'; import GraphQLBuffer from './custom/buffer'; import GraphQLGeneric from './custom/generic'; @@ -92,16 +92,6 @@ function getArguments(type, args = {}) { }, args); } -function addHooks(resolver, {pre, post} = {}) { - return async function resolve(...args) { - const preMiddleware = new Middleware(pre); - await preMiddleware.compose(...args); - const postMiddleware = new Middleware(post); - const result = await resolver(...args); - return await postMiddleware.compose(result) || result; - }; -} - // holds references to fields that later has to be resolved const resolveReference = {}; diff --git a/src/utils/index.js b/src/utils/index.js index 0e6d0d8..1366700 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -1,5 +1,16 @@ import Middleware from './Middleware'; +function addHooks(resolver, {pre, post} = {}) { + return async function resolve(...args) { + const preMiddleware = new Middleware(pre); + await preMiddleware.compose(...args); + const postMiddleware = new Middleware(post); + const result = await resolver(...args); + return await postMiddleware.compose(result) || result; + }; +} + export default { - Middleware + Middleware, + addHooks };