mirror of
https://github.com/strapi/strapi.git
synced 2025-11-14 01:02:04 +00:00
Merge branch 'master' into patch-content-manager
This commit is contained in:
commit
1d44f6c271
@ -22,7 +22,8 @@ By default, the [Shadow CRUD](#shadow-crud) feature is enabled and the GraphQL i
|
|||||||
```
|
```
|
||||||
{
|
{
|
||||||
"endpoint": "/graphql",
|
"endpoint": "/graphql",
|
||||||
"shadowCRUD": true
|
"shadowCRUD": true,
|
||||||
|
"depthLimit": 7
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -268,7 +269,26 @@ module.exports = {
|
|||||||
Query: {
|
Query: {
|
||||||
person: {
|
person: {
|
||||||
description: 'Return a single person',
|
description: 'Return a single person',
|
||||||
resolver: 'Person.findOne' // It will use the action `findOne` located in the `Person.js` controller.
|
resolver: 'Person.findOne' // It will use the action `findOne` located in the `Person.js` controller*.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
>The resolver parameter also accepts an object as a value to target a controller located in a plugin.
|
||||||
|
|
||||||
|
```js
|
||||||
|
module.exports = {
|
||||||
|
...
|
||||||
|
resolver: {
|
||||||
|
Query: {
|
||||||
|
person: {
|
||||||
|
description: 'Return a single person',
|
||||||
|
resolver: {
|
||||||
|
plugin: 'users-permissions',
|
||||||
|
handler: 'User.findOne' // It will use the action `findOne` located in the `Person.js` controller inside the plugin `Users & Permissions`.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,7 +39,8 @@ module.exports = function (strapi) {
|
|||||||
port: 27017,
|
port: 27017,
|
||||||
database: 'strapi',
|
database: 'strapi',
|
||||||
authenticationDatabase: '',
|
authenticationDatabase: '',
|
||||||
ssl: false
|
ssl: false,
|
||||||
|
debug: false
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -50,10 +51,11 @@ module.exports = function (strapi) {
|
|||||||
_.forEach(_.pickBy(strapi.config.connections, {connector: 'strapi-mongoose'}), (connection, connectionName) => {
|
_.forEach(_.pickBy(strapi.config.connections, {connector: 'strapi-mongoose'}), (connection, connectionName) => {
|
||||||
const instance = new Mongoose();
|
const instance = new Mongoose();
|
||||||
const { uri, host, port, username, password, database } = _.defaults(connection.settings, strapi.config.hook.settings.mongoose);
|
const { uri, host, port, username, password, database } = _.defaults(connection.settings, strapi.config.hook.settings.mongoose);
|
||||||
const { authenticationDatabase, ssl } = _.defaults(connection.options, strapi.config.hook.settings.mongoose);
|
const { authenticationDatabase, ssl, debug } = _.defaults(connection.options, strapi.config.hook.settings.mongoose);
|
||||||
|
|
||||||
// Connect to mongo database
|
// Connect to mongo database
|
||||||
const connectOptions = {};
|
const connectOptions = {};
|
||||||
|
const options = {};
|
||||||
|
|
||||||
if (!_.isEmpty(username)) {
|
if (!_.isEmpty(username)) {
|
||||||
connectOptions.user = username;
|
connectOptions.user = username;
|
||||||
@ -67,10 +69,16 @@ module.exports = function (strapi) {
|
|||||||
connectOptions.authSource = authenticationDatabase;
|
connectOptions.authSource = authenticationDatabase;
|
||||||
}
|
}
|
||||||
|
|
||||||
connectOptions.ssl = ssl === true || ssl === 'true';
|
connectOptions.ssl = Boolean(ssl);
|
||||||
|
|
||||||
|
options.debug = Boolean(debug);
|
||||||
|
|
||||||
instance.connect(uri || `mongodb://${host}:${port}/${database}`, connectOptions);
|
instance.connect(uri || `mongodb://${host}:${port}/${database}`, connectOptions);
|
||||||
|
|
||||||
|
for (let key in options) {
|
||||||
|
instance.set(key, options[key])
|
||||||
|
}
|
||||||
|
|
||||||
// Handle error
|
// Handle error
|
||||||
instance.connection.on('error', error => {
|
instance.connection.on('error', error => {
|
||||||
if (error.message.indexOf(`:${port}`)) {
|
if (error.message.indexOf(`:${port}`)) {
|
||||||
|
|||||||
@ -44,7 +44,7 @@ module.exports = {
|
|||||||
withRelated: populate || this.associations.map(x => x.alias)
|
withRelated: populate || this.associations.map(x => x.alias)
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = record ? record.toJSON() : record;
|
const data = record.toJSON ? record.toJSON() : record;
|
||||||
|
|
||||||
// Retrieve data manually.
|
// Retrieve data manually.
|
||||||
if (_.isEmpty(populate)) {
|
if (_.isEmpty(populate)) {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"endpoint": "/graphql",
|
"endpoint": "/graphql",
|
||||||
"shadowCRUD": true
|
"shadowCRUD": true,
|
||||||
|
"depthLimit": 7
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ const path = require('path');
|
|||||||
const glob = require('glob');
|
const glob = require('glob');
|
||||||
const { graphqlKoa } = require('apollo-server-koa');
|
const { graphqlKoa } = require('apollo-server-koa');
|
||||||
const koaPlayground = require('graphql-playground-middleware-koa').default;
|
const koaPlayground = require('graphql-playground-middleware-koa').default;
|
||||||
|
const depthLimit = require('graphql-depth-limit');
|
||||||
|
|
||||||
module.exports = strapi => {
|
module.exports = strapi => {
|
||||||
return {
|
return {
|
||||||
@ -109,8 +110,17 @@ module.exports = strapi => {
|
|||||||
|
|
||||||
const router = strapi.koaMiddlewares.routerJoi();
|
const router = strapi.koaMiddlewares.routerJoi();
|
||||||
|
|
||||||
router.post(strapi.plugins.graphql.config.endpoint, async (ctx, next) => graphqlKoa({ schema, context: ctx })(ctx, next));
|
router.post(strapi.plugins.graphql.config.endpoint, async (ctx, next) => graphqlKoa({
|
||||||
router.get(strapi.plugins.graphql.config.endpoint, async (ctx, next) => graphqlKoa({ schema, context: ctx })(ctx, next));
|
schema,
|
||||||
|
context: ctx,
|
||||||
|
validationRules: [ depthLimit(strapi.plugins.graphql.config.depthLimit) ]
|
||||||
|
})(ctx, next));
|
||||||
|
|
||||||
|
router.get(strapi.plugins.graphql.config.endpoint, async (ctx, next) => graphqlKoa({
|
||||||
|
schema,
|
||||||
|
context: ctx,
|
||||||
|
validationRules: [ depthLimit(strapi.plugins.graphql.config.depthLimit) ]
|
||||||
|
})(ctx, next));
|
||||||
|
|
||||||
// Disable GraphQL Playground in production environment.
|
// Disable GraphQL Playground in production environment.
|
||||||
if (strapi.config.environment !== 'production') {
|
if (strapi.config.environment !== 'production') {
|
||||||
|
|||||||
@ -24,6 +24,7 @@
|
|||||||
"apollo-server-koa": "^1.3.3",
|
"apollo-server-koa": "^1.3.3",
|
||||||
"glob": "^7.1.2",
|
"glob": "^7.1.2",
|
||||||
"graphql": "^0.13.2",
|
"graphql": "^0.13.2",
|
||||||
|
"graphql-depth-limit": "^1.1.0",
|
||||||
"graphql-playground-middleware-koa": "^1.5.1",
|
"graphql-playground-middleware-koa": "^1.5.1",
|
||||||
"graphql-tools": "^2.23.1",
|
"graphql-tools": "^2.23.1",
|
||||||
"graphql-type-json": "^0.2.0",
|
"graphql-type-json": "^0.2.0",
|
||||||
|
|||||||
@ -126,6 +126,22 @@ module.exports = {
|
|||||||
}, {});
|
}, {});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Security to avoid infinite limit.
|
||||||
|
*
|
||||||
|
* @return String
|
||||||
|
*/
|
||||||
|
|
||||||
|
amountLimiting: (params) => {
|
||||||
|
if (params.limit && params.limit < 0) {
|
||||||
|
params.limit = 0;
|
||||||
|
} else if (params.limit && params.limit > 100) {
|
||||||
|
params.limit = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert Strapi type to GraphQL type.
|
* Convert Strapi type to GraphQL type.
|
||||||
*
|
*
|
||||||
@ -135,19 +151,29 @@ module.exports = {
|
|||||||
convertType: (definition = {}) => {
|
convertType: (definition = {}) => {
|
||||||
// Type.
|
// Type.
|
||||||
if (definition.type) {
|
if (definition.type) {
|
||||||
|
let type = 'String';
|
||||||
|
|
||||||
switch (definition.type) {
|
switch (definition.type) {
|
||||||
case 'string':
|
case 'string':
|
||||||
case 'text':
|
case 'text':
|
||||||
return 'String';
|
type = 'String';
|
||||||
|
break;
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
return 'Boolean';
|
type = 'Boolean';
|
||||||
|
break;
|
||||||
case 'integer':
|
case 'integer':
|
||||||
return 'Int';
|
type = 'Int';
|
||||||
|
break;
|
||||||
case 'float':
|
case 'float':
|
||||||
return 'Float';
|
type = 'Float';
|
||||||
default:
|
break;
|
||||||
return 'String';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (definition.required) {
|
||||||
|
type += '!';
|
||||||
|
}
|
||||||
|
|
||||||
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ref = definition.model || definition.collection;
|
const ref = definition.model || definition.collection;
|
||||||
@ -193,19 +219,21 @@ module.exports = {
|
|||||||
// Extract custom resolver or type description.
|
// Extract custom resolver or type description.
|
||||||
const { resolver: handler = {} } = _schema;
|
const { resolver: handler = {} } = _schema;
|
||||||
|
|
||||||
const queryName = isSingular ?
|
let queryName;
|
||||||
|
|
||||||
|
if (isSingular === 'force') {
|
||||||
|
queryName = name;
|
||||||
|
} else {
|
||||||
|
queryName = isSingular ?
|
||||||
pluralize.singular(name):
|
pluralize.singular(name):
|
||||||
pluralize.plural(name);
|
pluralize.plural(name);
|
||||||
|
}
|
||||||
|
|
||||||
// Retrieve policies.
|
// Retrieve policies.
|
||||||
const policies = isSingular ?
|
const policies = _.get(handler, `Query.${queryName}.policies`, []);
|
||||||
_.get(handler, `Query.${pluralize.singular(name)}.policies`, []):
|
|
||||||
_.get(handler, `Query.${pluralize.plural(name)}.policies`, []);
|
|
||||||
|
|
||||||
// Retrieve resolverOf.
|
// Retrieve resolverOf.
|
||||||
const resolverOf = isSingular ?
|
const resolverOf = _.get(handler, `Query.${queryName}.resolverOf`, '');
|
||||||
_.get(handler, `Query.${pluralize.singular(name)}.resolverOf`, ''):
|
|
||||||
_.get(handler, `Query.${pluralize.plural(name)}.resolverOf`, '');
|
|
||||||
|
|
||||||
const policiesFn = [];
|
const policiesFn = [];
|
||||||
|
|
||||||
@ -216,13 +244,13 @@ module.exports = {
|
|||||||
// or the shadow CRUD resolver (aka Content-Manager).
|
// or the shadow CRUD resolver (aka Content-Manager).
|
||||||
const resolver = (() => {
|
const resolver = (() => {
|
||||||
// Try to retrieve custom resolver.
|
// Try to retrieve custom resolver.
|
||||||
const resolver = isSingular ?
|
const resolver = _.get(handler, `Query.${queryName}.resolver`);
|
||||||
_.get(handler, `Query.${pluralize.singular(name)}.resolver`):
|
|
||||||
_.get(handler, `Query.${pluralize.plural(name)}.resolver`);
|
if (_.isString(resolver) || _.isPlainObject(resolver)) {
|
||||||
|
const { handler = resolver } = _.isPlainObject(resolver) ? resolver : {};
|
||||||
|
|
||||||
if (_.isString(resolver)) {
|
|
||||||
// Retrieve the controller's action to be executed.
|
// Retrieve the controller's action to be executed.
|
||||||
const [ name, action ] = resolver.split('.');
|
const [ name, action ] = handler.split('.');
|
||||||
|
|
||||||
const controller = plugin ?
|
const controller = plugin ?
|
||||||
_.get(strapi.plugins, `${plugin}.controllers.${_.toLower(name)}.${action}`):
|
_.get(strapi.plugins, `${plugin}.controllers.${_.toLower(name)}.${action}`):
|
||||||
@ -287,6 +315,7 @@ module.exports = {
|
|||||||
|
|
||||||
// Plural.
|
// Plural.
|
||||||
return async (ctx, next) => {
|
return async (ctx, next) => {
|
||||||
|
ctx.params = this.amountLimiting(ctx.params);
|
||||||
ctx.query = Object.assign(
|
ctx.query = Object.assign(
|
||||||
this.convertToParams(_.omit(ctx.params, 'where')),
|
this.convertToParams(_.omit(ctx.params, 'where')),
|
||||||
ctx.params.where
|
ctx.params.where
|
||||||
@ -328,7 +357,7 @@ module.exports = {
|
|||||||
|
|
||||||
return async (obj, options, context) => {
|
return async (obj, options, context) => {
|
||||||
// Hack to be able to handle permissions for each query.
|
// Hack to be able to handle permissions for each query.
|
||||||
const ctx = Object.assign(context, {
|
const ctx = Object.assign(_.clone(context), {
|
||||||
request: Object.assign(_.clone(context.request), {
|
request: Object.assign(_.clone(context.request), {
|
||||||
graphql: null
|
graphql: null
|
||||||
})
|
})
|
||||||
@ -350,7 +379,7 @@ module.exports = {
|
|||||||
// Resolver can be a function. Be also a native resolver or a controller's action.
|
// Resolver can be a function. Be also a native resolver or a controller's action.
|
||||||
if (_.isFunction(resolver)) {
|
if (_.isFunction(resolver)) {
|
||||||
context.query = this.convertToParams(options);
|
context.query = this.convertToParams(options);
|
||||||
context.params = options;
|
context.params = this.amountLimiting(options);
|
||||||
|
|
||||||
if (isController) {
|
if (isController) {
|
||||||
const values = await resolver.call(null, context);
|
const values = await resolver.call(null, context);
|
||||||
@ -362,6 +391,7 @@ module.exports = {
|
|||||||
return values && values.toJSON ? values.toJSON() : values;
|
return values && values.toJSON ? values.toJSON() : values;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return resolver.call(null, obj, options, context);
|
return resolver.call(null, obj, options, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -394,7 +424,7 @@ module.exports = {
|
|||||||
// 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.
|
||||||
const initialState = {
|
const initialState = {
|
||||||
[model.primaryKey]: 'String'
|
[model.primaryKey]: 'String!'
|
||||||
};
|
};
|
||||||
|
|
||||||
const globalId = model.globalId;
|
const globalId = model.globalId;
|
||||||
@ -407,8 +437,8 @@ module.exports = {
|
|||||||
// Add timestamps attributes.
|
// Add timestamps attributes.
|
||||||
if (_.get(model, 'options.timestamps') === true) {
|
if (_.get(model, 'options.timestamps') === true) {
|
||||||
Object.assign(initialState, {
|
Object.assign(initialState, {
|
||||||
createdAt: 'String',
|
createdAt: 'String!',
|
||||||
updatedAt: 'String'
|
updatedAt: 'String!'
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.assign(acc.resolver[globalId], {
|
Object.assign(acc.resolver[globalId], {
|
||||||
@ -426,6 +456,7 @@ module.exports = {
|
|||||||
|
|
||||||
// Convert our layer Model to the GraphQL DL.
|
// Convert our layer Model to the GraphQL DL.
|
||||||
const attributes = Object.keys(model.attributes)
|
const attributes = Object.keys(model.attributes)
|
||||||
|
.filter(attribute => model.attributes[attribute].private !== true)
|
||||||
.reduce((acc, attribute) => {
|
.reduce((acc, attribute) => {
|
||||||
// Convert our type to the GraphQL type.
|
// Convert our type to the GraphQL type.
|
||||||
acc[attribute] = this.convertType(model.attributes[attribute]);
|
acc[attribute] = this.convertType(model.attributes[attribute]);
|
||||||
@ -495,6 +526,21 @@ module.exports = {
|
|||||||
// Build associations queries.
|
// Build associations queries.
|
||||||
(model.associations || []).forEach(association => {
|
(model.associations || []).forEach(association => {
|
||||||
switch (association.nature) {
|
switch (association.nature) {
|
||||||
|
case 'oneToManyMorph':
|
||||||
|
return _.merge(acc.resolver[globalId], {
|
||||||
|
[association.alias]: async (obj) => {
|
||||||
|
const withRelated = await resolvers.fetch({
|
||||||
|
id: obj[model.primaryKey],
|
||||||
|
model: name
|
||||||
|
}, plugin, [association.alias], false);
|
||||||
|
|
||||||
|
const entry = withRelated && withRelated.toJSON ? withRelated.toJSON() : withRelated;
|
||||||
|
|
||||||
|
entry[association.alias]._type = _.upperFirst(association.model);
|
||||||
|
|
||||||
|
return entry[association.alias];
|
||||||
|
}
|
||||||
|
});
|
||||||
case 'manyMorphToOne':
|
case 'manyMorphToOne':
|
||||||
case 'manyMorphToMany':
|
case 'manyMorphToMany':
|
||||||
case 'manyToManyMorph':
|
case 'manyToManyMorph':
|
||||||
@ -513,6 +559,8 @@ module.exports = {
|
|||||||
|
|
||||||
const entry = withRelated && withRelated.toJSON ? withRelated.toJSON() : withRelated;
|
const entry = withRelated && withRelated.toJSON ? withRelated.toJSON() : withRelated;
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// - Handle sort, limit and start (lodash or inside the query)
|
||||||
entry[association.alias].map((entry, index) => {
|
entry[association.alias].map((entry, index) => {
|
||||||
const type = _.get(withoutRelated, `${association.alias}.${index}.kind`) ||
|
const type = _.get(withoutRelated, `${association.alias}.${index}.kind`) ||
|
||||||
_.upperFirst(_.camelCase(_.get(withoutRelated, `${association.alias}.${index}.${association.alias}_type`))) ||
|
_.upperFirst(_.camelCase(_.get(withoutRelated, `${association.alias}.${index}.${association.alias}_type`))) ||
|
||||||
@ -549,7 +597,7 @@ module.exports = {
|
|||||||
strapi.models[params.model];
|
strapi.models[params.model];
|
||||||
|
|
||||||
// Apply optional arguments to make more precise nested request.
|
// Apply optional arguments to make more precise nested request.
|
||||||
const convertedParams = strapi.utils.models.convertParams(name, this.convertToParams(options));
|
const convertedParams = strapi.utils.models.convertParams(name, this.convertToParams(this.amountLimiting(options)));
|
||||||
const where = strapi.utils.models.convertParams(name, options.where || {});
|
const where = strapi.utils.models.convertParams(name, options.where || {});
|
||||||
|
|
||||||
// Limit, order, etc.
|
// Limit, order, etc.
|
||||||
@ -560,7 +608,7 @@ module.exports = {
|
|||||||
|
|
||||||
switch (association.nature) {
|
switch (association.nature) {
|
||||||
case 'manyToMany': {
|
case 'manyToMany': {
|
||||||
const arrayOfIds = obj[association.alias].map(related => {
|
const arrayOfIds = (obj[association.alias] || []).map(related => {
|
||||||
return related[ref.primaryKey] || related;
|
return related[ref.primaryKey] || related;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -642,9 +690,20 @@ module.exports = {
|
|||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
acc[type][resolver] = _.isFunction(acc[type][resolver]) ?
|
if (!_.isFunction(acc[type][resolver])) {
|
||||||
acc[type][resolver]:
|
acc[type][resolver] = acc[type][resolver].resolver;
|
||||||
acc[type][resolver].resolver;
|
}
|
||||||
|
|
||||||
|
if (_.isString(acc[type][resolver]) || _.isPlainObject(acc[type][resolver])) {
|
||||||
|
const { plugin = '' } = _.isPlainObject(acc[type][resolver]) ? acc[type][resolver] : {};
|
||||||
|
|
||||||
|
acc[type][resolver] = this.composeResolver(
|
||||||
|
strapi.plugins.graphql.config._schema.graphql,
|
||||||
|
plugin,
|
||||||
|
resolver,
|
||||||
|
'force' // Avoid singular/pluralize and force query name.
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, acc);
|
}, acc);
|
||||||
|
|||||||
@ -39,9 +39,11 @@ module.exports = async (ctx, next) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!permission) {
|
if (!permission) {
|
||||||
ctx.forbidden();
|
if (ctx.request.graphql === null) {
|
||||||
|
return ctx.request.graphql = strapi.errors.forbidden();
|
||||||
|
}
|
||||||
|
|
||||||
return ctx.request.graphql = ctx.body;
|
ctx.forbidden();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute the policies.
|
// Execute the policies.
|
||||||
|
|||||||
@ -19,6 +19,7 @@ module.exports = strapi => {
|
|||||||
this.delegator = delegate(strapi.app.context, 'response');
|
this.delegator = delegate(strapi.app.context, 'response');
|
||||||
this.createResponses();
|
this.createResponses();
|
||||||
|
|
||||||
|
strapi.errors = Boom;
|
||||||
strapi.app.use(async (ctx, next) => {
|
strapi.app.use(async (ctx, next) => {
|
||||||
try {
|
try {
|
||||||
// App logic.
|
// App logic.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user