Skip to content

Commit

Permalink
feat(hooks): support query, mutation hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
Andras Toth committed Nov 13, 2015
1 parent c424eca commit a822459
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 36 deletions.
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,14 +182,49 @@ 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: {
type: String,
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);
Expand Down
15 changes: 14 additions & 1 deletion example/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
96 changes: 95 additions & 1 deletion src/e2e.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {expect} from 'chai';
import {spy} from 'sinon';

import {getSchema, graphql} from './';
import User from '../fixture/user';
Expand All @@ -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({
Expand Down Expand Up @@ -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
});
});
});
});
4 changes: 4 additions & 0 deletions src/model/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
description: String?,
nonNull: Boolean?, // required?
hidden: Boolean? // included in the GraphQL schema
hooks: {
pre: (Function|Array<Function>)?
post: (Function|Array<Function>)?
}?
type: String('String'|'Number'|'Date'|'Buffer'|'Boolean'|'ObjectID'|'Object'|'Array'),
// if type == Array
subtype: String('String'|'Number'|'Date'|'Buffer'|'Boolean'|'ObjectID'|'Object'|'Array'),
Expand Down
50 changes: 28 additions & 22 deletions src/schema/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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: {
Expand All @@ -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) => {
Expand Down Expand Up @@ -154,7 +159,7 @@ function getMutationField(graffitiModel, type, viewer) {
})
}
},
mutateAndGetPayload: getAddOneMutateHandler(graffitiModel)
mutateAndGetPayload: addHooks(getAddOneMutateHandler(graffitiModel), mutation)
}),
[updateName]: mutationWithClientMutationId({
name: updateName,
Expand All @@ -171,7 +176,7 @@ function getMutationField(graffitiModel, type, viewer) {
resolve: (node) => node
}
},
mutateAndGetPayload: getUpdateOneMutateHandler(graffitiModel)
mutateAndGetPayload: addHooks(getUpdateOneMutateHandler(graffitiModel), mutation)
}),
[deleteName]: mutationWithClientMutationId({
name: deleteName,
Expand All @@ -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) => {
Expand All @@ -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)
}
};
}, {
Expand All @@ -243,7 +249,7 @@ function getFields(graffitiModels, {mutation = true} = {}) {
description: 'The ID of an object'
}
},
resolve: getIdFetcher(graffitiModels)
resolve: addHooks(getIdFetcher(graffitiModels), singular)
},
...queries
}
Expand Down
12 changes: 1 addition & 11 deletions src/type/type.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 = {};

Expand Down
Loading

0 comments on commit a822459

Please sign in to comment.