2016-03-16 14:41:15 +01:00

665 lines
20 KiB
JavaScript

'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')) : [];
}