mirror of
				https://github.com/strapi/strapi.git
				synced 2025-11-04 11:54:10 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			309 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			309 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
'use strict';
 | 
						|
 | 
						|
/**
 | 
						|
 * Query.js service
 | 
						|
 *
 | 
						|
 * @description: A set of functions similar to controller's actions to avoid code duplication.
 | 
						|
 */
 | 
						|
 | 
						|
const _ = require('lodash');
 | 
						|
const pluralize = require('pluralize');
 | 
						|
const policyUtils = require('strapi-utils').policy;
 | 
						|
const compose = require('koa-compose');
 | 
						|
 | 
						|
module.exports = {
 | 
						|
  /**
 | 
						|
   * Convert parameters to valid filters parameters.
 | 
						|
   *
 | 
						|
   * @return Object
 | 
						|
   */
 | 
						|
 | 
						|
  convertToParams: params => {
 | 
						|
    return Object.keys(params).reduce((acc, current) => {
 | 
						|
      const key = current === 'id' ? 'id' : `_${current}`;
 | 
						|
      acc[key] = params[current];
 | 
						|
      return acc;
 | 
						|
    }, {});
 | 
						|
  },
 | 
						|
 | 
						|
  convertToQuery: function(params) {
 | 
						|
    const result = {};
 | 
						|
 | 
						|
    _.forEach(params, (value, key) => {
 | 
						|
      if (_.isPlainObject(value)) {
 | 
						|
        const flatObject = this.convertToQuery(value);
 | 
						|
        _.forEach(flatObject, (_value, _key) => {
 | 
						|
          result[`${key}.${_key}`] = _value;
 | 
						|
        });
 | 
						|
      } else {
 | 
						|
        result[key] = value;
 | 
						|
      }
 | 
						|
    });
 | 
						|
 | 
						|
    return result;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Security to avoid infinite limit.
 | 
						|
   *
 | 
						|
   * @return String
 | 
						|
   */
 | 
						|
 | 
						|
  amountLimiting: (params = {}) => {
 | 
						|
    const { amountLimit } = strapi.plugins.graphql.config;
 | 
						|
 | 
						|
    if (!amountLimit) return params;
 | 
						|
 | 
						|
    if (!params.limit || params.limit === -1 || params.limit > amountLimit) {
 | 
						|
      params.limit = amountLimit;
 | 
						|
    } else if (params.limit < 0) {
 | 
						|
      params.limit = 0;
 | 
						|
    }
 | 
						|
 | 
						|
    return params;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Execute policies before the specified resolver.
 | 
						|
   *
 | 
						|
   * @return Promise or Error.
 | 
						|
   */
 | 
						|
 | 
						|
  composeQueryResolver: function({ _schema, plugin, name, isSingular }) {
 | 
						|
    const params = {
 | 
						|
      model: name,
 | 
						|
    };
 | 
						|
 | 
						|
    // Extract custom resolver or type description.
 | 
						|
    const { resolver: handler = {} } = _schema;
 | 
						|
 | 
						|
    let queryName;
 | 
						|
 | 
						|
    if (isSingular === 'force') {
 | 
						|
      queryName = name;
 | 
						|
    } else {
 | 
						|
      queryName = isSingular
 | 
						|
        ? pluralize.singular(name)
 | 
						|
        : pluralize.plural(name);
 | 
						|
    }
 | 
						|
 | 
						|
    // Retrieve policies.
 | 
						|
    const policies = _.get(handler, `Query.${queryName}.policies`, []);
 | 
						|
 | 
						|
    // Retrieve resolverOf.
 | 
						|
    const resolverOf = _.get(handler, `Query.${queryName}.resolverOf`, '');
 | 
						|
 | 
						|
    const policiesFn = [];
 | 
						|
 | 
						|
    // Boolean to define if the resolver is going to be a controller or not.
 | 
						|
    let isController = false;
 | 
						|
 | 
						|
    // Retrieve resolver. It could be the custom resolver of the user
 | 
						|
    // or the shadow CRUD resolver (aka Content-Manager).
 | 
						|
    const resolver = (() => {
 | 
						|
      // Try to retrieve custom resolver.
 | 
						|
      const resolver = _.get(handler, `Query.${queryName}.resolver`);
 | 
						|
 | 
						|
      if (_.isString(resolver) || _.isPlainObject(resolver)) {
 | 
						|
        const { handler = resolver } = _.isPlainObject(resolver)
 | 
						|
          ? resolver
 | 
						|
          : {};
 | 
						|
 | 
						|
        // Retrieve the controller's action to be executed.
 | 
						|
        const [name, action] = handler.split('.');
 | 
						|
 | 
						|
        const controller = plugin
 | 
						|
          ? _.get(
 | 
						|
              strapi.plugins,
 | 
						|
              `${plugin}.controllers.${_.toLower(name)}.${action}`
 | 
						|
            )
 | 
						|
          : _.get(strapi.controllers, `${_.toLower(name)}.${action}`);
 | 
						|
 | 
						|
        if (!controller) {
 | 
						|
          return new Error(
 | 
						|
            `Cannot find the controller's action ${name}.${action}`
 | 
						|
          );
 | 
						|
        }
 | 
						|
 | 
						|
        // We're going to return a controller instead.
 | 
						|
        isController = true;
 | 
						|
 | 
						|
        // Push global policy to make sure the permissions will work as expected.
 | 
						|
        policiesFn.push(
 | 
						|
          policyUtils.globalPolicy({
 | 
						|
            controller: name,
 | 
						|
            action,
 | 
						|
            plugin,
 | 
						|
          })
 | 
						|
        );
 | 
						|
 | 
						|
        // Return the controller.
 | 
						|
        return controller;
 | 
						|
      } else if (resolver) {
 | 
						|
        // Function.
 | 
						|
        return resolver;
 | 
						|
      }
 | 
						|
 | 
						|
      // We're going to return a controller instead.
 | 
						|
      isController = true;
 | 
						|
 | 
						|
      const controllers = plugin
 | 
						|
        ? strapi.plugins[plugin].controllers
 | 
						|
        : strapi.controllers;
 | 
						|
 | 
						|
      // Try to find the controller that should be related to this model.
 | 
						|
      const controller = isSingular
 | 
						|
        ? _.get(controllers, `${name}.findOne`)
 | 
						|
        : _.get(controllers, `${name}.find`);
 | 
						|
 | 
						|
      if (!controller) {
 | 
						|
        return new Error(
 | 
						|
          `Cannot find the controller's action ${name}.${
 | 
						|
            isSingular ? 'findOne' : 'find'
 | 
						|
          }`
 | 
						|
        );
 | 
						|
      }
 | 
						|
 | 
						|
      // Push global policy to make sure the permissions will work as expected.
 | 
						|
      // We're trying to detect the controller name.
 | 
						|
      policiesFn.push(
 | 
						|
        policyUtils.globalPolicy({
 | 
						|
          controller: name,
 | 
						|
          action: isSingular ? 'findOne' : 'find',
 | 
						|
          plugin,
 | 
						|
        })
 | 
						|
      );
 | 
						|
 | 
						|
      // Make the query compatible with our controller by
 | 
						|
      // setting in the context the parameters.
 | 
						|
      if (isSingular) {
 | 
						|
        return async (ctx, next) => {
 | 
						|
          ctx.params = {
 | 
						|
            ...params,
 | 
						|
            id: ctx.query.id,
 | 
						|
          };
 | 
						|
 | 
						|
          // Return the controller.
 | 
						|
          return controller(ctx, next);
 | 
						|
        };
 | 
						|
      }
 | 
						|
 | 
						|
      // Plural.
 | 
						|
      return controller;
 | 
						|
    })();
 | 
						|
 | 
						|
    // The controller hasn't been found.
 | 
						|
    if (_.isError(resolver)) {
 | 
						|
      return resolver;
 | 
						|
    }
 | 
						|
 | 
						|
    // Force policies of another action on a custom resolver.
 | 
						|
    if (_.isString(resolverOf) && !_.isEmpty(resolverOf)) {
 | 
						|
      // Retrieve the controller's action to be executed.
 | 
						|
      const [name, action] = resolverOf.split('.');
 | 
						|
 | 
						|
      const controller = plugin
 | 
						|
        ? _.get(
 | 
						|
            strapi.plugins,
 | 
						|
            `${plugin}.controllers.${_.toLower(name)}.${action}`
 | 
						|
          )
 | 
						|
        : _.get(strapi.controllers, `${_.toLower(name)}.${action}`);
 | 
						|
 | 
						|
      if (!controller) {
 | 
						|
        return new Error(
 | 
						|
          `Cannot find the controller's action ${name}.${action}`
 | 
						|
        );
 | 
						|
      }
 | 
						|
 | 
						|
      policiesFn[0] = policyUtils.globalPolicy({
 | 
						|
        controller: name,
 | 
						|
        action,
 | 
						|
        plugin,
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    if (strapi.plugins['users-permissions']) {
 | 
						|
      policies.unshift('plugins.users-permissions.permissions');
 | 
						|
    }
 | 
						|
 | 
						|
    // Populate policies.
 | 
						|
    policies.forEach(policy =>
 | 
						|
      policyUtils.get(
 | 
						|
        policy,
 | 
						|
        plugin,
 | 
						|
        policiesFn,
 | 
						|
        `GraphQL query "${queryName}"`,
 | 
						|
        name
 | 
						|
      )
 | 
						|
    );
 | 
						|
 | 
						|
    return async (obj, options = {}, graphqlContext) => {
 | 
						|
      const { context } = graphqlContext;
 | 
						|
      const _options = _.cloneDeep(options);
 | 
						|
 | 
						|
      // Hack to be able to handle permissions for each query.
 | 
						|
      const ctx = Object.assign(_.clone(context), {
 | 
						|
        request: Object.assign(_.clone(context.request), {
 | 
						|
          graphql: null,
 | 
						|
        }),
 | 
						|
        response: _.clone(context.response),
 | 
						|
      });
 | 
						|
 | 
						|
      // Note: we've to used the Object.defineProperties to reset the prototype. It seems that the cloning the context
 | 
						|
      // cause a lost of the Object prototype.
 | 
						|
      const opts = this.amountLimiting(_options);
 | 
						|
 | 
						|
      Object.defineProperty(ctx, 'query', {
 | 
						|
        enumerable: true,
 | 
						|
        configurable: true,
 | 
						|
        writable: true,
 | 
						|
        value: {
 | 
						|
          ...this.convertToParams(_.omit(opts, 'where')),
 | 
						|
          ...this.convertToQuery(opts.where),
 | 
						|
        },
 | 
						|
      });
 | 
						|
 | 
						|
      Object.defineProperty(ctx, 'params', {
 | 
						|
        enumerable: true,
 | 
						|
        configurable: true,
 | 
						|
        writable: true,
 | 
						|
        value: this.convertToParams(opts),
 | 
						|
      });
 | 
						|
 | 
						|
      // Execute policies stack.
 | 
						|
      const policy = await compose(policiesFn)(ctx);
 | 
						|
 | 
						|
      // Policy doesn't always return errors but they update the current context.
 | 
						|
      if (
 | 
						|
        _.isError(ctx.request.graphql) ||
 | 
						|
        _.get(ctx.request.graphql, 'isBoom')
 | 
						|
      ) {
 | 
						|
        return ctx.request.graphql;
 | 
						|
      }
 | 
						|
 | 
						|
      // Something went wrong in the policy.
 | 
						|
      if (policy) {
 | 
						|
        return policy;
 | 
						|
      }
 | 
						|
 | 
						|
      // Resolver can be a function. Be also a native resolver or a controller's action.
 | 
						|
      if (_.isFunction(resolver)) {
 | 
						|
        if (isController) {
 | 
						|
          const values = await resolver.call(null, ctx, null);
 | 
						|
 | 
						|
          if (ctx.body) {
 | 
						|
            return ctx.body;
 | 
						|
          }
 | 
						|
 | 
						|
          return values && values.toJSON ? values.toJSON() : values;
 | 
						|
        }
 | 
						|
 | 
						|
        return resolver.call(null, obj, opts, graphqlContext);
 | 
						|
      }
 | 
						|
 | 
						|
      // Resolver can be a promise.
 | 
						|
      return resolver;
 | 
						|
    };
 | 
						|
  },
 | 
						|
};
 |