Files
talk/graph/hooks.js
T
2017-03-20 07:36:15 -07:00

133 lines
4.6 KiB
JavaScript

const {forEachField} = require('graphql-tools');
const debug = require('debug')('talk:graph:schema');
/**
* XXX taken from graphql-js: src/execution/execute.js, because that function
* is not exported
*
* If a resolve function is not given, then a default resolve behavior is used
* which takes the property of the source object of the same name as the field
* and returns it as the result, or if it's a function, returns the result
* of calling that function.
*/
const defaultResolveFn = (source, args, context, {fieldName}) => {
// ensure source is a value for which property access is acceptable.
if (typeof source === 'object' || typeof source === 'function') {
const property = source[fieldName];
if (typeof property === 'function') {
return source[fieldName](args, context);
}
return property;
}
};
/**
* Decorates the schema with pre and post hooks as provided by the Plugin
* Manager.
* @param {GraphQLSchema} schema the schema to decorate
* @param {Array} hooks hooks to apply to the schema
* @return {void}
*/
const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeName, fieldName) => {
// Pull out the pre/post hooks from the available hooks.
const {
pre,
post
} = hooks
// Only grab hooks that are associated with thie field and typeName.
.filter(({hooks}) => (typeName in hooks) && (fieldName in hooks[typeName]))
// Grab the hooks we need.
.map(({plugin, hooks}) => ({plugin, hooks: hooks[typeName][fieldName]}))
// Combine the pre/post hooks from each plugin into an array we can
// execute.
.reduce((acc, {plugin, hooks}) => {
// Itterate over the hooks on the fields and look at it with a switch
// block to check for misconfigured plugins.
Object.keys(hooks).forEach((hook) => {
switch (hook) {
case 'pre':
debug(`adding pre hook to resolver ${typeName}.${fieldName} from plugin '${plugin.name}'`);
if (typeof hooks.pre !== 'function') {
throw new Error(`expected ${hook} hook on resolver ${typeName}.${fieldName} from plugin '${plugin.name}' to be a function, it was a '${typeof hooks[hook]}'`);
}
acc.pre.push(hooks.pre);
break;
case 'post':
debug(`adding post hook to resolver ${typeName}.${fieldName} from plugin '${plugin.name}'`);
if (typeof hooks.post !== 'function') {
throw new Error(`expected ${hook} hook on resolver ${typeName}.${fieldName} from plugin '${plugin.name}' to be a function, it was a '${typeof hooks[hook]}'`);
}
acc.post.unshift(hooks.post);
break;
default:
throw new Error(`invalid hook '${hook}' on resolver ${typeName}.${fieldName} from plugin '${plugin.name}'`);
}
});
return acc;
}, {
pre: [],
post: []
});
// If we have no hooks to add here, don't try to modify anything.
if (pre.length === 0 && post.length === 0) {
return;
}
// Cache the original resolve function, this emulates the beheviour found in
// graphql-tools: https://github.com/apollographql/graphql-tools/blob/6e9cc124b10d673448386041e6c3d058bc205a02/src/schemaGenerator.ts#L423-L425
let resolve = field.resolve;
if (typeof resolve === 'undefined') {
resolve = defaultResolveFn;
}
// Apply our async resolve function which will fire all pre functions (and
// wait until they resolve) followed by waiting for the response and then
// firing their post hooks. Lastly, we respond with the result of the
// original resolver.
field.resolve = async (obj, args, context, info) => {
// Issue all pre hooks before we resolve the field.
await Promise.all(pre.map((pre) => pre(obj, args, context, info)));
// Resolve the field.
let result = resolve(obj, args, context, info);
// Insure all post hooks after we've resolved the field with the result
// passed in as the fifth argument.
return await post.reduce(async (result, post) => {
// Wait for the accumulator to resolve before we continue.
result = await result;
// Check to see if this post function accepts a result, if it does, we
// expect that it modifies the result, otherwise, just fire the post hook,
// wait till it's done, then move onto the next hook.
if (post.length === 5) {
return await post(obj, args, context, info, result);
}
// Wait for the post hook to finish.
await post(obj, args, context, info);
// Return the result, which we already awaited for before.
return result;
}, result);
};
});
module.exports = {
decorateWithHooks
};