text: const {forEachField} = require('./utils');
  const debug = require('debug')('talk:graph:schema');
  const Joi = require('joi');
  
  /**
   * 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 field with the post resolvers (if available) and attaches a
   * default type in the form of `Default${typeName}`.
   */
  const decorateResolveFunction = (field, typeName, fieldName, post) => {
  
    // Cache the original resolverType function.
    let resolveType = field.resolveType;
  
    // defaultResolveType is the default type that is resolved on a resolver
    // when the interface being looked up is not defined.
    const defaultResolveType = `Default${typeName}`;
  
    // Return the function to handle the resolveType hooks.
    const defaultResolveFn = (obj, context, info) => {
      let type = resolveType(obj, context, info);
  
      // Only if a previous resolver was unable to resolve the field type do we
      // progress to the hooks (in order!) to resolve the field name until we
      // have resolved it.
      if (typeof type !== 'undefined' && type != null) {
        return type;
      }
  
      // All else fails, resort to the defaultResolveType.
      return defaultResolveType;
    };
  
    // This only needs to do something if post hooks are defined.
    if (post.length === 0) {
  
      // Set the default on the resolveType function.
      field.resolveType = defaultResolveFn;
  
      return;
    }
  
    // Ensure it matches the format we expect.
    Joi.assert(post, Joi.array().items(Joi.func().maxArity(3)), `invalid post hooks were found for ${typeName}.${fieldName}`);
  
    // Return the function to handle the resolveType hooks.
    field.resolveType = (obj, context, info) => {
      let type = defaultResolveFn(obj, context, info);
  
      // Only if a previous resolver was unable to resolve the field type do we
      // progress to the hooks (in order!) to resolve the field name until we
      // have resolved it.
      if (typeof type !== 'undefined' && type != null && type !== defaultResolveType) {
        return type;
      }
  
      // We will walk through the post hooks until we find the right one. This
      // follows what redux does to combine existing reducers.
      for (let i = 0; i < post.length; i++) {
        let resolveType = post[i];
        let resolvedType = resolveType(obj, context, info);
        if (typeof resolvedType !== 'undefined' && resolvedType != null) {
          return resolvedType;
        }
      }
  
      return type;
    };
  };
  
  /**
   * 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, isResolveType = false) => {
  
    // 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':
            Joi.assert(hooks.pre, Joi.func().maxArity(4));
  
            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':
            Joi.assert(hooks.pre, Joi.func().maxArity(5));
  
            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 this is a resolve type, we need to do some specific things to handle
    // this type of field.
    if (isResolveType) {
  
      // Warn if we have any pre hooks.
      if (pre.length !== 0) {
        throw new Error(`invalid pre hooks were found for ${typeName}.${fieldName}, only post hooks are supported on the __resolveType hook`);
      }
  
      // Decorate the resolve function on the field with the new resolveType func.
      decorateResolveFunction(field, typeName, fieldName, post);
      return;
    }
  
    // 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 = await 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);
    };
  }, {
    includeResolveType: true,
  });
  
  module.exports = {
    decorateWithHooks
  };
  