2019-10-23 01:20:13 +02:00

317 lines
8.1 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(
undefined,
{
handler: `${name}.${action}`,
},
undefined,
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(
undefined,
{
handler: `${name}.${isSingular ? 'findOne' : 'find'}`,
},
undefined,
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(
undefined,
{
handler: `${name}.${action}`,
},
undefined,
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,
}),
});
// 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;
};
},
};