mirror of
				https://github.com/strapi/strapi.git
				synced 2025-10-31 01:47:13 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			286 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			286 lines
		
	
	
		
			7.8 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) {
 | |
|           throw 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) {
 | |
|         throw 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) {
 | |
|         throw 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(policyName => {
 | |
|       try {
 | |
|         policiesFn.push(policyUtils.get(policyName, plugin, name));
 | |
|       } catch (error) {
 | |
|         strapi.stopWithError(`Error building graphql query "${queryName}": ${error.message}`);
 | |
|       }
 | |
|     });
 | |
| 
 | |
|     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;
 | |
|     };
 | |
|   },
 | |
| };
 | 
