mirror of
https://github.com/strapi/strapi.git
synced 2025-10-07 06:11:04 +00:00
Move GraphQL and Models logics to specific hook
This commit is contained in:
parent
40b196f88e
commit
bbee9996e5
@ -27,5 +27,7 @@ module.exports = {
|
||||
router: true,
|
||||
static: true,
|
||||
websockets: true,
|
||||
jsonapi: true
|
||||
jsonapi: true,
|
||||
models: true,
|
||||
graphql: true
|
||||
};
|
||||
|
67
lib/configuration/hooks/graphql/index.js
Normal file
67
lib/configuration/hooks/graphql/index.js
Normal file
@ -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;
|
||||
};
|
47
lib/configuration/hooks/graphql/scalars/json.js
Normal file
47
lib/configuration/hooks/graphql/scalars/json.js
Normal file
@ -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;
|
664
lib/configuration/hooks/graphql/schema.js
Normal file
664
lib/configuration/hooks/graphql/schema.js
Normal file
@ -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')) : [];
|
||||
}
|
99
lib/configuration/hooks/graphql/utils/index.js
Normal file
99
lib/configuration/hooks/graphql/utils/index.js
Normal file
@ -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();
|
||||
}
|
||||
};
|
26
lib/configuration/hooks/models/index.js
Normal file
26
lib/configuration/hooks/models/index.js
Normal file
@ -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;
|
||||
};
|
159
lib/configuration/hooks/models/utils/index.js
Normal file
159
lib/configuration/hooks/models/utils/index.js
Normal file
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
@ -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());
|
||||
|
@ -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) {
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user