Add description and execute policies before the resolver

This commit is contained in:
Aurelsicoko 2018-03-30 17:05:24 +02:00
parent 6fe3e1bb85
commit dd61fc8262
8 changed files with 272 additions and 98 deletions

View File

@ -1,3 +1,4 @@
{ {
"endpoint": "/graphql" "endpoint": "/graphql",
"shadowCRUD": true
} }

View File

@ -6,14 +6,59 @@
// Public node modules. // Public node modules.
const _ = require('lodash'); const _ = require('lodash');
const path = require('path');
const glob = require('glob');
const { graphqlKoa, graphiqlKoa } = require('apollo-server-koa'); const { graphqlKoa, graphiqlKoa } = require('apollo-server-koa');
module.exports = strapi => { module.exports = strapi => {
return { return {
beforeInitialize: function() { beforeInitialize: async function() {
// Try to inject this hook just after the others hooks to skip the router processing. // Try to inject this hook just after the others hooks to skip the router processing.
strapi.config.hook.load.order = strapi.config.hook.load.order.concat(Object.keys(strapi.hook).filter(hook => hook !== 'graphql')); strapi.config.hook.load.order = strapi.config.hook.load.order.concat(Object.keys(strapi.hook).filter(hook => hook !== 'graphql'));
strapi.config.hook.load.order.push('graphql'); strapi.config.hook.load.order.push('graphql');
// Load core utils.
const utils = require(path.resolve(strapi.config.appPath, 'node_modules', 'strapi', 'lib', 'utils'));
// Set '*.graphql' files configurations in the global variable.
await Promise.all([
// Load root configurations.
new Promise((resolve, reject) => {
glob('./config/**/*.*(graphql)', {
cwd: strapi.config.appPath
}, (err, files) => {
if (err) {
return reject(err);
}
utils.loadConfig.call(strapi, files, true).then(resolve).catch(reject);
});
}),
// Load APIs configurations.
new Promise((resolve, reject) => {
glob('./api/*/config/**/*.*(graphql)', {
cwd: strapi.config.appPath
}, (err, files) => {
if (err) {
return reject(err);
}
utils.loadConfig.call(strapi, files, true).then(resolve).catch(reject);
});
}),
// Load plugins configurations.
new Promise((resolve, reject) => {
glob('./plugins/*/config/!(generated)/*.*(graphql)', {
cwd: strapi.config.appPath
}, (err, files) => {
if (err) {
return reject(err);
}
utils.loadConfig.call(strapi, files, true).then(resolve).catch(reject);
});
})
]);
}, },
initialize: function(cb) { initialize: function(cb) {
@ -27,8 +72,8 @@ module.exports = strapi => {
const router = strapi.koaMiddlewares.routerJoi(); const router = strapi.koaMiddlewares.routerJoi();
router.post(strapi.plugins.graphql.config.endpoint, graphqlKoa({ schema })); router.post(strapi.plugins.graphql.config.endpoint, async (ctx, next) => graphqlKoa({ schema, context: ctx })(ctx, next));
router.get(strapi.plugins.graphql.config.endpoint, graphqlKoa({ schema })); router.get(strapi.plugins.graphql.config.endpoint, async (ctx, next) => graphqlKoa({ schema, context: ctx })(ctx, next));
router.get('/graphiql', graphiqlKoa({ endpointURL: strapi.plugins.graphql.config.endpoint })); router.get('/graphiql', graphiqlKoa({ endpointURL: strapi.plugins.graphql.config.endpoint }));

View File

@ -26,7 +26,8 @@
"apollo-server-koa": "^1.3.3", "apollo-server-koa": "^1.3.3",
"graphql": "^0.13.2", "graphql": "^0.13.2",
"graphql-tools": "^2.23.1", "graphql-tools": "^2.23.1",
"pluralize": "^7.0.0" "pluralize": "^7.0.0",
"strapi-utils": "3.0.0-alpha.11.1"
}, },
"devDependencies": { "devDependencies": {
"strapi-helper-plugin": "3.0.0-alpha.11.1" "strapi-helper-plugin": "3.0.0-alpha.11.1"

View File

@ -6,16 +6,73 @@
* @description: A set of functions similar to controller's actions to avoid code duplication. * @description: A set of functions similar to controller's actions to avoid code duplication.
*/ */
const policyUtils = require('strapi-utils').policy;
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const _ = require('lodash'); const _ = require('lodash');
const pluralize = require('pluralize'); const pluralize = require('pluralize');
const { makeExecutableSchema } = require('graphql-tools'); const { makeExecutableSchema } = require('graphql-tools');
module.exports = { module.exports = {
formatGQL: (str) => JSON.stringify(str, null, 2).replace(/['"]+/g, ''), /**
* Receive an Object and return a string which is following the GraphQL specs.
*
* @return String
*/
formatGQL: function (fields, description, type = 'field') {
const typeFields = JSON.stringify(fields, null, 2).replace(/['",]+/g, '');
const lines = typeFields.split('\n');
// Try to add description for field.
if (type === 'field') {
return lines
.map((line, index) => {
if ([0, lines.length - 1].includes(index)) {
return line;
}
const split = line.split(':');
const attribute = _.trim(split[0]);
const info = description[attribute];
if (info) {
return ` """\n ${info}\n """\n${line}`;
}
return line;
})
.join('\n');
}
return typeFields;
},
/**
* Retrieve description from variable and return a string which follow the GraphQL specs.
*
* @return String
*/
getDescription: (description) => {
const format = `"""\n`;
const str = _.get(description, `_description`) || description;
if (str) {
return `${format}${str}\n${format}`;
}
return ``;
},
/**
* Convert Strapi type to GraphQL type.
*
* @return String
*/
convertType: (type) => { convertType: (type) => {
switch (type) { switch (type) {
@ -24,11 +81,44 @@ module.exports = {
return 'String'; return 'String';
case 'boolean': case 'boolean':
return 'Boolean'; return 'Boolean';
case 'integer':
return 'Int';
default: default:
return 'String'; return 'String';
} }
}, },
/**
* Execute policies before the specified resolver.
*
* @return Promise or Error.
*/
composeResolver: async (context, plugin, policies = [], resolver) => {
const policiesFn = [];
// Populate policies.
policies.forEach(policy => policyUtils.get(policy, plugin, policiesFn, 'GraphQL error'));
// Execute policies stack.
const policy = await strapi.koaMiddlewares.compose(policiesFn)(context);
// Policy doesn't always return errors but they update the current context.
if (_.isError(context.response.body) || _.get(context.response.body, 'isBoom')) {
return context.response.body;
}
// When everything is okay, the policy variable should be undefined
// so it will return the resolver instead.
return policy || resolver;
},
/**
* Construct the GraphQL query & definition and apply the right resolvers.
*
* @return Object
*/
shadowCRUD: function (models) { shadowCRUD: function (models) {
const initialState = { definition: ``, query: {}, resolver: {} }; const initialState = { definition: ``, query: {}, resolver: {} };
// Retrieve generic service from the Content Manager plugin. // Retrieve generic service from the Content Manager plugin.
@ -39,11 +129,12 @@ module.exports = {
} }
return models.reduce((acc, model) => { return models.reduce((acc, model) => {
const plugin = undefined;
const params = { const params = {
model model
}; };
const query = {}; const queryOpts = {};
// Setup initial state with default attribute that should be displayed // Setup initial state with default attribute that should be displayed
// but these attributes are not properly defined in the models. // but these attributes are not properly defined in the models.
@ -59,6 +150,11 @@ module.exports = {
}); });
} }
const globalId = strapi.models[model].globalId;
// Retrieve user customisation.
const { resolver = {}, query, definition, _type = {} } = _.get(strapi.api, `${model}.config.schema.graphql`, {});
// Convert our layer Model to the GraphQL DL. // Convert our layer Model to the GraphQL DL.
const attributes = Object.keys(strapi.models[model].attributes) const attributes = Object.keys(strapi.models[model].attributes)
.reduce((acc, attribute) => { .reduce((acc, attribute) => {
@ -68,7 +164,7 @@ module.exports = {
return acc; return acc;
}, initialState); }, initialState);
acc.definition += `type ${strapi.models[model].globalId} ${this.formatGQL(attributes)}\n\n`; acc.definition += `${this.getDescription(_type[globalId])}type ${globalId} ${this.formatGQL(attributes, _type[globalId])}\n\n`;
Object.assign(acc.query, { Object.assign(acc.query, {
[`${pluralize.plural(model)}`]: `[${strapi.models[model].globalId}]`, [`${pluralize.plural(model)}`]: `[${strapi.models[model].globalId}]`,
@ -76,23 +172,38 @@ module.exports = {
}); });
// TODO // TODO
// - Apply and execute policies first.
// - Handle mutations. // - Handle mutations.
Object.assign(acc.resolver, { Object.assign(acc.resolver, {
[`${pluralize.plural(model)}`]: (_, options) => resolvers.fetchAll(params, {...query, ...options}), [pluralize.plural(model)]: (obj, options, context) => this.composeResolver(
[`${pluralize.singular(model)}`]: (_, { id }) => resolvers.fetch({ ...params, id }, query) context,
plugin,
_.get(resolver, `Query.${pluralize.plural(model)}.policy`),
resolvers.fetchAll(params, {...queryOpts, ...options})
),
[pluralize.singular(model)]: (obj, { id }, context) => this.composeResolver(
context,
plugin,
_.get(resolver, `Query.${pluralize.singular(model)}.policy`),
resolvers.fetch({ ...params, id }, queryOpts)
)
}); });
return acc; return acc;
}, initialState); }, initialState);
}, },
/**
* Generate GraphQL schema.
*
* @return Schema
*/
generateSchema: function () { generateSchema: function () {
// Exclude core models. // Exclude core models.
const models = Object.keys(strapi.models).filter(model => model !== 'core_store'); const models = Object.keys(strapi.models).filter(model => model !== 'core_store');
// Generate type definition and query/mutation for models. // Generate type definition and query/mutation for models.
const shadowCRUD = true ? this.shadowCRUD(models) : {}; const shadowCRUD = strapi.plugins.graphql.config.shadowCRUD !== false ? this.shadowCRUD(models) : {};
// Build resolvers. // Build resolvers.
const resolvers = { const resolvers = {
@ -105,7 +216,9 @@ module.exports = {
} }
// Concatenate. // Concatenate.
const typeDefs = shadowCRUD.definition + `type Query ${this.formatGQL(shadowCRUD.query)}`; const typeDefs = shadowCRUD.definition + `type Query ${this.formatGQL(shadowCRUD.query, {}, 'query')}`;
console.log(typeDefs);
// Write schema. // Write schema.
this.writeGenerateSchema(typeDefs); this.writeGenerateSchema(typeDefs);
@ -119,6 +232,12 @@ module.exports = {
return schema; return schema;
}, },
/**
* Save into a file the readable GraphQL schema.
*
* @return void
*/
writeGenerateSchema(schema) { writeGenerateSchema(schema) {
// Disable auto-reload. // Disable auto-reload.
strapi.reload.isWatching = false; strapi.reload.isWatching = false;

View File

@ -13,5 +13,6 @@ module.exports = {
knex: require('./knex'), knex: require('./knex'),
logger: require('./logger'), logger: require('./logger'),
models: require('./models'), models: require('./models'),
policy: require('./policy'),
regex: require('./regex') regex: require('./regex')
}; };

View File

@ -0,0 +1,84 @@
// Public dependencies.
const _ = require('lodash');
module.exports = {
get: (policy, plugin, policies = [], endpoint) => {
// Define global policy prefix.
const globalPolicyPrefix = 'global.';
const pluginPolicyPrefix = 'plugins.';
const policySplited = policy.split('.');
// Looking for global policy or namespaced.
if (
_.startsWith(policy, globalPolicyPrefix, 0) &&
!_.isEmpty(
strapi.config.policies,
policy.replace(globalPolicyPrefix, '')
)
) {
// Global policy.
return policies.push(
strapi.config.policies[
policy.replace(globalPolicyPrefix, '').toLowerCase()
]
);
} else if (
_.startsWith(policy, pluginPolicyPrefix, 0) &&
strapi.plugins[policySplited[1]] &&
!_.isUndefined(
_.get(
strapi.plugins,
policySplited[1] +
'.config.policies.' +
policySplited[2].toLowerCase()
)
)
) {
// Plugin's policies can be used from app APIs with a specific syntax (`plugins.pluginName.policyName`).
return policies.push(
_.get(
strapi.plugins,
policySplited[1] +
'.config.policies.' +
policySplited[2].toLowerCase()
)
);
} else if (
!_.startsWith(policy, globalPolicyPrefix, 0) &&
plugin &&
!_.isUndefined(
_.get(
strapi.plugins,
plugin + '.config.policies.' + policy.toLowerCase()
)
)
) {
// Plugin policy used in the plugin itself.
return policies.push(
_.get(
strapi.plugins,
plugin + '.config.policies.' + policy.toLowerCase()
)
);
} else if (
!_.startsWith(policy, globalPolicyPrefix, 0) &&
!_.isUndefined(
_.get(
strapi.api,
currentApiName + '.config.policies.' + policy.toLowerCase()
)
)
) {
// API policy used in the API itself.
return policies.push(
_.get(
strapi.api,
currentApiName + '.config.policies.' + policy.toLowerCase()
)
);
}
strapi.log.error(`Ignored attempt to bind route ${endpoint} with unknown policy ${policy}`);
}
};

View File

@ -11,7 +11,7 @@ const _ = require('lodash');
const finder = require('strapi-utils').finder; const finder = require('strapi-utils').finder;
const regex = require('strapi-utils').regex; const regex = require('strapi-utils').regex;
const joijson = require('strapi-utils').joijson; const joijson = require('strapi-utils').joijson;
const policyUtils = require('strapi-utils').policy;
// Middleware used for every routes. // Middleware used for every routes.
// Expose the endpoint in `this`. // Expose the endpoint in `this`.
@ -67,88 +67,7 @@ module.exports = strapi => function routerChecker(value, endpoint, plugin) {
!_.isEmpty(_.get(value, 'config.policies')) !_.isEmpty(_.get(value, 'config.policies'))
) { ) {
_.forEach(value.config.policies, policy => { _.forEach(value.config.policies, policy => {
// Define global policy prefix. policyUtils.get(policy, plugin, policies, endpoint);
const globalPolicyPrefix = 'global.';
const pluginPolicyPrefix = 'plugins.';
const policySplited = policy.split('.');
// Looking for global policy or namespaced.
if (
_.startsWith(policy, globalPolicyPrefix, 0) &&
!_.isEmpty(
strapi.config.policies,
policy.replace(globalPolicyPrefix, '')
)
) {
// Global policy.
return policies.push(
strapi.config.policies[
policy.replace(globalPolicyPrefix, '').toLowerCase()
]
);
} else if (
_.startsWith(policy, pluginPolicyPrefix, 0) &&
strapi.plugins[policySplited[1]] &&
!_.isUndefined(
_.get(
strapi.plugins,
policySplited[1] +
'.config.policies.' +
policySplited[2].toLowerCase()
)
)
) {
// Plugin's policies can be used from app APIs with a specific syntax (`plugins.pluginName.policyName`).
return policies.push(
_.get(
strapi.plugins,
policySplited[1] +
'.config.policies.' +
policySplited[2].toLowerCase()
)
);
} else if (
!_.startsWith(policy, globalPolicyPrefix, 0) &&
plugin &&
!_.isUndefined(
_.get(
strapi.plugins,
plugin + '.config.policies.' + policy.toLowerCase()
)
)
) {
// Plugin policy used in the plugin itself.
return policies.push(
_.get(
strapi.plugins,
plugin + '.config.policies.' + policy.toLowerCase()
)
);
} else if (
!_.startsWith(policy, globalPolicyPrefix, 0) &&
!_.isUndefined(
_.get(
strapi.api,
currentApiName + '.config.policies.' + policy.toLowerCase()
)
)
) {
// API policy used in the API itself.
return policies.push(
_.get(
strapi.api,
currentApiName + '.config.policies.' + policy.toLowerCase()
)
);
}
strapi.log.error(
'Ignored attempt to bind route `' +
endpoint +
'` with unknown policy `' +
policy +
'`.'
);
}); });
} }

View File

@ -71,8 +71,12 @@ module.exports = {
.toLowerCase(); .toLowerCase();
}, },
loadConfig: function(files) { loadConfig: function(files, shouldBeAggregated = false) {
const aggregate = files.filter(p => { const aggregate = files.filter(p => {
if (shouldBeAggregated) {
return true;
}
if (intersection(p.split('/').map(p => p.replace('.json', '')), ['environments', 'database', 'security', 'request', 'response', 'server']).length === 2) { if (intersection(p.split('/').map(p => p.replace('.json', '')), ['environments', 'database', 'security', 'request', 'response', 'server']).length === 2) {
return true; return true;
} }