diff --git a/lib/configuration/hooks/defaultHooks.js b/lib/configuration/hooks/defaultHooks.js index 1f7c58f1bf..5e9df6daac 100755 --- a/lib/configuration/hooks/defaultHooks.js +++ b/lib/configuration/hooks/defaultHooks.js @@ -27,5 +27,7 @@ module.exports = { router: true, static: true, websockets: true, - jsonapi: true + jsonapi: true, + models: true, + graphql: true }; diff --git a/lib/configuration/hooks/graphql/index.js b/lib/configuration/hooks/graphql/index.js new file mode 100644 index 0000000000..7df17d8b51 --- /dev/null +++ b/lib/configuration/hooks/graphql/index.js @@ -0,0 +1,67 @@ +'use strict'; + +/** + * GraphQL hook + */ + +// Public dependencies +const _ = require('lodash'); + +module.exports = function (strapi) { + const hook = { + + /** + * Default options + */ + + defaults: { + graphql: { + enabled: false, + route: '/graphql', + graphiql: false, + pretty: true, + usefulQueries: true, + ignoreMutations: true + } + }, + + /** + * Initialize the hook + */ + + initialize: function (cb) { + const self = this; + + // Override default configuration for GraphQL + _.assign(this.defaults.graphql, strapi.config.graphql); + + // Define GraphQL route to GraphQL schema + if (this.defaults.graphql.enabled === true) { + require('./schema').getGraphQLSchema(_.assign({ + collections: strapi.bookshelf.collections + }, strapi.config.graphql), function (schemas) { + // Mount GraphQL server + strapi.app.use(strapi.middlewares.mount(self.defaults.graphql.route, strapi.middlewares.graphql((request, context) => ({ + schema: schemas, + pretty: self.defaults.graphql.pretty, + rootValue: { + context: context + }, + graphiql: self.defaults.graphql.graphiql + })))); + + // Expose the GraphQL schemas at `strapi.schemas` + strapi.schemas = schemas; + + cb(); + }); + } else { + global.graphql = undefined; + + cb(); + } + } + }; + + return hook; +}; diff --git a/lib/configuration/hooks/graphql/scalars/json.js b/lib/configuration/hooks/graphql/scalars/json.js new file mode 100644 index 0000000000..7fe2316c9f --- /dev/null +++ b/lib/configuration/hooks/graphql/scalars/json.js @@ -0,0 +1,47 @@ +const GraphQL = require('graphql'); +const GraphQLLanguage = require('graphql/language'); +const Kind = GraphQLLanguage.Kind; + +const GraphQLJson = new GraphQL.GraphQLScalarType({ + name: 'JSON', + description: 'The `JSON` scalar type to support raw JSON values.', + serialize: value => value, + parseValue: value => value, + parseLiteral: tree => { + const parser = getParser[tree.kind]; + return parser.call(this, tree); + } +}); + +function getParser(kind) { + switch (kind) { + case Kind.INT: + return tree => GraphQL.GraphQLInt.parseLiteral(tree); + + case Kind.FLOAT: + return tree => GraphQL.GraphQLFloat.parseLiteral(tree); + + case Kind.BOOLEAN: + return tree => GraphQL.GraphQLBoolean.parseLiteral(tree); + + case Kind.STRING: + return tree => GraphQL.GraphQLString.parseLiteral(tree); + + case Kind.ENUM: + return tree => String(tree.value); + + case Kind.LIST: + return tree => tree.values.map(node => GraphQLJson.parseLiteral(node)); + + case Kind.OBJECT: + return tree => tree.fields.reduce((fields, field) => { + fields[field.name.value] = GraphQLJson.parseLiteral(field.value); + return fields; + }, {}); + + default: + return null; + } +} + +module.exports = GraphQLJson; diff --git a/lib/configuration/hooks/graphql/schema.js b/lib/configuration/hooks/graphql/schema.js new file mode 100644 index 0000000000..9f5a61f8fa --- /dev/null +++ b/lib/configuration/hooks/graphql/schema.js @@ -0,0 +1,664 @@ +'use strict'; + +// Public dependencies +const _ = require('lodash'); +const GraphQL = require('graphql'); +const GraphQLJson = require('./scalars/json'); +const utils = require('./utils/'); + +// Core dependencies +const fs = require('fs-extra'); +const path = require('path'); + +module.exports = { + + /* + * Defaults parameters object + */ + + defaults: { + collections: {}, + usefulQueries: true + }, + + /* + * Starter to manage conversion process and build valid GraphQL schemas + */ + + getGraphQLSchema: function (params, cb) { + if (_.isEmpty(params.collections)) { + return 'Error: Empty object collections'; + } + + // Set defaults properties + this.defaults = _.assign(this.defaults, params); + + const Query = this.getQueries(); + const Mutation = _.get(this.defaults, 'ignoreMutations') === true ? null : this.getMutations(); + + const Schema = new GraphQL.GraphQLSchema(_.omit({ + query: Query, + mutation: Mutation + }, _.isNull)); + + // Return schema + cb(Schema); + + // Build policies + this.buildPolicies(); + }, + + /* + * Build policies files + */ + + buildPolicies: function () { + const self = this; + + _.forEach(this.defaults.collections, function (collection, rootKey) { + // Identify queries related to this collection + const queries = _.pick(self.defaults.queryFields, function (query, key) { + if (key.indexOf(rootKey) !== -1 || key.indexOf(_.capitalize(rootKey)) !== -1) { + return true; + } + }); + + // Identify mutations related to this collection + const mutations = _.pick(self.defaults.mutations, function (query, key) { + if (key.indexOf(rootKey) !== -1 || key.indexOf(_.capitalize(rootKey)) !== -1) { + return true; + } + }); + + // Initialize query and mutations to empty array + const value = { + queries: _.mapValues(queries, function () { + return []; + }), + mutations: _.mapValues(mutations, function () { + return []; + }) + }; + + fs.readJson(path.join(strapi.config.appPath, 'api', rootKey, 'config', 'graphql.json'), function (err, rootValue) { + // File doesn't exist + if (err && err.code === 'ENOENT' && err.syscall === 'open') { + rootValue = {}; + } else if (err) { + console.log(err); + + return; + } + + // Override or write file + fs.writeJson(path.join(strapi.config.appPath, 'api', rootKey, 'config', 'graphql.json'), _.merge(value, rootValue), function (err) { + if (err) { + console.log(err); + } + }); + }); + }); + }, + + /* + * Manager to create queries for each collection + */ + + getQueries: function () { + const self = this; + + // Create required keys + this.defaults.types = {}; + this.defaults.queryFields = {}; + + // Build Node Interface to expand compatibility + this.buildNodeInterface(); + + // Build GraphQL type system objects + _.forEach(this.defaults.collections, function (collection, key) { + self.buildType(collection, key); + }); + + // Build GraphQL query + _.forEach(this.defaults.collections, function (collection, key) { + self.buildQueryFields(collection, key); + }); + + // Build GraphQL query object + return new GraphQL.GraphQLObjectType({ + name: 'Queries', + description: 'Root of the Schema', + fields: function () { + return self.defaults.queryFields; + } + }); + }, + + /* + * Manager to create mutations for each collection + */ + getMutations: function () { + const self = this; + + // Create require key + this.defaults.mutations = {}; + + // Build GraphQL mutation + _.forEach(this.defaults.collections, function (collection, key) { + self.buildMutation(collection, key); + }); + + // Build GraphQL mutation object + return new GraphQL.GraphQLObjectType({ + name: 'Mutations', + description: 'Mutations of the Schema', + fields: function () { + return self.defaults.mutations; + } + }); + }, + + /* + * Create GraphQL type system from BookShelf collection + */ + + buildType: function (collection) { + const self = this; + const collectionIdentity = _.capitalize(collection.forge().tableName); + const collectionAttributes = collection._attributes; + + const Type = new GraphQL.GraphQLObjectType({ + name: _.capitalize(collectionIdentity), + description: 'This represents a/an ' + _.capitalize(collectionIdentity), + interfaces: [self.defaults.node], + fields: function () { + const fields = {}; + + _.forEach(collectionAttributes, function (rules, key) { + if (rules.hasOwnProperty('model')) { + fields[key] = { + type: self.defaults.types[_.capitalize(rules.model)], + resolve: function (object) { + const criteria = {}; + criteria[collection.primaryKey] = object[key][self.defaults.collections[rules.model].primaryKey]; + + return self.defaults.queryFields[rules.model.toLowerCase()].resolve(object, criteria); + } + }; + } else if (rules.hasOwnProperty('collection')) { + fields[key] = { + type: new GraphQL.GraphQLList(self.defaults.types[_.capitalize(rules.collection)]), + resolve: function (object) { + const criteria = {}; + criteria[rules.via.toLowerCase()] = object[collection.primaryKey]; + + return self.defaults.queryFields[rules.collection.toLowerCase() + 's'].resolve(object, {}, { + where: criteria + }); + } + }; + } else { + fields[key] = { + type: rules.required ? new GraphQL.GraphQLNonNull(convertToGraphQLQueryType(rules)) : convertToGraphQLQueryType(rules) + }; + } + }); + + // Handle interface + fields.id = { + type: new GraphQL.GraphQLNonNull(GraphQL.GraphQLString) + }; + + fields.type = { + type: new GraphQL.GraphQLNonNull(GraphQL.GraphQLString) + }; + + return fields; + } + }); + + // Save to global parameters + this.defaults.types[collectionIdentity] = Type; + }, + + /* + * Create query framework for each collection + */ + + buildQueryFields: function (collection) { + const collectionIdentity = _.capitalize(collection.forge().tableName); + const fields = {}; + + // Get single record + fields[collectionIdentity.toLowerCase()] = { + type: this.defaults.types[collectionIdentity], + args: { + id: { + name: 'id', + type: new GraphQL.GraphQLNonNull(GraphQL.GraphQLString) + } + }, + resolve: function (rootValue, criteria) { + return utils.applyPolicies(rootValue, 'queries', collectionIdentity, collectionIdentity) + .then(function () { + return collection.forge(criteria) + .fetch({withRelated: getAssociationsByIdentity(collectionIdentity)}); + }) + .then(function (data) { + return _.isEmpty(data) ? data : data.toJSON(); + }) + .catch(function () { + return null; + }); + } + }; + + // Get multiples records + fields[collectionIdentity.toLowerCase() + 's'] = { + type: new GraphQL.GraphQLList(this.defaults.types[collectionIdentity]), + args: { + limit: { + name: 'limit', + type: GraphQL.GraphQLInt + }, + skip: { + name: 'skip', + type: GraphQL.GraphQLInt + }, + sort: { + name: 'sort', + type: GraphQL.GraphQLString + } + }, + resolve: function (rootValue, criteria) { + return utils.applyPolicies(rootValue, 'queries', collectionIdentity, collectionIdentity + 's') + .then(function () { + const filters = _.omit(handleFilters(criteria), function (value) { + return _.isUndefined(value) || _.isNumber(value) ? _.isNull(value) : _.isEmpty(value); + }); + + return collection.forge() + .query(filters) + .fetchAll({withRelated: getAssociationsByIdentity(collectionIdentity)}); + }) + .then(function (data) { + return data.toJSON() || data; + }) + .catch(function () { + return null; + }); + } + }; + + if (this.defaults.usefulQueries === true) { + // Get latest records sorted by creation date + fields['getLatest' + collectionIdentity + 's'] = { + type: new GraphQL.GraphQLList(this.defaults.types[collectionIdentity]), + args: { + count: { + name: 'count', + type: new GraphQL.GraphQLNonNull(GraphQL.GraphQLInt) + } + }, + resolve: function (rootValue, criteria) { + return utils.applyPolicies(rootValue, 'queries', collectionIdentity, 'getLatest' + collectionIdentity + 's') + .then(function () { + const filters = _.omit(handleFilters(criteria), function (value) { + return _.isUndefined(value) || _.isNumber(value) ? _.isNull(value) : _.isEmpty(value); + }); + + // Handle filters + filters.orderBy = 'createdAt DESC'; + filters.limit = filters.count; + + delete filters.count; + + return collection.forge(criteria) + .query(filters) + .fetchAll({withRelated: getAssociationsByIdentity(collectionIdentity)}); + }) + .then(function (data) { + return data.toJSON() || data; + }) + .catch(function () { + return null; + }); + } + }; + + // Get first records sorted by creation date + fields['getFirst' + collectionIdentity + 's'] = { + type: new GraphQL.GraphQLList(this.defaults.types[collectionIdentity]), + args: { + count: { + name: 'count', + type: new GraphQL.GraphQLNonNull(GraphQL.GraphQLInt) + } + }, + resolve: function (rootValue, criteria) { + return utils.applyPolicies(rootValue, 'queries', collectionIdentity, 'getFirst' + collectionIdentity + 's') + .then(function () { + const filters = _.omit(handleFilters(criteria), function (value) { + return _.isUndefined(value) || _.isNumber(value) ? _.isNull(value) : _.isEmpty(value); + }); + + // Handle filters + filters.orderBy = 'createdAt ASC'; + filters.limit = filters.count; + + delete filters.count; + + return collection.forge(criteria) + .query(filters) + .fetchAll({withRelated: getAssociationsByIdentity(collectionIdentity)}); + }) + .then(function (data) { + return data.toJSON() || data; + }) + .catch(function () { + return null; + }); + } + }; + + // Get count of records + fields['count' + collectionIdentity + 's'] = { + type: GraphQL.GraphQLInt, + resolve: function (rootValue) { + return utils.applyPolicies(rootValue, 'queries', collectionIdentity, 'count' + collectionIdentity + 's') + .then(function () { + return collection.forge().count(); + }) + .then(function (data) { + return data.toJSON() || data; + }) + .catch(function () { + return null; + }); + + } + }; + } + + // Apply date filters to each query + _.forEach(_.omit(fields, collectionIdentity.toLowerCase()), function (field) { + if (_.isEmpty(field.args)) { + field.args = {}; + } + + field.args.start = { + name: 'start', + type: GraphQL.GraphQLString + }; + + field.args.end = { + name: 'end', + type: GraphQL.GraphQLString + }; + }); + + _.assign(this.defaults.queryFields, fields); + }, + + /* + * Create functions to do the same as an API + */ + + buildMutation: function (collection) { + const self = this; + const collectionIdentity = _.capitalize(collection.forge().tableName); + const collectionAttributes = collection._attributes; + const PK = utils.getPK(collection); + const fields = {}; + const args = { + required: {}, + notRequired: {} + }; + + // Build args + _.forEach(collectionAttributes, function (rules, key) { + // Exclude relations + if (!rules.hasOwnProperty('model') && !rules.hasOwnProperty('collection') && rules.required) { + args.required[key] = { + type: rules.required ? new GraphQL.GraphQLNonNull(convertToGraphQLQueryType(rules, self)) : convertToGraphQLQueryType(rules, self) + }; + } else if (!rules.hasOwnProperty('model') && !rules.hasOwnProperty('collection') && !rules.required) { + args.notRequired[key] = { + type: convertToGraphQLQueryType(rules, self) + }; + } else if (rules.required) { + args.required[key] = { + type: rules.required ? new GraphQL.GraphQLNonNull(convertToGraphQLRelationType(rules, self)) : convertToGraphQLRelationType(rules, self) + }; + } else { + args.notRequired[key] = { + type: rules.required ? new GraphQL.GraphQLNonNull(convertToGraphQLRelationType(rules, self)) : convertToGraphQLRelationType(rules, self) + }; + } + }); + + // Create record + fields['create' + collectionIdentity] = { + type: this.defaults.types[collectionIdentity], + resolve: function (rootValue, args) { + return utils.applyPolicies(rootValue, 'mutations', collectionIdentity, 'create' + collectionIdentity) + .then(function () { + _.merge(args, rootValue.context.request.body); + + return strapi.services[collectionIdentity.toLowerCase()].add(rootValue.context.request.body); + }) + .then(function (data) { + return _.isFunction(_.get(data, 'toJSON')) ? data.toJSON() : data; + }) + .catch(function () { + return null; + }); + } + }; + + // Set primary key as required for update/delete mutation + const argPK = _.set({}, PK, { + type: new GraphQL.GraphQLNonNull(convertToGraphQLQueryType(PK)) + }); + + // Update record(s) + fields['update' + collectionIdentity] = { + type: this.defaults.types[collectionIdentity], + args: _.assign(args.required, argPK), + resolve: function (rootValue, args) { + return utils.applyPolicies(rootValue, 'mutations', collectionIdentity, 'update' + collectionIdentity) + .then(function () { + _.merge(args, rootValue.context.request.body); + + return strapi.services[collectionIdentity.toLowerCase()].edit(_.set({}, PK, args[PK]), _.omit(args, PK)); + }) + .then(function (data) { + return _.isFunction(_.get(data, 'toJSON')) ? data.toJSON() : data; + }) + .catch(function () { + return null; + }); + } + }; + + // Delete record(s) + fields['delete' + collectionIdentity] = { + type: this.defaults.types[collectionIdentity], + args: _.assign(args.notRequired, argPK), + resolve: function (rootValue, args) { + return utils.applyPolicies(rootValue, 'mutations', collectionIdentity, 'delete' + collectionIdentity) + .then(function () { + _.merge(args, rootValue.context.request.body); + + return strapi.services[collectionIdentity.toLowerCase()].remove(args); + }) + .then(function (data) { + return _.isFunction(_.get(data, 'toJSON')) ? data.toJSON() : data; + }) + .catch(function () { + return null; + }); + } + }; + + _.assign(this.defaults.mutations, fields); + }, + + /* + * Build node interface + */ + + buildNodeInterface: function () { + const self = this; + + this.defaults.node = new GraphQL.GraphQLInterfaceType({ + name: 'Node', + description: 'An object with an ID', + fields: function fields() { + return { + id: { + type: new GraphQL.GraphQLNonNull(GraphQL.GraphQLString), + description: 'The global unique ID of an object' + }, + type: { + type: new GraphQL.GraphQLNonNull(GraphQL.GraphQLString), + description: 'The type of the object' + } + }; + }, + resolveType: function resolveType(object) { + return object.type; + } + }); + + this.defaults.nodeFields = { + name: 'Node', + type: this.defaults.node, + description: 'A node interface field', + args: { + id: { + type: new GraphQL.GraphQLNonNull(GraphQL.GraphQLString), + description: 'Id of node interface' + } + }, + resolve: function resolve(object, criteria) { + const arrayOfPromises = []; + + // Search value in each collection + _.forEach(self.defaults.collections, function (collection) { + arrayOfPromises.push(collection.find(criteria)); + }); + + return Promise.all(arrayOfPromises) + .then(function (results) { + let typeIndex; + let object; + + _.forEach(results, function (value, index) { + if (_.size(value) === 1) { + // Save the index + typeIndex = index; + + // Get object from array + object = _.first(value); + + return false; + } + }); + + object.type = self.defaults.queryFields[_.keys(self.defaults.queryFields)[typeIndex]].type; + + return object; + }) + .catch(function (error) { + return error; + }); + } + }; + } +}; + +/* + * Helper: convert model type to GraphQL type system + */ + +function convertToGraphQLQueryType(rules, ctx) { + if (rules.hasOwnProperty('type')) { + switch (rules.type.toLowerCase()) { + case 'string': + return GraphQL.GraphQLString; + case 'integer': + return GraphQL.GraphQLInt; + case 'boolean': + return GraphQL.GraphQLBoolean; + case 'float': + return GraphQL.GraphQLFloat; + case 'json': + return GraphQLJson; + default: + return GraphQL.GraphQLString; + } + } else if (rules.hasOwnProperty('model')) { + return ctx.defaults.types[_.capitalize(rules.model)]; + } else if (rules.hasOwnProperty('collection')) { + return new GraphQL.GraphQLList(ctx.defaults.types[_.capitalize(rules.collection)]); + } else { + return GraphQL.GraphQLString; + } +} + +/* + * Helper: convert model type to GraphQL type system for input fields + */ + +function convertToGraphQLRelationType(rules, PK) { + if (rules.hasOwnProperty('model')) { + return convertToGraphQLQueryType(PK); + } else if (rules.hasOwnProperty('collection')) { + return new GraphQL.GraphQLList(convertToGraphQLQueryType(PK)); + } else { + return convertToGraphQLQueryType(rules); + } +} + +/* + * Helper: convert GraphQL argument to Bookshelf filters + */ + +function handleFilters(filters) { + if (!_.isEmpty(_.get(filters, 'start'))) { + // _.set(filters, 'where.start', new Date(filters.start).getTime()); + + delete filters.start; + } + + if (!_.isEmpty(_.get(filters, 'end'))) { + // _.set(filters, 'where.end', new Date(filters.end).getTime()); + + delete filters.end; + } + + if (_.isNumber(_.get(filters, 'skip'))) { + _.set(filters, 'offset', filters.skip); + + delete filters.skip; + } + + if (!_.isEmpty(_.get(filters, 'sort'))) { + _.set(filters, 'orderBy', filters.sort); + + delete filters.sort; + } + + return filters; +} + +/* + * Helper: get Strapi model name based on the collection identity + */ + +function getAssociationsByIdentity(collectionIdentity) { + const model = _.find(strapi.models, {tableName: collectionIdentity}); + + return !_.isUndefined(model) && model.hasOwnProperty('associations') ? _.keys(_.groupBy(model.associations, 'alias')) : []; +} diff --git a/lib/configuration/hooks/graphql/utils/index.js b/lib/configuration/hooks/graphql/utils/index.js new file mode 100644 index 0000000000..e6f071ce8b --- /dev/null +++ b/lib/configuration/hooks/graphql/utils/index.js @@ -0,0 +1,99 @@ +'use strict'; + +/** + * Module dependencies + */ + +// Public node modules. +const _ = require('lodash'); +const co = require('co'); + +/** + * GraphQL utils + */ + +module.exports = { + + /** + * Find primary key per ORM + */ + + getPK: function (collectionIdentity, collection, models) { + if (_.isString(collectionIdentity)) { + const ORM = this.getORM(collectionIdentity); + const GraphQLFunctions = require('strapi-' + ORM + '/lib/utils/'); + + if (!_.isUndefined(GraphQLFunctions)) { + return GraphQLFunctions.getPK(collectionIdentity, collection, models); + } + } + + return undefined; + }, + + /** + * Find primary key per ORM + */ + + getCount: function (collectionIdentity) { + if (_.isString(collectionIdentity)) { + const ORM = this.getORM(collectionIdentity); + const ORMFunctions = require('strapi-' + ORM + '/lib/utils/'); + + if (!_.isUndefined(ORMFunctions)) { + return ORMFunctions.getCount(collectionIdentity); + } + } + + return undefined; + }, + + /** + * Allow to resolve GraphQL function or not. + */ + + applyPolicies: function (rootValue, type, model, action) { + if (type.toLowerCase() === 'queries' || type.toLowerCase() === 'mutations') { + const policies = _.get(strapi.api, model.toLowerCase() + '.config.' + type.toLowerCase() + '.' + _.camelCase(action)); + + // Invalid model or action. + if (_.isUndefined(policies)) { + return Promise.reject(); + } else if (_.isEmpty(policies)) { + return Promise.resolve(); + } else if (_.size(_.intersection(_.keys(strapi.policies), policies)) !== _.size(policies)) { + // Some specified policies don't exist + return Promise.reject('Some specified policies don\'t exist'); + } + + // Wrap generator function into regular function. + const executePolicy = co.wrap(function * (policy) { + try { + let next; + + // Set next variable if `next` function has been called + yield strapi.policies[policy].apply(rootValue.context, [function * () { + next = true; + }]); + + if (_.isUndefined(next)) { + return yield Promise.reject(); + } + + return yield Promise.resolve(); + } catch (err) { + return yield Promise.reject(err); + } + }); + + // Build promises array. + const arrayOfPromises = _.map(policies, function (policy) { + return executePolicy(policy); + }); + + return Promise.all(arrayOfPromises); + } + + return Promise.reject(); + } +}; diff --git a/lib/configuration/hooks/models/index.js b/lib/configuration/hooks/models/index.js new file mode 100644 index 0000000000..b1f99b421a --- /dev/null +++ b/lib/configuration/hooks/models/index.js @@ -0,0 +1,26 @@ +'use strict'; + +/** + * Models hook + */ + +module.exports = function () { + const hook = { + + /** + * Default options + */ + + defaults: {}, + + /** + * Initialize the hook + */ + + initialize: function (cb) { + cb(); + } + }; + + return hook; +}; diff --git a/lib/configuration/hooks/models/utils/index.js b/lib/configuration/hooks/models/utils/index.js new file mode 100644 index 0000000000..ae822be563 --- /dev/null +++ b/lib/configuration/hooks/models/utils/index.js @@ -0,0 +1,159 @@ +'use strict'; + +/** + * Module dependencies + */ + +// Public node modules. +const _ = require('lodash'); + +/* + * Set of utils for models + */ + +module.exports = { + + /** + * Find relation nature with verbose + */ + + getNature: function (association, key, models) { + const strapi = _.isUndefined(global['strapi']) && !_.isUndefined(models) ? _.set({}, 'models', models) : global['strapi']; + const types = { + current: '', + other: '' + }; + + if (association.hasOwnProperty('via') && association.hasOwnProperty('collection')) { + const relatedAttribute = strapi.models[association.collection].attributes[association.via]; + + types.current = 'collection'; + + if (relatedAttribute.hasOwnProperty('collection')) { + types.other = 'collection'; + } else if (relatedAttribute.hasOwnProperty('model')) { + types.other = 'model'; + } + } else if (association.hasOwnProperty('via') && association.hasOwnProperty('model')) { + types.current = 'modelD'; + + // We have to find if they are a model linked to this key + _.forIn(strapi.models, function (model) { + _.forIn(model.attributes, function (attribute) { + if (attribute.hasOwnProperty('via') && attribute.via === key && attribute.hasOwnProperty('collection')) { + types.other = 'collection'; + + // Break loop + return false; + } else if (attribute.hasOwnProperty('model')) { + types.other = 'model'; + + // Break loop + return false; + } + }); + }); + } else if (association.hasOwnProperty('model')) { + types.current = 'model'; + + // We have to find if they are a model linked to this key + _.forIn(strapi.models, function (model) { + _.forIn(model.attributes, function (attribute) { + if (attribute.hasOwnProperty('via') && attribute.via === key) { + if (attribute.hasOwnProperty('collection')) { + types.other = 'collection'; + + // Break loop + return false; + } else if (attribute.hasOwnProperty('model')) { + types.other = 'modelD'; + + // Break loop + return false; + } + } + }); + }); + } + + if (types.current === 'modelD' && types.other === 'model') { + return { + nature: 'oneToOne', + verbose: 'belongsTo' + }; + } else if (types.current === 'model' && types.other === 'modelD') { + return { + nature: 'oneToOne', + verbose: 'hasOne' + }; + } else if (types.current === 'model' && types.other === 'collection') { + return { + nature: 'oneToMany', + verbose: 'belongsTo' + }; + } else if (types.current === 'collection' && types.other === 'model') { + return { + nature: 'manyToOne', + verbose: 'hasMany' + }; + } else if (types.current === 'collection' && types.other === 'collection') { + return { + nature: 'manyToMany', + verbose: 'belongsToMany' + }; + } else if (types.current === 'model' && types.other === '') { + return { + nature: 'oneWay', + verbose: 'belongsTo' + }; + } + + return undefined; + }, + + /** + * Return ORM used for this collection. + */ + + getORM: function (collectionIdentity) { + return _.get(strapi.models, collectionIdentity.toLowerCase() + '.orm'); + }, + + /** + * Define associations key to models + */ + + defineAssociations: function (model, definition, association, key) { + // Initialize associations object + definition.associations = []; + + // Exclude non-relational attribute + if (!association.hasOwnProperty('collection') && !association.hasOwnProperty('model')) { + return undefined; + } + + // Get relation nature + const infos = this.getNature(association, key); + + // Build associations object + if (association.hasOwnProperty('collection')) { + definition.associations.push({ + alias: key, + type: 'collection', + collection: association.collection, + via: association.via || undefined, + nature: infos.nature, + autoPopulate: _.get(association, 'autoPopulate') === true + }); + } else if (association.hasOwnProperty('model')) { + definition.associations.push({ + alias: key, + type: 'model', + model: association.model, + via: association.via || undefined, + nature: infos.nature, + autoPopulate: _.get(association, 'autoPopulate') === true + }); + } + } +}; diff --git a/lib/configuration/hooks/router/index.js b/lib/configuration/hooks/router/index.js index 1fa4dd7da0..a17268b634 100644 --- a/lib/configuration/hooks/router/index.js +++ b/lib/configuration/hooks/router/index.js @@ -28,13 +28,7 @@ module.exports = function (strapi) { defaults: { prefix: '', - routes: {}, - graphql: { - enabled: true, - route: '/graphql', - graphiql: false, - pretty: true - } + routes: {} }, /** @@ -117,24 +111,6 @@ module.exports = function (strapi) { } }); - // Override default configuration for GraphQL - _.assign(this.defaults.graphql, strapi.config.graphql); - - // Define GraphQL route to GraphQL schema - // or disable the global variable - if (this.defaults.graphql.enabled === true) { - strapi.app.use(strapi.middlewares.mount(this.defaults.graphql.route, strapi.middlewares.graphql((request, context) => ({ - schema: strapi.schemas, - pretty: this.defaults.graphql.pretty, - rootValue: { - context: context - }, - graphiql: this.defaults.graphql.graphiql - })))); - } else { - global.graphql = undefined; - } - // Let the router use our routes and allowed methods. strapi.app.use(strapi.router.routes()); strapi.app.use(strapi.router.allowedMethods()); diff --git a/lib/private/loadHooks.js b/lib/private/loadHooks.js index 5b9c2c5435..e95b2464f2 100755 --- a/lib/private/loadHooks.js +++ b/lib/private/loadHooks.js @@ -76,6 +76,7 @@ module.exports = function (strapi) { function loadHook(id, cb) { hooks[id].load(function (err) { if (err) { + console.log(err); strapi.log.error('The hook `' + id + '` failed to load!'); strapi.emit('hook:' + id + ':error'); return cb(err); @@ -122,7 +123,7 @@ module.exports = function (strapi) { // Prepare all other hooks. prepare: function prepareHooks(cb) { - async.each(_.without(_.keys(hooks), '_config', '_api', '_hooks', 'router'), function (id, cb) { + async.each(_.without(_.keys(hooks), '_config', '_api', '_hooks', 'router', 'graphql'), function (id, cb) { prepareHook(id); process.nextTick(cb); }, cb); @@ -130,7 +131,7 @@ module.exports = function (strapi) { // Apply the default config for all other hooks. defaults: function defaultConfigHooks(cb) { - async.each(_.without(_.keys(hooks), '_config', '_api', '_hooks', 'router'), function (id, cb) { + async.each(_.without(_.keys(hooks), '_config', '_api', '_hooks', 'router', 'graphql'), function (id, cb) { const hook = hooks[id]; applyDefaults(hook); process.nextTick(cb); @@ -139,11 +140,22 @@ module.exports = function (strapi) { // Load all other hooks. load: function loadOtherHooks(cb) { - async.each(_.without(_.keys(hooks), '_config', '_api', '_hooks', 'router'), function (id, cb) { + async.each(_.without(_.keys(hooks), '_config', '_api', '_hooks', 'router', 'graphql'), function (id, cb) { loadHook(id, cb); }, cb); }, + // Load the GraphQL hook. + graphql: function loadGraphQLHook(cb) { + if (!hooks.graphql) { + return cb(); + } + + prepareHook('graphql'); + applyDefaults(hooks.graphql); + loadHook('graphql', cb); + }, + // Load the router hook. router: function loadRouterHook(cb) { if (!hooks.router) { diff --git a/package.json b/package.json index a011701683..a9f86dce00 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "async": "~1.5.0", "consolidate": "~0.13.1", "grant-koa": "~3.5.3", + "graphql": "^0.4.18", "herd": "~1.0.0", "include-all": "~0.1.6", "jsonapi-serializer": "seyz/jsonapi-serializer",