mirror of
https://github.com/strapi/strapi.git
synced 2025-12-27 23:24:03 +00:00
GraphQL Mutations, Aggregations, Decimals
Merging Mutations and Aggregations. Fixed Mongoose Hook to use Decimals (2 decimal places) and Floats (20 decimal places).
This commit is contained in:
parent
b4dedc12f1
commit
3378403954
@ -5,7 +5,8 @@
|
||||
*/
|
||||
|
||||
module.exports = mongoose => {
|
||||
require('mongoose-float').loadType(mongoose);
|
||||
var Decimal = require('mongoose-float').loadType(mongoose, 2);
|
||||
var Float = require('mongoose-float').loadType(mongoose, 20);
|
||||
|
||||
return {
|
||||
convertType: mongooseType => {
|
||||
@ -22,9 +23,9 @@ module.exports = mongoose => {
|
||||
case 'timestamp':
|
||||
return Date;
|
||||
case 'decimal':
|
||||
return 'Float';
|
||||
return Decimal;
|
||||
case 'float':
|
||||
return mongoose.Schema.Types.Decimal128;
|
||||
return Float;
|
||||
case 'json':
|
||||
return 'Mixed';
|
||||
case 'biginteger':
|
||||
@ -39,8 +40,7 @@ module.exports = mongoose => {
|
||||
case 'text':
|
||||
return 'String';
|
||||
default:
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@ -1 +1 @@
|
||||
# Strapi plugin
|
||||
# Strapi plugin
|
||||
|
||||
@ -8,13 +8,12 @@
|
||||
const _ = require('lodash');
|
||||
const path = require('path');
|
||||
const glob = require('glob');
|
||||
const { graphqlKoa } = require('apollo-server-koa');
|
||||
const koaPlayground = require('graphql-playground-middleware-koa').default;
|
||||
const { ApolloServer } = require('apollo-server-koa');
|
||||
const depthLimit = require('graphql-depth-limit');
|
||||
|
||||
module.exports = strapi => {
|
||||
return {
|
||||
beforeInitialize: async function(){
|
||||
beforeInitialize: async function() {
|
||||
// Try to inject this hook just after the others hooks to skip the router processing.
|
||||
if (!_.get(strapi.config.hook.load, 'after')) {
|
||||
_.set(strapi.config.hook.load, 'after', []);
|
||||
@ -23,46 +22,73 @@ module.exports = strapi => {
|
||||
strapi.config.hook.load.after.push('graphql');
|
||||
|
||||
// Load core utils.
|
||||
const utils = require(path.resolve(strapi.config.appPath, 'node_modules', 'strapi', 'lib', '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);
|
||||
}
|
||||
glob(
|
||||
'./config/*.graphql',
|
||||
{
|
||||
cwd: strapi.config.appPath,
|
||||
},
|
||||
(err, files) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
utils.loadConfig.call(strapi, files, true).then(resolve).catch(reject);
|
||||
});
|
||||
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);
|
||||
}
|
||||
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);
|
||||
});
|
||||
utils.loadConfig
|
||||
.call(strapi, files, true)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
},
|
||||
);
|
||||
}),
|
||||
// Load plugins configurations.
|
||||
new Promise((resolve, reject) => {
|
||||
glob('./plugins/*/config/*.graphql', {
|
||||
cwd: strapi.config.appPath
|
||||
}, (err, files) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
glob(
|
||||
'./plugins/*/config/*.graphql',
|
||||
{
|
||||
cwd: strapi.config.appPath,
|
||||
},
|
||||
(err, files) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
utils.loadConfig.call(strapi, files, true).then(resolve).catch(reject);
|
||||
});
|
||||
})
|
||||
utils.loadConfig
|
||||
.call(strapi, files, true)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
},
|
||||
);
|
||||
}),
|
||||
]);
|
||||
|
||||
/*
|
||||
@ -70,66 +96,93 @@ module.exports = strapi => {
|
||||
*/
|
||||
|
||||
// Set path with initial state.
|
||||
_.set(strapi.plugins.graphql, 'config._schema.graphql', { definition: '', query: '', type : {}, resolver: {} });
|
||||
_.set(strapi.plugins.graphql, 'config._schema.graphql', {
|
||||
definition: '',
|
||||
query: '',
|
||||
mutation: '',
|
||||
type: {},
|
||||
resolver: {},
|
||||
});
|
||||
|
||||
// Merge user API.
|
||||
Object.keys(strapi.api || {}).reduce((acc, current) => {
|
||||
const { definition, query, type, resolver } = _.get(strapi.api[current], 'config.schema.graphql', {});
|
||||
const { definition, query, mutation, type, resolver } = _.get(
|
||||
strapi.api[current],
|
||||
'config.schema.graphql',
|
||||
{},
|
||||
);
|
||||
|
||||
acc.definition += definition || '';
|
||||
acc.query += query || '';
|
||||
acc.mutation += mutation || '';
|
||||
|
||||
return _.merge(acc, {
|
||||
type,
|
||||
resolver
|
||||
resolver,
|
||||
});
|
||||
}, strapi.plugins.graphql.config._schema.graphql);
|
||||
|
||||
// Merge plugins API.
|
||||
Object.keys(strapi.plugins || {}).reduce((acc, current) => {
|
||||
const { definition, query, type, resolver } = _.get(strapi.plugins[current], 'config.schema.graphql', {});
|
||||
const { definition, query, mutation, type, resolver } = _.get(
|
||||
strapi.plugins[current],
|
||||
'config.schema.graphql',
|
||||
{},
|
||||
);
|
||||
|
||||
acc.definition += definition || '';
|
||||
acc.query += query || '';
|
||||
acc.mutation += mutation || '';
|
||||
|
||||
return _.merge(acc, {
|
||||
type,
|
||||
resolver
|
||||
resolver,
|
||||
});
|
||||
}, strapi.plugins.graphql.config._schema.graphql);
|
||||
},
|
||||
|
||||
initialize: function(cb) {
|
||||
const schema = strapi.plugins.graphql.services.graphql.generateSchema();
|
||||
const {
|
||||
typeDefs,
|
||||
resolvers,
|
||||
} = strapi.plugins.graphql.services.schema.generateSchema();
|
||||
|
||||
if (_.isEmpty(schema)) {
|
||||
strapi.log.warn('GraphQL schema has not been generated because it\'s empty');
|
||||
if (_.isEmpty(typeDefs)) {
|
||||
strapi.log.warn(
|
||||
'GraphQL schema has not been generated because it\'s empty',
|
||||
);
|
||||
|
||||
return cb();
|
||||
}
|
||||
|
||||
const router = strapi.koaMiddlewares.routerJoi();
|
||||
|
||||
router.post(strapi.plugins.graphql.config.endpoint, async (ctx, next) => graphqlKoa({
|
||||
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));
|
||||
const serverParams = {
|
||||
typeDefs,
|
||||
resolvers,
|
||||
context: async ({ ctx }) => ({
|
||||
context: ctx,
|
||||
}),
|
||||
validationRules: [depthLimit(strapi.plugins.graphql.config.depthLimit)],
|
||||
playground: false,
|
||||
};
|
||||
|
||||
// Disable GraphQL Playground in production environment.
|
||||
if (strapi.config.environment !== 'production' || strapi.plugins.graphql.config.playgroundAlways) {
|
||||
router.get('/playground', koaPlayground({ endpoint: strapi.plugins.graphql.config.endpoint}));
|
||||
if (
|
||||
strapi.config.environment !== 'production' ||
|
||||
strapi.plugins.graphql.config.playgroundAlways
|
||||
) {
|
||||
serverParams.playground = {
|
||||
endpoint: strapi.plugins.graphql.config.endpoint,
|
||||
};
|
||||
}
|
||||
|
||||
strapi.app.use(router.middleware());
|
||||
const server = new ApolloServer(serverParams);
|
||||
|
||||
server.applyMiddleware({
|
||||
app: strapi.app,
|
||||
path: strapi.plugins.graphql.config.endpoint,
|
||||
});
|
||||
|
||||
cb();
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
563
packages/strapi-plugin-graphql/services/Aggregator.js
Normal file
563
packages/strapi-plugin-graphql/services/Aggregator.js
Normal file
@ -0,0 +1,563 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Aggregator.js service
|
||||
*
|
||||
* @description: A set of functions similar to controller's actions to avoid code duplication.
|
||||
*/
|
||||
|
||||
const _ = require('lodash');
|
||||
const pluralize = require('pluralize');
|
||||
const Schema = require('./Schema.js');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Returns all fields of type primitive
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
isPrimitiveType: _type => {
|
||||
const type = _type.replace('!', '');
|
||||
return (
|
||||
type === 'Int' ||
|
||||
type === 'Float' ||
|
||||
type === 'String' ||
|
||||
type === 'Boolean' ||
|
||||
type === 'DateTime' ||
|
||||
type === 'JSON'
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the field is of type enum
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
isEnumType: type => {
|
||||
return type === 'enumeration';
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns all fields that are not of type array
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* isNotOfTypeArray([String])
|
||||
* // => false
|
||||
* isNotOfTypeArray(String!)
|
||||
* // => true
|
||||
*/
|
||||
isNotOfTypeArray: type => {
|
||||
return !/(\[\w+!?\])/.test(type);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns all fields of type Integer or float
|
||||
*/
|
||||
isNumberType: type => {
|
||||
return type === 'Int' || type === 'Float';
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert parameters to valid filters parameters.
|
||||
*
|
||||
* @return Object
|
||||
*/
|
||||
|
||||
convertToParams: params => {
|
||||
return Object.keys(params).reduce((acc, current) => {
|
||||
return Object.assign(acc, {
|
||||
[`_${current}`]: params[current],
|
||||
});
|
||||
}, {});
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a list of fields that have type included in fieldTypes.
|
||||
*/
|
||||
getFieldsByTypes: (fields, typeCheck, returnType) => {
|
||||
return _.reduce(
|
||||
fields,
|
||||
(acc, fieldType, fieldName) => {
|
||||
if (typeCheck(fieldType)) {
|
||||
acc[fieldName] = returnType(fieldType, fieldName);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Use the field resolver otherwise fall through the field value
|
||||
*
|
||||
* @returns {function}
|
||||
*/
|
||||
fieldResolver: (field, key) => {
|
||||
return object => {
|
||||
const resolver =
|
||||
field.resolve ||
|
||||
function resolver(obj, options, context) {
|
||||
// eslint-disable-line no-unused-vars
|
||||
return obj[key];
|
||||
};
|
||||
return resolver(object);
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Create fields resolvers
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
createFieldsResolver: function(fields, resolver, typeCheck) {
|
||||
return Object.keys(fields).reduce((acc, fieldKey) => {
|
||||
const field = fields[fieldKey];
|
||||
// Check if the field is of the correct type
|
||||
if (typeCheck(field)) {
|
||||
return _.set(acc, fieldKey, (obj, options, context) => {
|
||||
return resolver(
|
||||
obj,
|
||||
options,
|
||||
context,
|
||||
this.fieldResolver(field, fieldKey),
|
||||
fieldKey,
|
||||
obj,
|
||||
field,
|
||||
);
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert non-primitive type to string (non-primitive types corresponds to a reference to an other model)
|
||||
*
|
||||
* @returns {String}
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* extractType(String!)
|
||||
* // => String
|
||||
*
|
||||
* extractType(user)
|
||||
* // => ID
|
||||
*
|
||||
* extractType(ENUM_TEST_FIELD, enumeration)
|
||||
* // => String
|
||||
*
|
||||
*/
|
||||
extractType: function(_type, attributeType) {
|
||||
return this.isPrimitiveType(_type)
|
||||
? _type.replace('!', '')
|
||||
: this.isEnumType(attributeType)
|
||||
? 'String'
|
||||
: 'ID';
|
||||
},
|
||||
|
||||
/**
|
||||
* Build the mongoose aggregator by applying the filters
|
||||
*/
|
||||
getModelAggregator: function(model, filters = {}) {
|
||||
const aggregation = model.aggregate();
|
||||
if (!_.isEmpty(filters.where)) {
|
||||
aggregation.match(filters.where);
|
||||
}
|
||||
if (filters.limit) {
|
||||
aggregation.limit(filters.limit);
|
||||
}
|
||||
return aggregation;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create the resolvers for each aggregation field
|
||||
*
|
||||
* @return {Object}
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* const model = // Strapi model
|
||||
*
|
||||
* const fields = {
|
||||
* username: String,
|
||||
* age: Int,
|
||||
* }
|
||||
*
|
||||
* const typeCheck = (type) => type === 'Int' || type === 'Float',
|
||||
*
|
||||
* const fieldsResoler = createAggregationFieldsResolver(model, fields, 'sum', typeCheck);
|
||||
*
|
||||
* // => {
|
||||
* age: function ageResolver() { .... }
|
||||
* }
|
||||
*/
|
||||
createAggregationFieldsResolver: function(
|
||||
model,
|
||||
fields,
|
||||
operation,
|
||||
typeCheck,
|
||||
) {
|
||||
return this.createFieldsResolver(
|
||||
fields,
|
||||
async (obj, options, context, fieldResolver, fieldKey) => {
|
||||
// eslint-disable-line no-unused-vars
|
||||
const result = await this.getModelAggregator(model, obj).group({
|
||||
_id: null,
|
||||
[fieldKey]: { [`$${operation}`]: `$${fieldKey}` },
|
||||
});
|
||||
return _.get(result, `0.${fieldKey}`);
|
||||
},
|
||||
typeCheck,
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Correctly format the data returned by the group by
|
||||
*/
|
||||
preProcessGroupByData: function({ result, fieldKey, filters, modelName }) {
|
||||
const _result = _.toArray(result);
|
||||
return _.map(_result, value => {
|
||||
const params = Object.assign(
|
||||
{},
|
||||
this.convertToParams(_.omit(filters, 'where')),
|
||||
filters.where,
|
||||
{
|
||||
[fieldKey]: value._id,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
key: value._id,
|
||||
connection: strapi.utils.models.convertParams(modelName, params),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Create the resolvers for each group by field
|
||||
*
|
||||
* @return {Object}
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* const model = // Strapi model
|
||||
* const fields = {
|
||||
* username: [UserConnectionUsername],
|
||||
* email: [UserConnectionEmail],
|
||||
* }
|
||||
* const fieldsResoler = createGroupByFieldsResolver(model, fields);
|
||||
*
|
||||
* // => {
|
||||
* username: function usernameResolver() { .... }
|
||||
* email: function emailResolver() { .... }
|
||||
* }
|
||||
*/
|
||||
createGroupByFieldsResolver: function(model, fields, name) {
|
||||
return this.createFieldsResolver(
|
||||
fields,
|
||||
async (obj, options, context, fieldResolver, fieldKey) => {
|
||||
const result = await this.getModelAggregator(model, obj).group({
|
||||
_id: `$${fieldKey}`,
|
||||
});
|
||||
|
||||
return this.preProcessGroupByData({
|
||||
result,
|
||||
fieldKey,
|
||||
filters: obj,
|
||||
modelName: name,
|
||||
});
|
||||
},
|
||||
() => true,
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* This method is the entry point to the GraphQL's Aggregation.
|
||||
* It takes as param the model and its fields and it'll create the aggregation types and resolver to it
|
||||
* Example:
|
||||
* type User {
|
||||
* username: String,
|
||||
* age: Int,
|
||||
* }
|
||||
*
|
||||
* It'll create
|
||||
* type UserConnection {
|
||||
* values: [User],
|
||||
* groupBy: UserGroupBy,
|
||||
* aggreate: UserAggregate
|
||||
* }
|
||||
*
|
||||
* type UserAggregate {
|
||||
* count: Int
|
||||
* sum: UserAggregateSum
|
||||
* avg: UserAggregateAvg
|
||||
* }
|
||||
*
|
||||
* type UserAggregateSum {
|
||||
* age: Float
|
||||
* }
|
||||
*
|
||||
* type UserAggregateAvg {
|
||||
* age: Float
|
||||
* }
|
||||
*
|
||||
* type UserGroupBy {
|
||||
* username: [UserConnectionUsername]
|
||||
* age: [UserConnectionAge]
|
||||
* }
|
||||
*
|
||||
* type UserConnectionUsername {
|
||||
* key: String
|
||||
* connection: UserConnection
|
||||
* }
|
||||
*
|
||||
* type UserConnectionAge {
|
||||
* key: Int
|
||||
* connection: UserConnection
|
||||
* }
|
||||
*
|
||||
*/
|
||||
formatModelConnectionsGQL: function(fields, model, name, modelResolver) {
|
||||
const { globalId } = model;
|
||||
|
||||
const connectionGlobalId = `${globalId}Connection`;
|
||||
const aggregatorFormat = this.formatConnectionAggregator(
|
||||
fields,
|
||||
model,
|
||||
name,
|
||||
);
|
||||
const groupByFormat = this.formatConnectionGroupBy(fields, model, name);
|
||||
const connectionFields = {
|
||||
values: `[${globalId}]`,
|
||||
groupBy: `${globalId}GroupBy`,
|
||||
aggregate: `${globalId}Aggregator`,
|
||||
};
|
||||
|
||||
let modelConnectionTypes = `type ${connectionGlobalId} {${Schema.formatGQL(
|
||||
connectionFields,
|
||||
)}}\n\n`;
|
||||
if (aggregatorFormat) {
|
||||
modelConnectionTypes += aggregatorFormat.type;
|
||||
}
|
||||
modelConnectionTypes += groupByFormat.type;
|
||||
|
||||
return {
|
||||
globalId: connectionGlobalId,
|
||||
type: modelConnectionTypes,
|
||||
query: {
|
||||
[`${pluralize.plural(
|
||||
name,
|
||||
)}Connection(sort: String, limit: Int, start: Int, where: JSON)`]: connectionGlobalId,
|
||||
},
|
||||
resolver: {
|
||||
Query: {
|
||||
[`${pluralize.plural(name)}Connection`]: (obj, options, context) => {
|
||||
// eslint-disable-line no-unused-vars
|
||||
const params = Object.assign(
|
||||
{},
|
||||
this.convertToParams(_.omit(options, 'where')),
|
||||
options.where,
|
||||
);
|
||||
return strapi.utils.models.convertParams(name, params);
|
||||
},
|
||||
},
|
||||
[connectionGlobalId]: {
|
||||
values: (obj, option, context) => {
|
||||
// Object here contains the key/value of the field that has been grouped-by
|
||||
// for instance obj = { where: { country: 'USA' } } so the values here needs to be filtered according to the parent value
|
||||
return modelResolver(obj, obj, context);
|
||||
},
|
||||
groupBy: (obj, option, context) => {
|
||||
// eslint-disable-line no-unused-vars
|
||||
// There is noting to resolve here, it's the aggregation resolver that will take care of it
|
||||
return obj;
|
||||
},
|
||||
aggregate: (obj, option, context) => {
|
||||
// eslint-disable-line no-unused-vars
|
||||
// There is noting to resolve here, it's the aggregation resolver that will take care of it
|
||||
return obj;
|
||||
},
|
||||
},
|
||||
...aggregatorFormat.resolver,
|
||||
...groupByFormat.resolver,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a list of fields that have type included in fieldTypes.
|
||||
*/
|
||||
getFieldsByTypes: (fields, typeCheck, returnType) => {
|
||||
return _.reduce(
|
||||
fields,
|
||||
(acc, fieldType, fieldName) => {
|
||||
if (typeCheck(fieldType)) {
|
||||
acc[fieldName] = returnType(fieldType, fieldName);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate the connection type of each non-array field of the model
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
generateConnectionFieldsTypes: function(fields, model) {
|
||||
const { globalId, attributes } = model;
|
||||
const primitiveFields = this.getFieldsByTypes(
|
||||
fields,
|
||||
this.isNotOfTypeArray,
|
||||
(type, name) => this.extractType(type, (attributes[name] || {}).type),
|
||||
);
|
||||
|
||||
const connectionFields = _.mapValues(primitiveFields, fieldType => ({
|
||||
key: fieldType,
|
||||
connection: `${globalId}Connection`,
|
||||
}));
|
||||
|
||||
return Object.keys(primitiveFields)
|
||||
.map(
|
||||
fieldKey =>
|
||||
`type ${globalId}Connection${_.upperFirst(
|
||||
fieldKey,
|
||||
)} {${Schema.formatGQL(connectionFields[fieldKey])}}`,
|
||||
)
|
||||
.join('\n\n');
|
||||
},
|
||||
|
||||
formatConnectionGroupBy: function(fields, model, name) {
|
||||
const { globalId } = model;
|
||||
const groupByGlobalId = `${globalId}GroupBy`;
|
||||
|
||||
// Extract all primitive fields and change their types
|
||||
const groupByFields = this.getFieldsByTypes(
|
||||
fields,
|
||||
this.isNotOfTypeArray,
|
||||
(fieldType, fieldName) =>
|
||||
`[${globalId}Connection${_.upperFirst(fieldName)}]`,
|
||||
);
|
||||
|
||||
// Get the generated field types
|
||||
let groupByTypes = `type ${groupByGlobalId} {${Schema.formatGQL(
|
||||
groupByFields,
|
||||
)}}\n\n`;
|
||||
groupByTypes += this.generateConnectionFieldsTypes(fields, model);
|
||||
|
||||
return {
|
||||
globalId: groupByGlobalId,
|
||||
type: groupByTypes,
|
||||
resolver: {
|
||||
[groupByGlobalId]: this.createGroupByFieldsResolver(
|
||||
model,
|
||||
groupByFields,
|
||||
name,
|
||||
),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
formatConnectionAggregator: function(fields, model) {
|
||||
const { globalId } = model;
|
||||
|
||||
// Extract all fields of type Integer and Float and change their type to Float
|
||||
const numericFields = this.getFieldsByTypes(
|
||||
fields,
|
||||
this.isNumberType,
|
||||
() => 'Float',
|
||||
);
|
||||
|
||||
// Don't create an aggregator field if the model has not number fields
|
||||
const aggregatorGlobalId = `${globalId}Aggregator`;
|
||||
const initialFields = {
|
||||
count: 'Int',
|
||||
};
|
||||
|
||||
// Only add the aggregator's operations if there are some numeric fields
|
||||
if (!_.isEmpty(numericFields)) {
|
||||
['sum', 'avg', 'min', 'max'].forEach(agg => {
|
||||
initialFields[agg] = `${aggregatorGlobalId}${_.startCase(agg)}`;
|
||||
});
|
||||
}
|
||||
|
||||
const gqlNumberFormat = Schema.formatGQL(numericFields);
|
||||
let aggregatorTypes = `type ${aggregatorGlobalId} {${Schema.formatGQL(
|
||||
initialFields,
|
||||
)}}\n\n`;
|
||||
|
||||
let resolvers = {
|
||||
[aggregatorGlobalId]: {
|
||||
count: async (obj, options, context) => {
|
||||
// eslint-disable-line no-unused-vars
|
||||
// Object here corresponds to the filter that needs to be applied to the aggregation
|
||||
const result = await this.getModelAggregator(model, obj).group({
|
||||
_id: null,
|
||||
count: { $sum: 1 },
|
||||
});
|
||||
|
||||
return _.get(result, '0.count');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Only add the aggregator's operations types and resolver if there are some numeric fields
|
||||
if (!_.isEmpty(numericFields)) {
|
||||
// Returns the actual object and handle aggregation in the query resolvers
|
||||
const defaultAggregatorFunc = (obj, options, context) => {
|
||||
// eslint-disable-line no-unused-vars
|
||||
return obj;
|
||||
};
|
||||
|
||||
aggregatorTypes += `type ${aggregatorGlobalId}Sum {${gqlNumberFormat}}\n\n`;
|
||||
aggregatorTypes += `type ${aggregatorGlobalId}Avg {${gqlNumberFormat}}\n\n`;
|
||||
aggregatorTypes += `type ${aggregatorGlobalId}Min {${gqlNumberFormat}}\n\n`;
|
||||
aggregatorTypes += `type ${aggregatorGlobalId}Max {${gqlNumberFormat}}\n\n`;
|
||||
|
||||
_.merge(resolvers[aggregatorGlobalId], {
|
||||
sum: defaultAggregatorFunc,
|
||||
avg: defaultAggregatorFunc,
|
||||
min: defaultAggregatorFunc,
|
||||
max: defaultAggregatorFunc,
|
||||
});
|
||||
|
||||
resolvers = {
|
||||
...resolvers,
|
||||
[`${aggregatorGlobalId}Sum`]: this.createAggregationFieldsResolver(
|
||||
model,
|
||||
fields,
|
||||
'sum',
|
||||
this.isNumberType,
|
||||
),
|
||||
[`${aggregatorGlobalId}Avg`]: this.createAggregationFieldsResolver(
|
||||
model,
|
||||
fields,
|
||||
'avg',
|
||||
this.isNumberType,
|
||||
),
|
||||
[`${aggregatorGlobalId}Min`]: this.createAggregationFieldsResolver(
|
||||
model,
|
||||
fields,
|
||||
'min',
|
||||
this.isNumberType,
|
||||
),
|
||||
[`${aggregatorGlobalId}Max`]: this.createAggregationFieldsResolver(
|
||||
model,
|
||||
fields,
|
||||
'max',
|
||||
this.isNumberType,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
globalId: aggregatorGlobalId,
|
||||
type: aggregatorTypes,
|
||||
resolver: resolvers,
|
||||
};
|
||||
},
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
240
packages/strapi-plugin-graphql/services/Mutation.js
Normal file
240
packages/strapi-plugin-graphql/services/Mutation.js
Normal file
@ -0,0 +1,240 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Mutation.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 Query = require('./Query.js');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Convert parameters to valid filters parameters.
|
||||
*
|
||||
* @return Object
|
||||
*/
|
||||
|
||||
convertToParams: params => {
|
||||
return Object.keys(params).reduce((acc, current) => {
|
||||
return Object.assign(acc, {
|
||||
[`_${current}`]: params[current],
|
||||
});
|
||||
}, {});
|
||||
},
|
||||
|
||||
/**
|
||||
* Execute policies before the specified resolver.
|
||||
*
|
||||
* @return Promise or Error.
|
||||
*/
|
||||
|
||||
composeMutationResolver: function(_schema, plugin, name, action) {
|
||||
// Extract custom resolver or type description.
|
||||
const { resolver: handler = {} } = _schema;
|
||||
|
||||
const queryName = `${action}${_.capitalize(name)}`;
|
||||
|
||||
// Retrieve policies.
|
||||
const policies = _.get(handler, `Mutation.${queryName}.policies`, []);
|
||||
|
||||
// Retrieve resolverOf.
|
||||
const resolverOf = _.get(handler, `Mutation.${queryName}.resolverOf`, '');
|
||||
|
||||
const policiesFn = [];
|
||||
|
||||
// Boolean to define if the resolver is going to be a resolver 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, `Mutation.${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 = _.get(
|
||||
controllers,
|
||||
`${name}.${action === 'delete' ? 'destroy' : action}`,
|
||||
);
|
||||
|
||||
if (!controller) {
|
||||
return new Error(
|
||||
`Cannot find the controller's action ${name}.${action}`,
|
||||
);
|
||||
}
|
||||
|
||||
// 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}.${action === 'delete' ? 'destroy' : action}`,
|
||||
},
|
||||
undefined,
|
||||
plugin,
|
||||
),
|
||||
);
|
||||
|
||||
// Make the query compatible with our controller by
|
||||
// setting in the context the parameters.
|
||||
return async (ctx, next) => {
|
||||
return controller(ctx, next);
|
||||
};
|
||||
})();
|
||||
|
||||
// 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.push('plugins.users-permissions.permissions');
|
||||
}
|
||||
|
||||
// Populate policies.
|
||||
policies.forEach(policy =>
|
||||
policyUtils.get(
|
||||
policy,
|
||||
plugin,
|
||||
policiesFn,
|
||||
`GraphQL query "${queryName}"`,
|
||||
name,
|
||||
),
|
||||
);
|
||||
|
||||
return async (obj, options, { context }) => {
|
||||
// Hack to be able to handle permissions for each query.
|
||||
const ctx = Object.assign(_.clone(context), {
|
||||
request: Object.assign(_.clone(context.request), {
|
||||
graphql: null,
|
||||
}),
|
||||
});
|
||||
|
||||
// Execute policies stack.
|
||||
const policy = await strapi.koaMiddlewares.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)) {
|
||||
context.params = Query.convertToParams(options.input.where || {});
|
||||
context.request.body = options.input.data || {};
|
||||
|
||||
if (isController) {
|
||||
const values = await resolver.call(null, context);
|
||||
|
||||
if (ctx.body) {
|
||||
return {
|
||||
[pluralize.singular(name)]: ctx.body,
|
||||
};
|
||||
}
|
||||
|
||||
const body = values && values.toJSON ? values.toJSON() : values;
|
||||
|
||||
return {
|
||||
[pluralize.singular(name)]: body,
|
||||
};
|
||||
}
|
||||
|
||||
return resolver.call(null, obj, options, context);
|
||||
}
|
||||
|
||||
// Resolver can be a promise.
|
||||
return resolver;
|
||||
};
|
||||
},
|
||||
};
|
||||
285
packages/strapi-plugin-graphql/services/Query.js
Normal file
285
packages/strapi-plugin-graphql/services/Query.js
Normal file
@ -0,0 +1,285 @@
|
||||
'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;
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Convert parameters to valid filters parameters.
|
||||
*
|
||||
* @return Object
|
||||
*/
|
||||
|
||||
convertToParams: params => {
|
||||
return Object.keys(params).reduce((acc, current) => {
|
||||
return Object.assign(acc, {
|
||||
[`_${current}`]: params[current],
|
||||
});
|
||||
}, {});
|
||||
},
|
||||
|
||||
/**
|
||||
* 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;
|
||||
},
|
||||
|
||||
/**
|
||||
* Execute policies before the specified resolver.
|
||||
*
|
||||
* @return Promise or Error.
|
||||
*/
|
||||
|
||||
composeQueryResolver: function(_schema, plugin, name, isSingular) {
|
||||
const params = {
|
||||
model: name,
|
||||
};
|
||||
|
||||
const model = plugin
|
||||
? strapi.plugins[plugin].models[name]
|
||||
: strapi.models[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 resolver 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,
|
||||
[model.primaryKey]: ctx.params.id,
|
||||
};
|
||||
|
||||
// Return the controller.
|
||||
return controller(ctx, next);
|
||||
};
|
||||
}
|
||||
|
||||
// Plural.
|
||||
return async (ctx, next) => {
|
||||
ctx.params = this.amountLimiting(ctx.params);
|
||||
ctx.query = Object.assign(
|
||||
this.convertToParams(_.omit(ctx.params, 'where')),
|
||||
ctx.params.where,
|
||||
);
|
||||
|
||||
return controller(ctx, next);
|
||||
};
|
||||
})();
|
||||
|
||||
// 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.push('plugins.users-permissions.permissions');
|
||||
}
|
||||
|
||||
// Populate policies.
|
||||
policies.forEach(policy =>
|
||||
policyUtils.get(
|
||||
policy,
|
||||
plugin,
|
||||
policiesFn,
|
||||
`GraphQL query "${queryName}"`,
|
||||
name,
|
||||
),
|
||||
);
|
||||
|
||||
return async (obj, options, { context }) => {
|
||||
// Hack to be able to handle permissions for each query.
|
||||
const ctx = Object.assign(_.clone(context), {
|
||||
request: Object.assign(_.clone(context.request), {
|
||||
graphql: null,
|
||||
}),
|
||||
});
|
||||
|
||||
// Execute policies stack.
|
||||
const policy = await strapi.koaMiddlewares.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)) {
|
||||
context.query = this.convertToParams(options);
|
||||
context.params = this.amountLimiting(options);
|
||||
|
||||
if (isController) {
|
||||
const values = await resolver.call(null, context);
|
||||
|
||||
if (ctx.body) {
|
||||
return ctx.body;
|
||||
}
|
||||
|
||||
return values && values.toJSON ? values.toJSON() : values;
|
||||
}
|
||||
|
||||
return resolver.call(null, obj, options, context);
|
||||
}
|
||||
|
||||
// Resolver can be a promise.
|
||||
return resolver;
|
||||
};
|
||||
},
|
||||
};
|
||||
443
packages/strapi-plugin-graphql/services/Resolvers.js
Normal file
443
packages/strapi-plugin-graphql/services/Resolvers.js
Normal file
@ -0,0 +1,443 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* GraphQL.js service
|
||||
*
|
||||
* @description: A set of functions similar to controller's actions to avoid code duplication.
|
||||
*/
|
||||
|
||||
const _ = require('lodash');
|
||||
const pluralize = require('pluralize');
|
||||
const Aggregator = require('./Aggregator');
|
||||
const Query = require('./Query.js');
|
||||
const Mutation = require('./Mutation.js');
|
||||
const Types = require('./Types.js');
|
||||
const Schema = require('./Schema.js');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Construct the GraphQL query & definition and apply the right resolvers.
|
||||
*
|
||||
* @return Object
|
||||
*/
|
||||
|
||||
shadowCRUD: function(models, plugin) {
|
||||
// Retrieve generic service from the Content Manager plugin.
|
||||
const resolvers =
|
||||
strapi.plugins['content-manager'].services['contentmanager'];
|
||||
|
||||
const initialState = {
|
||||
definition: '',
|
||||
query: {},
|
||||
mutation: {},
|
||||
resolver: { Query: {}, Mutation: {} },
|
||||
};
|
||||
|
||||
if (_.isEmpty(models)) {
|
||||
return initialState;
|
||||
}
|
||||
|
||||
return models.reduce((acc, name) => {
|
||||
const model = plugin
|
||||
? strapi.plugins[plugin].models[name]
|
||||
: strapi.models[name];
|
||||
|
||||
// Setup initial state with default attribute that should be displayed
|
||||
// but these attributes are not properly defined in the models.
|
||||
const initialState = {
|
||||
[model.primaryKey]: 'ID!',
|
||||
};
|
||||
|
||||
const globalId = model.globalId;
|
||||
const _schema = _.cloneDeep(
|
||||
_.get(strapi.plugins, 'graphql.config._schema.graphql', {}),
|
||||
);
|
||||
|
||||
if (!acc.resolver[globalId]) {
|
||||
acc.resolver[globalId] = {};
|
||||
}
|
||||
|
||||
// Add timestamps attributes.
|
||||
if (_.get(model, 'options.timestamps') === true) {
|
||||
Object.assign(initialState, {
|
||||
createdAt: 'DateTime!',
|
||||
updatedAt: 'DateTime!',
|
||||
});
|
||||
|
||||
Object.assign(acc.resolver[globalId], {
|
||||
createdAt: (obj, options, context) => {
|
||||
// eslint-disable-line no-unused-vars
|
||||
return obj.createdAt || obj.created_at;
|
||||
},
|
||||
updatedAt: (obj, options, context) => {
|
||||
// eslint-disable-line no-unused-vars
|
||||
return obj.updatedAt || obj.updated_at;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Retrieve user customisation.
|
||||
const { type = {}, resolver = {} } = _schema;
|
||||
|
||||
// Convert our layer Model to the GraphQL DL.
|
||||
const attributes = Object.keys(model.attributes)
|
||||
.filter(attribute => model.attributes[attribute].private !== true)
|
||||
.reduce((acc, attribute) => {
|
||||
// Convert our type to the GraphQL type.
|
||||
acc[attribute] = Types.convertType({
|
||||
definition: model.attributes[attribute],
|
||||
modelName: globalId,
|
||||
attributeName: attribute,
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, initialState);
|
||||
|
||||
// Detect enum and generate it for the schema definition
|
||||
const enums = Object.keys(model.attributes)
|
||||
.filter(attribute => model.attributes[attribute].type === 'enumeration')
|
||||
.map(attribute => {
|
||||
const definition = model.attributes[attribute];
|
||||
|
||||
return `enum ${Types.convertEnumType(
|
||||
definition,
|
||||
globalId,
|
||||
attribute,
|
||||
)} { ${definition.enum.join(' \n ')} }`;
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
acc.definition += enums;
|
||||
|
||||
// Add parameters to optimize association query.
|
||||
(model.associations || [])
|
||||
.filter(association => association.type === 'collection')
|
||||
.forEach(association => {
|
||||
attributes[
|
||||
`${
|
||||
association.alias
|
||||
}(sort: String, limit: Int, start: Int, where: JSON)`
|
||||
] = attributes[association.alias];
|
||||
|
||||
delete attributes[association.alias];
|
||||
});
|
||||
|
||||
acc.definition += `${Schema.getDescription(
|
||||
type[globalId],
|
||||
model,
|
||||
)}type ${globalId} {${Schema.formatGQL(
|
||||
attributes,
|
||||
type[globalId],
|
||||
model,
|
||||
)}}\n\n`;
|
||||
|
||||
// Add definition to the schema but this type won't be "queriable" or "mutable".
|
||||
if (
|
||||
type[model.globalId] === false ||
|
||||
_.get(type, `${model.globalId}.enabled`) === false
|
||||
) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
// Build resolvers.
|
||||
const queries = {
|
||||
singular:
|
||||
_.get(resolver, `Query.${pluralize.singular(name)}`) !== false
|
||||
? Query.composeQueryResolver(_schema, plugin, name, true)
|
||||
: null,
|
||||
plural:
|
||||
_.get(resolver, `Query.${pluralize.plural(name)}`) !== false
|
||||
? Query.composeQueryResolver(_schema, plugin, name, false)
|
||||
: null,
|
||||
};
|
||||
|
||||
Object.keys(queries).forEach(type => {
|
||||
// The query cannot be built.
|
||||
if (_.isError(queries[type])) {
|
||||
console.error(queries[type]);
|
||||
strapi.stop();
|
||||
}
|
||||
|
||||
// Only create query if the function is available.
|
||||
if (_.isFunction(queries[type])) {
|
||||
if (type === 'singular') {
|
||||
Object.assign(acc.query, {
|
||||
[`${pluralize.singular(name)}(id: ID!)`]: model.globalId,
|
||||
});
|
||||
} else {
|
||||
Object.assign(acc.query, {
|
||||
[`${pluralize.plural(
|
||||
name,
|
||||
)}(sort: String, limit: Int, start: Int, where: JSON)`]: `[${
|
||||
model.globalId
|
||||
}]`,
|
||||
});
|
||||
}
|
||||
|
||||
_.merge(acc.resolver.Query, {
|
||||
[type === 'singular'
|
||||
? pluralize.singular(name)
|
||||
: pluralize.plural(name)]: queries[type],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// TODO:
|
||||
// - Implement batch methods (need to update the content-manager as well).
|
||||
// - Implement nested transactional methods (create/update).
|
||||
const mutations = {
|
||||
create:
|
||||
_.get(resolver, `Mutation.create${_.capitalize(name)}`) !== false
|
||||
? Mutation.composeMutationResolver(_schema, plugin, name, 'create')
|
||||
: null,
|
||||
update:
|
||||
_.get(resolver, `Mutation.update${_.capitalize(name)}`) !== false
|
||||
? Mutation.composeMutationResolver(_schema, plugin, name, 'update')
|
||||
: null,
|
||||
delete:
|
||||
_.get(resolver, `Mutation.delete${_.capitalize(name)}`) !== false
|
||||
? Mutation.composeMutationResolver(_schema, plugin, name, 'delete')
|
||||
: null,
|
||||
};
|
||||
|
||||
// Add model Input definition.
|
||||
acc.definition += Types.generateInputModel(model, name);
|
||||
|
||||
Object.keys(mutations).forEach(type => {
|
||||
if (_.isFunction(mutations[type])) {
|
||||
let mutationDefinition;
|
||||
let mutationName = `${type}${_.capitalize(name)}`;
|
||||
|
||||
// Generate the Input for this specific action.
|
||||
acc.definition += Types.generateInputPayloadArguments(
|
||||
model,
|
||||
name,
|
||||
type,
|
||||
);
|
||||
|
||||
switch (type) {
|
||||
case 'create':
|
||||
mutationDefinition = {
|
||||
[`${mutationName}(input: ${mutationName}Input)`]: `${mutationName}Payload`,
|
||||
};
|
||||
|
||||
break;
|
||||
case 'update':
|
||||
mutationDefinition = {
|
||||
[`${mutationName}(input: ${mutationName}Input)`]: `${mutationName}Payload`,
|
||||
};
|
||||
|
||||
break;
|
||||
case 'delete':
|
||||
mutationDefinition = {
|
||||
[`${mutationName}(input: ${mutationName}Input)`]: `${mutationName}Payload`,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
// Nothing.
|
||||
}
|
||||
|
||||
// Assign mutation definition to global definition.
|
||||
Object.assign(acc.mutation, mutationDefinition);
|
||||
|
||||
// Assign resolver to this mutation and merge it with the others.
|
||||
_.merge(acc.resolver.Mutation, {
|
||||
[`${mutationName}`]: mutations[type],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// TODO:
|
||||
// - Add support for Graphql Aggregation in Bookshelf ORM
|
||||
if (model.orm === 'mongoose') {
|
||||
// Generation the aggregation for the given model
|
||||
const modelAggregator = Aggregator.formatModelConnectionsGQL(
|
||||
attributes,
|
||||
model,
|
||||
name,
|
||||
queries.plural,
|
||||
);
|
||||
if (modelAggregator) {
|
||||
acc.definition += modelAggregator.type;
|
||||
if (!acc.resolver[modelAggregator.globalId]) {
|
||||
acc.resolver[modelAggregator.globalId] = {};
|
||||
}
|
||||
|
||||
_.merge(acc.resolver, modelAggregator.resolver);
|
||||
_.merge(acc.query, modelAggregator.query);
|
||||
}
|
||||
}
|
||||
|
||||
// Build associations queries.
|
||||
(model.associations || []).forEach(association => {
|
||||
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;
|
||||
|
||||
// Set the _type only when the value is defined
|
||||
if (entry[association.alias]) {
|
||||
entry[association.alias]._type = _.upperFirst(
|
||||
association.model,
|
||||
);
|
||||
}
|
||||
|
||||
return entry[association.alias];
|
||||
},
|
||||
});
|
||||
case 'manyMorphToOne':
|
||||
case 'manyMorphToMany':
|
||||
case 'manyToManyMorph':
|
||||
return _.merge(acc.resolver[globalId], {
|
||||
[association.alias]: async (obj, options, context) => {
|
||||
// eslint-disable-line no-unused-vars
|
||||
const [withRelated, withoutRelated] = await Promise.all([
|
||||
resolvers.fetch(
|
||||
{
|
||||
id: obj[model.primaryKey],
|
||||
model: name,
|
||||
},
|
||||
plugin,
|
||||
[association.alias],
|
||||
false,
|
||||
),
|
||||
resolvers.fetch(
|
||||
{
|
||||
id: obj[model.primaryKey],
|
||||
model: name,
|
||||
},
|
||||
plugin,
|
||||
[],
|
||||
),
|
||||
]);
|
||||
|
||||
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) => {
|
||||
const type =
|
||||
_.get(
|
||||
withoutRelated,
|
||||
`${association.alias}.${index}.kind`,
|
||||
) ||
|
||||
_.upperFirst(
|
||||
_.camelCase(
|
||||
_.get(
|
||||
withoutRelated,
|
||||
`${association.alias}.${index}.${
|
||||
association.alias
|
||||
}_type`,
|
||||
),
|
||||
),
|
||||
) ||
|
||||
_.upperFirst(_.camelCase(association[association.type]));
|
||||
|
||||
entry._type = type;
|
||||
|
||||
return entry;
|
||||
});
|
||||
|
||||
return entry[association.alias];
|
||||
},
|
||||
});
|
||||
default:
|
||||
}
|
||||
|
||||
_.merge(acc.resolver[globalId], {
|
||||
[association.alias]: async (obj, options, context) => {
|
||||
// eslint-disable-line no-unused-vars
|
||||
// Construct parameters object to retrieve the correct related entries.
|
||||
const params = {
|
||||
model: association.model || association.collection,
|
||||
};
|
||||
|
||||
const queryOpts = {
|
||||
source: association.plugin,
|
||||
};
|
||||
|
||||
if (association.type === 'model') {
|
||||
params.id = obj[association.alias];
|
||||
} else {
|
||||
// Get refering model.
|
||||
const ref = association.plugin
|
||||
? strapi.plugins[association.plugin].models[params.model]
|
||||
: strapi.models[params.model];
|
||||
|
||||
// Apply optional arguments to make more precise nested request.
|
||||
const convertedParams = strapi.utils.models.convertParams(
|
||||
name,
|
||||
Query.convertToParams(Query.amountLimiting(options)),
|
||||
);
|
||||
const where = strapi.utils.models.convertParams(
|
||||
name,
|
||||
options.where || {},
|
||||
);
|
||||
|
||||
// Limit, order, etc.
|
||||
Object.assign(queryOpts, convertedParams);
|
||||
|
||||
// Skip.
|
||||
queryOpts.skip = convertedParams.start;
|
||||
|
||||
switch (association.nature) {
|
||||
case 'manyToMany': {
|
||||
if (association.dominant) {
|
||||
const arrayOfIds = (obj[association.alias] || []).map(
|
||||
related => {
|
||||
return related[ref.primaryKey] || related;
|
||||
},
|
||||
);
|
||||
|
||||
// Where.
|
||||
queryOpts.query = strapi.utils.models.convertParams(name, {
|
||||
// Construct the "where" query to only retrieve entries which are
|
||||
// related to this entry.
|
||||
[ref.primaryKey]: arrayOfIds,
|
||||
...where.where,
|
||||
}).where;
|
||||
}
|
||||
break;
|
||||
// falls through
|
||||
}
|
||||
default:
|
||||
// Where.
|
||||
queryOpts.query = strapi.utils.models.convertParams(name, {
|
||||
// Construct the "where" query to only retrieve entries which are
|
||||
// related to this entry.
|
||||
[association.via]: obj[ref.primaryKey],
|
||||
...where.where,
|
||||
}).where;
|
||||
}
|
||||
}
|
||||
|
||||
const value = await (association.model
|
||||
? resolvers.fetch(params, association.plugin, [])
|
||||
: resolvers.fetchAll(params, { ...queryOpts, populate: [] }));
|
||||
|
||||
return value && value.toJSON ? value.toJSON() : value;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, initialState);
|
||||
},
|
||||
};
|
||||
316
packages/strapi-plugin-graphql/services/Schema.js
Normal file
316
packages/strapi-plugin-graphql/services/Schema.js
Normal file
@ -0,0 +1,316 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* GraphQL.js service
|
||||
*
|
||||
* @description: A set of functions similar to controller's actions to avoid code duplication.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { gql, makeExecutableSchema } = require('apollo-server-koa');
|
||||
const _ = require('lodash');
|
||||
const graphql = require('graphql');
|
||||
const Query = require('./Query.js');
|
||||
const Mutation = require('./Mutation.js');
|
||||
const Types = require('./Types.js');
|
||||
const Resolvers = require('./Resolvers.js');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Receive an Object and return a string which is following the GraphQL specs.
|
||||
*
|
||||
* @return String
|
||||
*/
|
||||
|
||||
formatGQL: function(fields, description = {}, model = {}, 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 => {
|
||||
if (['{', '}'].includes(line)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const split = line.split(':');
|
||||
const attribute = _.trim(split[0]);
|
||||
const info =
|
||||
(_.isString(description[attribute])
|
||||
? description[attribute]
|
||||
: _.get(description[attribute], 'description')) ||
|
||||
_.get(model, `attributes.${attribute}.description`);
|
||||
const deprecated =
|
||||
_.get(description[attribute], 'deprecated') ||
|
||||
_.get(model, `attributes.${attribute}.deprecated`);
|
||||
|
||||
// Snakecase an attribute when we find a dash.
|
||||
if (attribute.indexOf('-') !== -1) {
|
||||
line = ` ${_.snakeCase(attribute)}: ${_.trim(split[1])}`;
|
||||
}
|
||||
|
||||
if (info) {
|
||||
line = ` """\n ${info}\n """\n${line}`;
|
||||
}
|
||||
|
||||
if (deprecated) {
|
||||
line = `${line} @deprecated(reason: "${deprecated}")`;
|
||||
}
|
||||
|
||||
return line;
|
||||
})
|
||||
.join('\n');
|
||||
} else if (type === 'query' || type === 'mutation') {
|
||||
return lines
|
||||
.map((line, index) => {
|
||||
if (['{', '}'].includes(line)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const split = Object.keys(fields)[index - 1].split('(');
|
||||
const attribute = _.trim(split[0]);
|
||||
const info = _.get(description[attribute], 'description');
|
||||
const deprecated = _.get(description[attribute], 'deprecated');
|
||||
|
||||
// Snakecase an attribute when we find a dash.
|
||||
if (attribute.indexOf('-') !== -1) {
|
||||
line = ` ${_.snakeCase(attribute)}(${_.trim(split[1])}`;
|
||||
}
|
||||
|
||||
if (info) {
|
||||
line = ` """\n ${info}\n """\n${line}`;
|
||||
}
|
||||
|
||||
if (deprecated) {
|
||||
line = `${line} @deprecated(reason: "${deprecated}")`;
|
||||
}
|
||||
|
||||
return line;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
return lines
|
||||
.map((line, index) => {
|
||||
if ([0, lines.length - 1].includes(index)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return line;
|
||||
})
|
||||
.join('\n');
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve description from variable and return a string which follow the GraphQL specs.
|
||||
*
|
||||
* @return String
|
||||
*/
|
||||
|
||||
getDescription: (description, model = {}) => {
|
||||
const format = '"""\n';
|
||||
|
||||
const str =
|
||||
_.get(description, '_description') || _.isString(description)
|
||||
? description
|
||||
: undefined || _.get(model, 'info.description');
|
||||
|
||||
if (str) {
|
||||
return `${format}${str}\n${format}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate GraphQL schema.
|
||||
*
|
||||
* @return Schema
|
||||
*/
|
||||
|
||||
generateSchema: function() {
|
||||
// Generate type definition and query/mutation for models.
|
||||
const shadowCRUD =
|
||||
strapi.plugins.graphql.config.shadowCRUD !== false
|
||||
? (() => {
|
||||
// Exclude core models.
|
||||
const models = Object.keys(strapi.models).filter(
|
||||
model => model !== 'core_store',
|
||||
);
|
||||
|
||||
// Reproduce the same pattern for each plugin.
|
||||
return Object.keys(strapi.plugins).reduce((acc, plugin) => {
|
||||
const {
|
||||
definition,
|
||||
query,
|
||||
mutation,
|
||||
resolver,
|
||||
} = Resolvers.shadowCRUD(
|
||||
Object.keys(strapi.plugins[plugin].models),
|
||||
plugin,
|
||||
);
|
||||
|
||||
// We cannot put this in the merge because it's a string.
|
||||
acc.definition += definition || '';
|
||||
|
||||
return _.merge(acc, {
|
||||
query,
|
||||
resolver,
|
||||
mutation,
|
||||
});
|
||||
}, Resolvers.shadowCRUD(models));
|
||||
})()
|
||||
: { definition: '', query: '', mutation: '', resolver: '' };
|
||||
|
||||
// Extract custom definition, query or resolver.
|
||||
const {
|
||||
definition,
|
||||
query,
|
||||
mutation,
|
||||
resolver = {},
|
||||
} = strapi.plugins.graphql.config._schema.graphql;
|
||||
|
||||
// Polymorphic.
|
||||
const {
|
||||
polymorphicDef,
|
||||
polymorphicResolver,
|
||||
} = Types.addPolymorphicUnionType(definition, shadowCRUD.definition);
|
||||
|
||||
// Build resolvers.
|
||||
const resolvers =
|
||||
_.omitBy(
|
||||
_.merge(shadowCRUD.resolver, resolver, polymorphicResolver),
|
||||
_.isEmpty,
|
||||
) || {};
|
||||
|
||||
// Transform object to only contain function.
|
||||
Object.keys(resolvers).reduce((acc, type) => {
|
||||
return Object.keys(acc[type]).reduce((acc, resolver) => {
|
||||
// Disabled this query.
|
||||
if (acc[type][resolver] === false) {
|
||||
delete acc[type][resolver];
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (!_.isFunction(acc[type][resolver])) {
|
||||
console.log(type, resolver);
|
||||
|
||||
acc[type][resolver] = acc[type][resolver].resolver;
|
||||
}
|
||||
|
||||
if (
|
||||
_.isString(acc[type][resolver]) ||
|
||||
_.isPlainObject(acc[type][resolver])
|
||||
) {
|
||||
const { plugin = '' } = _.isPlainObject(acc[type][resolver])
|
||||
? acc[type][resolver]
|
||||
: {};
|
||||
|
||||
switch (type) {
|
||||
case 'Mutation':
|
||||
// TODO: Verify this...
|
||||
acc[type][resolver] = Mutation.composeMutationResolver(
|
||||
strapi.plugins.graphql.config._schema.graphql,
|
||||
plugin,
|
||||
resolver,
|
||||
);
|
||||
break;
|
||||
case 'Query':
|
||||
default:
|
||||
acc[type][resolver] = Query.composeQueryResolver(
|
||||
strapi.plugins.graphql.config._schema.graphql,
|
||||
plugin,
|
||||
resolver,
|
||||
'force', // Avoid singular/pluralize and force query name.
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, acc);
|
||||
}, resolvers);
|
||||
|
||||
// Return empty schema when there is no model.
|
||||
if (_.isEmpty(shadowCRUD.definition) && _.isEmpty(definition)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Concatenate.
|
||||
let typeDefs = `
|
||||
${definition}
|
||||
${shadowCRUD.definition}
|
||||
type Query {${shadowCRUD.query &&
|
||||
this.formatGQL(
|
||||
shadowCRUD.query,
|
||||
resolver.Query,
|
||||
null,
|
||||
'query',
|
||||
)}${query}}
|
||||
type Mutation {${shadowCRUD.mutation &&
|
||||
this.formatGQL(
|
||||
shadowCRUD.mutation,
|
||||
resolver.Mutation,
|
||||
null,
|
||||
'mutation',
|
||||
)}${mutation}}
|
||||
${Types.addCustomScalar(resolvers)}
|
||||
${Types.addInput()}
|
||||
${polymorphicDef}
|
||||
`;
|
||||
|
||||
// Build schema.
|
||||
const schema = makeExecutableSchema({
|
||||
typeDefs,
|
||||
resolvers,
|
||||
});
|
||||
|
||||
// Write schema.
|
||||
this.writeGenerateSchema(graphql.printSchema(schema));
|
||||
|
||||
// Remove custom scaler (like Upload);
|
||||
typeDefs = Types.removeCustomScalar(typeDefs, resolvers);
|
||||
|
||||
return {
|
||||
typeDefs: gql(typeDefs),
|
||||
resolvers,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Save into a file the readable GraphQL schema.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
|
||||
writeGenerateSchema: schema => {
|
||||
// Disable auto-reload.
|
||||
strapi.reload.isWatching = false;
|
||||
|
||||
const generatedFolder = path.resolve(
|
||||
strapi.config.appPath,
|
||||
'plugins',
|
||||
'graphql',
|
||||
'config',
|
||||
'generated',
|
||||
);
|
||||
|
||||
// Create folder if necessary.
|
||||
try {
|
||||
fs.accessSync(generatedFolder, fs.constants.R_OK | fs.constants.W_OK);
|
||||
} catch (err) {
|
||||
if (err && err.code === 'ENOENT') {
|
||||
fs.mkdirSync(generatedFolder);
|
||||
} else {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join(generatedFolder, 'schema.graphql'), schema);
|
||||
|
||||
strapi.reload.isWatching = true;
|
||||
},
|
||||
};
|
||||
239
packages/strapi-plugin-graphql/services/Types.js
Normal file
239
packages/strapi-plugin-graphql/services/Types.js
Normal file
@ -0,0 +1,239 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Types.js service
|
||||
*
|
||||
* @description: A set of functions to make the schema easier to build.
|
||||
*/
|
||||
|
||||
const _ = require('lodash');
|
||||
const { GraphQLUpload } = require('apollo-server-koa');
|
||||
const graphql = require('graphql');
|
||||
const GraphQLJSON = require('graphql-type-json');
|
||||
const GraphQLDateTime = require('graphql-type-datetime');
|
||||
const pluralize = require('pluralize');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Convert Strapi type to GraphQL type.
|
||||
* @param {Object} attribute Information about the attribute.
|
||||
* @param {Object} attribute.definition Definition of the attribute.
|
||||
* @param {String} attribute.modelName Name of the model which owns the attribute.
|
||||
* @param {String} attribute.attributeName Name of the attribute.
|
||||
* @return String
|
||||
*/
|
||||
|
||||
convertType: function({
|
||||
definition = {},
|
||||
modelName = '',
|
||||
attributeName = '',
|
||||
rootType = 'query',
|
||||
}) {
|
||||
// Type
|
||||
if (definition.type) {
|
||||
let type = 'String';
|
||||
|
||||
switch (definition.type) {
|
||||
// TODO: Handle fields of type Array, Perhaps default to [Int] or [String] ...
|
||||
case 'boolean':
|
||||
type = 'Boolean';
|
||||
break;
|
||||
case 'integer':
|
||||
type = 'Int';
|
||||
break;
|
||||
case 'decimal':
|
||||
type = 'Float';
|
||||
break;
|
||||
case 'float':
|
||||
type = 'Float';
|
||||
break;
|
||||
case 'time':
|
||||
case 'date':
|
||||
case 'datetime':
|
||||
case 'timestamp':
|
||||
type = 'DateTime';
|
||||
break;
|
||||
case 'enumeration':
|
||||
type = this.convertEnumType(definition, modelName, attributeName);
|
||||
break;
|
||||
}
|
||||
|
||||
if (definition.required) {
|
||||
type += '!';
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
const ref = definition.model || definition.collection;
|
||||
|
||||
// Association
|
||||
if (ref && ref !== '*') {
|
||||
// Add bracket or not
|
||||
const globalId = definition.plugin
|
||||
? strapi.plugins[definition.plugin].models[ref].globalId
|
||||
: strapi.models[ref].globalId;
|
||||
const plural = !_.isEmpty(definition.collection);
|
||||
|
||||
if (plural) {
|
||||
if (rootType === 'mutation') {
|
||||
return '[ID]';
|
||||
}
|
||||
|
||||
return `[${globalId}]`;
|
||||
}
|
||||
|
||||
if (rootType === 'mutation') {
|
||||
return 'ID';
|
||||
}
|
||||
|
||||
return globalId;
|
||||
}
|
||||
|
||||
if (rootType === 'mutation') {
|
||||
return definition.model ? 'ID' : '[ID]';
|
||||
}
|
||||
|
||||
return definition.model ? 'Morph' : '[Morph]';
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert Strapi enumeration to GraphQL Enum.
|
||||
* @param {Object} definition Definition of the attribute.
|
||||
* @param {String} model Name of the model which owns the attribute.
|
||||
* @param {String} field Name of the attribute.
|
||||
* @return String
|
||||
*/
|
||||
|
||||
convertEnumType: (definition, model, field) =>
|
||||
definition.enumName
|
||||
? definition.enumName
|
||||
: `ENUM_${model.toUpperCase()}_${field.toUpperCase()}`,
|
||||
|
||||
/**
|
||||
* Remove custom scalar type such as Upload because Apollo automatically adds it in the schema.
|
||||
* but we need to add it to print the schema on our side.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
|
||||
removeCustomScalar: (typeDefs, resolvers) => {
|
||||
delete resolvers.Upload;
|
||||
return typeDefs.replace('scalar Upload', '');
|
||||
},
|
||||
|
||||
/**
|
||||
* Add custom scalar type such as JSON.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
|
||||
addCustomScalar: resolvers => {
|
||||
Object.assign(resolvers, {
|
||||
JSON: GraphQLJSON,
|
||||
DateTime: GraphQLDateTime,
|
||||
Upload: GraphQLUpload,
|
||||
});
|
||||
|
||||
return 'scalar JSON \n scalar DateTime \n scalar Upload';
|
||||
},
|
||||
|
||||
/**
|
||||
* Add Union Type that contains the types defined by the user.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
|
||||
addPolymorphicUnionType: (customDefs, defs) => {
|
||||
const types = graphql
|
||||
.parse(customDefs + defs)
|
||||
.definitions.filter(
|
||||
def =>
|
||||
def.kind === 'ObjectTypeDefinition' && def.name.value !== 'Query',
|
||||
)
|
||||
.map(def => def.name.value);
|
||||
|
||||
if (types.length > 0) {
|
||||
return {
|
||||
polymorphicDef: `union Morph = ${types.join(' | ')}`,
|
||||
polymorphicResolver: {
|
||||
Morph: {
|
||||
__resolveType(obj, context, info) {
|
||||
// eslint-disable-line no-unused-vars
|
||||
return obj.kind || obj._type;
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
polymorphicDef: '',
|
||||
polymorphicResolver: {},
|
||||
};
|
||||
},
|
||||
|
||||
addInput: function() {
|
||||
return `
|
||||
input InputID { id: ID!}
|
||||
`;
|
||||
},
|
||||
|
||||
generateInputModel: function(model, name) {
|
||||
const globalId = model.globalId;
|
||||
const inputName = `${_.capitalize(name)}Input`;
|
||||
|
||||
/* eslint-disable */
|
||||
return `
|
||||
input ${inputName} {
|
||||
${Object.keys(model.attributes)
|
||||
.filter(attribute => model.attributes[attribute].private !== true)
|
||||
.map(attribute => {
|
||||
return `${attribute}: ${this.convertType({
|
||||
definition: model.attributes[attribute],
|
||||
modelName: globalId,
|
||||
attributeName: attribute,
|
||||
rootType: 'mutation',
|
||||
})}`;
|
||||
})
|
||||
.join('\n')}
|
||||
}
|
||||
`;
|
||||
/* eslint-enable */
|
||||
},
|
||||
|
||||
generateInputPayloadArguments: function(model, name, type, resolver) {
|
||||
if (_.get(resolver, `Mutation.${type}${_.capitalize(name)}`) === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const inputName = `${_.capitalize(name)}Input`;
|
||||
const payloadName = `${_.capitalize(name)}Payload`;
|
||||
|
||||
switch (type) {
|
||||
case 'create':
|
||||
return `
|
||||
input ${type}${inputName} { data: ${inputName} }
|
||||
type ${type}${payloadName} { ${pluralize.singular(name)}: ${
|
||||
model.globalId
|
||||
} }
|
||||
`;
|
||||
case 'update':
|
||||
return `
|
||||
input ${type}${inputName} { where: InputID, data: ${inputName} }
|
||||
type ${type}${payloadName} { ${pluralize.singular(name)}: ${
|
||||
model.globalId
|
||||
} }
|
||||
`;
|
||||
case 'delete':
|
||||
return `
|
||||
input ${type}${inputName} { where: InputID }
|
||||
type ${type}${payloadName} { ${pluralize.singular(name)}: ${
|
||||
model.globalId
|
||||
} }
|
||||
`;
|
||||
default:
|
||||
// Nothing
|
||||
}
|
||||
},
|
||||
};
|
||||
@ -1,10 +1,44 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = {
|
||||
mutation: `
|
||||
upload(refId: ID, ref: String, source: String, file: Upload!): UploadFile!
|
||||
`,
|
||||
resolver: {
|
||||
Query: {
|
||||
file: false,
|
||||
files: {
|
||||
resolver: 'Upload.find'
|
||||
}
|
||||
},
|
||||
Mutation: {
|
||||
createFile: false,
|
||||
updateFile: false,
|
||||
deleteFile: false,
|
||||
upload: {
|
||||
description: 'Upload one or many files',
|
||||
resolver: async (obj, { file, ...fields}, { context }) => {
|
||||
// Construct context to fit with koa-parser guidelines
|
||||
// and avoid to update our business logic too much.
|
||||
context.request.body = {
|
||||
files: {
|
||||
files: await file
|
||||
},
|
||||
fields
|
||||
};
|
||||
|
||||
// Call controller action.
|
||||
await strapi.plugins.upload.controllers.upload.upload(context);
|
||||
|
||||
// Handle case when the user is uploading only one file.
|
||||
if (_.isArray(context.body) && context.body.length === 1) {
|
||||
return context.body[0];
|
||||
}
|
||||
|
||||
// Return response.
|
||||
return context.body;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -7,20 +7,10 @@
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const crypto = require('crypto');
|
||||
const _ = require('lodash');
|
||||
const toArray = require('stream-to-array');
|
||||
const uuid = require('uuid/v4');
|
||||
|
||||
function niceHash(buffer) {
|
||||
return crypto.createHash('sha256')
|
||||
.update(buffer)
|
||||
.digest('base64')
|
||||
.replace(/=/g, '')
|
||||
.replace(/\//g, '-')
|
||||
.replace(/\+/, '_');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
bufferize: async files => {
|
||||
if (_.isEmpty(files) === 0) {
|
||||
@ -33,32 +23,47 @@ module.exports = {
|
||||
// transform all files in buffer
|
||||
return Promise.all(
|
||||
files.map(async stream => {
|
||||
const parts = await toArray(fs.createReadStream(stream.path));
|
||||
const parts = stream.path
|
||||
? await toArray(fs.createReadStream(stream.path))
|
||||
: await toArray(stream.stream);
|
||||
|
||||
const buffers = parts.map(
|
||||
part => _.isBuffer(part) ? part : Buffer.from(part)
|
||||
part => (_.isBuffer(part) ? part : Buffer.from(part)),
|
||||
);
|
||||
|
||||
const buffer = Buffer.concat(buffers);
|
||||
if (!stream.path) {
|
||||
stream.name = stream.filename;
|
||||
stream.type = stream.mimetype;
|
||||
stream.size = parseInt(stream.encoding.replace('bit', ''));
|
||||
}
|
||||
|
||||
return {
|
||||
name: stream.name,
|
||||
sha256: niceHash(buffer),
|
||||
hash: uuid().replace(/-/g, ''),
|
||||
ext: stream.name.split('.').length > 1 ? `.${_.last(stream.name.split('.'))}` : '',
|
||||
buffer,
|
||||
ext:
|
||||
stream.name.split('.').length > 1
|
||||
? `.${_.last(stream.name.split('.'))}`
|
||||
: '',
|
||||
buffer: Buffer.concat(buffers),
|
||||
mime: stream.type,
|
||||
size: (stream.size / 1000).toFixed(2)
|
||||
size: (stream.size / 1000).toFixed(2),
|
||||
};
|
||||
})
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
upload: async (files, config) => {
|
||||
// Get upload provider settings to configure the provider to use.
|
||||
const provider = _.find(strapi.plugins.upload.config.providers, { provider: config.provider });
|
||||
const provider = _.find(strapi.plugins.upload.config.providers, {
|
||||
provider: config.provider,
|
||||
});
|
||||
|
||||
if (!provider) {
|
||||
throw new Error(`The provider package isn't installed. Please run \`npm install strapi-upload-${config.provider}\``);
|
||||
throw new Error(
|
||||
`The provider package isn't installed. Please run \`npm install strapi-upload-${
|
||||
config.provider
|
||||
}\``,
|
||||
);
|
||||
}
|
||||
|
||||
const actions = provider.init(config);
|
||||
@ -74,16 +79,22 @@ module.exports = {
|
||||
file.provider = provider.provider;
|
||||
|
||||
return await strapi.plugins['upload'].services.upload.add(file);
|
||||
})
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
add: async (values) => {
|
||||
add: async values => {
|
||||
// Use Content Manager business logic to handle relation.
|
||||
if (strapi.plugins['content-manager']) {
|
||||
return await strapi.plugins['content-manager'].services['contentmanager'].add({
|
||||
model: 'file'
|
||||
}, values, 'upload');
|
||||
return await strapi.plugins['content-manager'].services[
|
||||
'contentmanager'
|
||||
].add(
|
||||
{
|
||||
model: 'file',
|
||||
},
|
||||
values,
|
||||
'upload',
|
||||
);
|
||||
}
|
||||
|
||||
return strapi.query('file', 'upload').create(values);
|
||||
@ -93,20 +104,26 @@ module.exports = {
|
||||
// Use Content Manager business logic to handle relation.
|
||||
if (strapi.plugins['content-manager']) {
|
||||
params.model = 'file';
|
||||
params.id = (params._id || params.id);
|
||||
params.id = params._id || params.id;
|
||||
|
||||
return await strapi.plugins['content-manager'].services['contentmanager'].edit(params, values, 'upload');
|
||||
return await strapi.plugins['content-manager'].services[
|
||||
'contentmanager'
|
||||
].edit(params, values, 'upload');
|
||||
}
|
||||
|
||||
return strapi.query('file', 'upload').update(_.assign(params, values));
|
||||
},
|
||||
|
||||
fetch: (params) => {
|
||||
return strapi.query('file', 'upload').findOne(_.pick(params, ['_id', 'id']));
|
||||
fetch: params => {
|
||||
return strapi
|
||||
.query('file', 'upload')
|
||||
.findOne(_.pick(params, ['_id', 'id']));
|
||||
},
|
||||
|
||||
fetchAll: (params) => {
|
||||
return strapi.query('file', 'upload').find(strapi.utils.models.convertParams('file', params));
|
||||
fetchAll: params => {
|
||||
return strapi
|
||||
.query('file', 'upload')
|
||||
.find(strapi.utils.models.convertParams('file', params));
|
||||
},
|
||||
|
||||
count: async () => {
|
||||
@ -114,12 +131,16 @@ module.exports = {
|
||||
},
|
||||
|
||||
remove: async (params, config) => {
|
||||
params.id = (params._id || params.id);
|
||||
params.id = params._id || params.id;
|
||||
|
||||
const file = await strapi.plugins['upload'].services.upload.fetch(params);
|
||||
|
||||
// get upload provider settings to configure the provider to use
|
||||
const provider = _.cloneDeep(_.find(strapi.plugins.upload.config.providers, {provider: config.provider}));
|
||||
const provider = _.cloneDeep(
|
||||
_.find(strapi.plugins.upload.config.providers, {
|
||||
provider: config.provider,
|
||||
}),
|
||||
);
|
||||
_.assign(provider, config);
|
||||
const actions = provider.init(config);
|
||||
|
||||
@ -132,48 +153,55 @@ module.exports = {
|
||||
if (strapi.plugins['content-manager']) {
|
||||
params.model = 'file';
|
||||
|
||||
await strapi.plugins['content-manager'].services['contentmanager'].delete(params, {source: 'upload'});
|
||||
await strapi.plugins['content-manager'].services['contentmanager'].delete(
|
||||
params,
|
||||
{ source: 'upload' },
|
||||
);
|
||||
}
|
||||
|
||||
return strapi.query('file', 'upload').delete(params);
|
||||
},
|
||||
|
||||
uploadToEntity: async function (params, files, source) {
|
||||
uploadToEntity: async function(params, files, source) {
|
||||
// Retrieve provider settings from database.
|
||||
const config = await strapi.store({
|
||||
environment: strapi.config.environment,
|
||||
type: 'plugin',
|
||||
name: 'upload'
|
||||
}).get({ key: 'provider' });
|
||||
const config = await strapi
|
||||
.store({
|
||||
environment: strapi.config.environment,
|
||||
type: 'plugin',
|
||||
name: 'upload',
|
||||
})
|
||||
.get({ key: 'provider' });
|
||||
|
||||
const model = source && source !== 'content-manager' ?
|
||||
strapi.plugins[source].models[params.model]:
|
||||
strapi.models[params.model];
|
||||
const model =
|
||||
source && source !== 'content-manager'
|
||||
? strapi.plugins[source].models[params.model]
|
||||
: strapi.models[params.model];
|
||||
|
||||
// Asynchronous upload.
|
||||
await Promise.all(
|
||||
Object.keys(files)
|
||||
.map(async attribute => {
|
||||
// Bufferize files per attribute.
|
||||
const buffers = await this.bufferize(files[attribute]);
|
||||
const enhancedFiles = buffers.map(file => {
|
||||
const details = model.attributes[attribute];
|
||||
Object.keys(files).map(async attribute => {
|
||||
// Bufferize files per attribute.
|
||||
const buffers = await this.bufferize(files[attribute]);
|
||||
const enhancedFiles = buffers.map(file => {
|
||||
const details = model.attributes[attribute];
|
||||
|
||||
// Add related information to be able to make
|
||||
// the relationships later.
|
||||
file[details.via] = [{
|
||||
// Add related information to be able to make
|
||||
// the relationships later.
|
||||
file[details.via] = [
|
||||
{
|
||||
refId: params.id,
|
||||
ref: params.model,
|
||||
source,
|
||||
field: attribute,
|
||||
}];
|
||||
},
|
||||
];
|
||||
|
||||
return file;
|
||||
});
|
||||
return file;
|
||||
});
|
||||
|
||||
// Make upload async.
|
||||
return this.upload(enhancedFiles, config);
|
||||
})
|
||||
// Make upload async.
|
||||
return this.upload(enhancedFiles, config);
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@ -21,6 +21,35 @@ module.exports = {
|
||||
return ctx.body.roles;
|
||||
}
|
||||
}
|
||||
},
|
||||
Mutation: {
|
||||
createRole: {
|
||||
description: 'Create a new role',
|
||||
resolverOf: 'UsersPermissions.createRole',
|
||||
resolver: async (obj, options, ctx) => {
|
||||
await strapi.plugins['users-permissions'].controllers.userspermissions.createRole(ctx);
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
},
|
||||
updateRole: {
|
||||
description: 'Update an existing role',
|
||||
resolverOf: 'UsersPermissions.updateRole',
|
||||
resolver: async (obj, options, ctx) => {
|
||||
await strapi.plugins['users-permissions'].controllers.userspermissions.updateRole(ctx);
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
},
|
||||
deleteRole: {
|
||||
description: 'Delete an existing role',
|
||||
resolverOf: 'UsersPermissions.deleteRole',
|
||||
resolver: async (obj, options, ctx) => {
|
||||
await strapi.plugins['users-permissions'].controllers.userspermissions.deleteRole(ctx);
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -70,7 +70,6 @@ class Strapi extends EventEmitter {
|
||||
port: process.env.PORT || 1337,
|
||||
environment: toLower(process.env.NODE_ENV) || 'development',
|
||||
environments: {},
|
||||
admin: {},
|
||||
paths: {
|
||||
admin: 'admin',
|
||||
api: 'api',
|
||||
@ -111,21 +110,27 @@ class Strapi extends EventEmitter {
|
||||
// Update source admin.
|
||||
await admin.call(this);
|
||||
// Launch server.
|
||||
this.server.listen(this.config.port, async (err) => {
|
||||
this.server.listen(this.config.port, err => {
|
||||
if (err) {
|
||||
this.log.debug(`⚠️ Server wasn't able to start properly.`);
|
||||
this.log.debug('⚠️ Server wasn\'t able to start properly.');
|
||||
this.log.error(err);
|
||||
return this.stop();
|
||||
}
|
||||
|
||||
this.log.info('Time: ' + new Date());
|
||||
this.log.info('Launched in: ' + (Date.now() - this.config.launchedAt) + ' ms');
|
||||
this.log.info(
|
||||
'Launched in: ' + (Date.now() - this.config.launchedAt) + ' ms',
|
||||
);
|
||||
this.log.info('Environment: ' + this.config.environment);
|
||||
this.log.info('Process PID: ' + process.pid);
|
||||
this.log.info(`Version: ${this.config.info.strapi} (node v${this.config.info.node})`);
|
||||
this.log.info(
|
||||
`Version: ${this.config.info.strapi} (node v${
|
||||
this.config.info.node
|
||||
})`,
|
||||
);
|
||||
this.log.info('To shut down your server, press <CTRL> + C at any time');
|
||||
console.log();
|
||||
this.log.info(`☄️ Admin panel: ${this.config.admin.url}`);
|
||||
this.log.info(`☄️ Admin panel: ${this.config.url}/admin`);
|
||||
this.log.info(`⚡️ Server: ${this.config.url}`);
|
||||
console.log();
|
||||
|
||||
@ -135,15 +140,11 @@ class Strapi extends EventEmitter {
|
||||
if (cb && typeof cb === 'function') {
|
||||
cb();
|
||||
}
|
||||
|
||||
if (this.config.environment === 'development' && get(this.config.currentEnvironment, 'server.admin.autoOpen', true) !== false) {
|
||||
await utils.openBrowser.call(this);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
this.log.debug(`⛔️ Server wasn't able to start properly.`);
|
||||
this.log.debug('⛔️ Server wasn\'t able to start properly.');
|
||||
this.log.error(err);
|
||||
console.log(err);
|
||||
console.error(err);
|
||||
this.stop();
|
||||
}
|
||||
}
|
||||
@ -162,8 +163,10 @@ class Strapi extends EventEmitter {
|
||||
|
||||
this.server.on('error', err => {
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
this.log.debug(`⛔️ Server wasn't able to start properly.`);
|
||||
this.log.error(`The port ${err.port} is already used by another application.`);
|
||||
this.log.debug('⛔️ Server wasn\'t able to start properly.');
|
||||
this.log.error(
|
||||
`The port ${err.port} is already used by another application.`,
|
||||
);
|
||||
this.stop();
|
||||
return;
|
||||
}
|
||||
@ -187,7 +190,8 @@ class Strapi extends EventEmitter {
|
||||
if (
|
||||
cluster.isWorker &&
|
||||
this.config.environment === 'development' &&
|
||||
get(this.config, 'currentEnvironment.server.autoReload.enabled', true) === true
|
||||
get(this.config, 'currentEnvironment.server.autoReload.enabled', true) ===
|
||||
true
|
||||
) {
|
||||
process.send('stop');
|
||||
}
|
||||
@ -226,7 +230,10 @@ class Strapi extends EventEmitter {
|
||||
await store.call(this);
|
||||
|
||||
// Initialize hooks and middlewares.
|
||||
await Promise.all([initializeMiddlewares.call(this), initializeHooks.call(this)]);
|
||||
await Promise.all([
|
||||
initializeMiddlewares.call(this),
|
||||
initializeHooks.call(this),
|
||||
]);
|
||||
|
||||
// Harmonize plugins configuration.
|
||||
await plugins.call(this);
|
||||
@ -247,7 +254,11 @@ class Strapi extends EventEmitter {
|
||||
if (
|
||||
cluster.isWorker &&
|
||||
this.config.environment === 'development' &&
|
||||
get(this.config, 'currentEnvironment.server.autoReload.enabled', true) === true
|
||||
get(
|
||||
this.config,
|
||||
'currentEnvironment.server.autoReload.enabled',
|
||||
true,
|
||||
) === true
|
||||
) {
|
||||
process.send('reload');
|
||||
}
|
||||
@ -280,8 +291,8 @@ class Strapi extends EventEmitter {
|
||||
const timeoutMs = this.config.bootstrapTimeout || 3500;
|
||||
const timer = setTimeout(() => {
|
||||
this.log.warn(
|
||||
`Bootstrap is taking unusually long to execute its callback ${timeoutMs} miliseconds).`,
|
||||
);
|
||||
`Bootstrap is taking unusually long to execute its callback ${timeoutMs} miliseconds).`,
|
||||
);
|
||||
this.log.warn('Perhaps you forgot to call it?');
|
||||
}, timeoutMs);
|
||||
|
||||
@ -290,7 +301,9 @@ class Strapi extends EventEmitter {
|
||||
try {
|
||||
fn(err => {
|
||||
if (ranBootstrapFn) {
|
||||
this.log.error('You called the callback in `strapi.config.boostrap` more than once!');
|
||||
this.log.error(
|
||||
'You called the callback in `strapi.config.boostrap` more than once!',
|
||||
);
|
||||
|
||||
return reject();
|
||||
}
|
||||
@ -302,7 +315,9 @@ class Strapi extends EventEmitter {
|
||||
});
|
||||
} catch (e) {
|
||||
if (ranBootstrapFn) {
|
||||
this.log.error('The bootstrap function threw an error after its callback was called.');
|
||||
this.log.error(
|
||||
'The bootstrap function threw an error after its callback was called.',
|
||||
);
|
||||
|
||||
return reject(e);
|
||||
}
|
||||
@ -315,7 +330,9 @@ class Strapi extends EventEmitter {
|
||||
});
|
||||
|
||||
return Promise.all(
|
||||
Object.values(this.plugins).map(x => execBootstrap(get(x, 'config.functions.bootstrap'))),
|
||||
Object.values(this.plugins).map(x =>
|
||||
execBootstrap(get(x, 'config.functions.bootstrap')),
|
||||
),
|
||||
).then(() => execBootstrap(this.config.functions.bootstrap));
|
||||
}
|
||||
|
||||
@ -335,14 +352,16 @@ class Strapi extends EventEmitter {
|
||||
query(entity, plugin) {
|
||||
if (!entity) {
|
||||
return this.log.error(
|
||||
`You can't call the query method without passing the model's name as a first argument.`,
|
||||
'You can\'t call the query method without passing the model\'s name as a first argument.',
|
||||
);
|
||||
}
|
||||
|
||||
const model = entity.toLowerCase();
|
||||
|
||||
const Model =
|
||||
get(strapi.plugins, [plugin, 'models', model]) || get(strapi, ['models', model]) || undefined;
|
||||
get(strapi.plugins, [plugin, 'models', model]) ||
|
||||
get(strapi, ['models', model]) ||
|
||||
undefined;
|
||||
|
||||
if (!Model) {
|
||||
return this.log.error(`The model ${model} can't be found.`);
|
||||
@ -351,7 +370,9 @@ class Strapi extends EventEmitter {
|
||||
const connector = Model.orm;
|
||||
|
||||
if (!connector) {
|
||||
return this.log.error(`Impossible to determine the use ORM for the model ${model}.`);
|
||||
return this.log.error(
|
||||
`Impossible to determine the use ORM for the model ${model}.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Get stack trace.
|
||||
@ -363,7 +384,9 @@ class Strapi extends EventEmitter {
|
||||
let pluginPath = undefined;
|
||||
|
||||
if (file.indexOf('strapi-plugin-') !== -1) {
|
||||
pluginPath = file.split(path.sep).filter(x => x.indexOf('strapi-plugin-') !== -1)[0];
|
||||
pluginPath = file
|
||||
.split(path.sep)
|
||||
.filter(x => x.indexOf('strapi-plugin-') !== -1)[0];
|
||||
} else if (file.indexOf(path.sep + 'plugins' + path.sep) !== -1) {
|
||||
const pathTerms = file.split(path.sep);
|
||||
const index = pathTerms.indexOf('plugins');
|
||||
@ -374,15 +397,22 @@ class Strapi extends EventEmitter {
|
||||
}
|
||||
|
||||
if (!pluginPath) {
|
||||
return this.log.error('Impossible to find the plugin where `strapi.query` has been called.');
|
||||
return this.log.error(
|
||||
'Impossible to find the plugin where `strapi.query` has been called.',
|
||||
);
|
||||
}
|
||||
|
||||
// Get plugin name.
|
||||
const pluginName = pluginPath.replace('strapi-plugin-', '').toLowerCase();
|
||||
const queries = get(this.plugins, `${pluginName}.config.queries.${connector}`);
|
||||
const queries = get(
|
||||
this.plugins,
|
||||
`${pluginName}.config.queries.${connector}`,
|
||||
);
|
||||
|
||||
if (!queries) {
|
||||
return this.log.error(`There is no query available for the model ${model}.`);
|
||||
return this.log.error(
|
||||
`There is no query available for the model ${model}.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Bind queries with the current model to allow the use of `this`.
|
||||
@ -393,7 +423,7 @@ class Strapi extends EventEmitter {
|
||||
{
|
||||
orm: connector,
|
||||
primaryKey: Model.primaryKey,
|
||||
associations: Model.associations
|
||||
associations: Model.associations,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user