Merge branch 'v2' of github.com:wistityhq/strapi into v2

This commit is contained in:
loicsaintroch 2016-01-20 11:01:35 +01:00
commit c45ea07a27
24 changed files with 58 additions and 1729 deletions

View File

@ -1,4 +1,10 @@
# Strapi [![Build Status](https://travis-ci.org/wistityhq/strapi.svg?branch=master)](https://travis-ci.org/wistityhq/strapi) [![Slack Status](http://strapi-slack.herokuapp.com/badge.svg)](http://slack.strapi.io) # Strapi
[![npm version](https://img.shields.io/npm/v/strapi.svg)](https://www.npmjs.org/package/strapi)
[![npm downloads](https://img.shields.io/npm/dm/strapi.svg)](https://www.npmjs.org/package/strapi)
[![npm dependencies](https://david-dm.org/wistityhq/strapi.svg)](https://david-dm.org/wistityhq/strapi)
[![Build status](https://travis-ci.org/wistityhq/strapi.svg?branch=master)](https://travis-ci.org/wistityhq/strapi)
[![Slack status](http://strapi-slack.herokuapp.com/badge.svg)](http://slack.strapi.io)
[Website](http://strapi.io/) - [Getting Started](#user-content-getting-started-in-a-minute) - [Documentation](http://strapi.io/documentation/introduction) - [Support](http://strapi.io/support) [Website](http://strapi.io/) - [Getting Started](#user-content-getting-started-in-a-minute) - [Documentation](http://strapi.io/documentation/introduction) - [Support](http://strapi.io/support)
@ -55,7 +61,6 @@ This will generate a Strapi application without:
- the built-in `user`, `email` and `upload` APIs, - the built-in `user`, `email` and `upload` APIs,
- the `grant` hook, - the `grant` hook,
- the open-source admin panel, - the open-source admin panel,
- the Waterline ORM (`waterline` and `blueprints` hooks disabled),
- the Strapi Studio connection (`studio` hook disabled). - the Strapi Studio connection (`studio` hook disabled).
This feature allows you to only use Strapi for your HTTP server structure if you want to. This feature allows you to only use Strapi for your HTTP server structure if you want to.

View File

@ -39,6 +39,10 @@ cmd = program.command('version');
cmd.description('output your version of Strapi'); cmd.description('output your version of Strapi');
cmd.action(program.versionInformation); cmd.action(program.versionInformation);
/**
* Basic commands
*/
// `$ strapi new <name>` // `$ strapi new <name>`
cmd = program.command('new'); cmd = program.command('new');
cmd.unknownOption = NOOP; cmd.unknownOption = NOOP;
@ -64,24 +68,16 @@ cmd.unknownOption = NOOP;
cmd.description('open the Strapi framework console'); cmd.description('open the Strapi framework console');
cmd.action(require('./strapi-console')); cmd.action(require('./strapi-console'));
/**
* Commands for the Strapi Studio
*/
// `$ strapi link` // `$ strapi link`
cmd = program.command('link'); cmd = program.command('link');
cmd.unknownOption = NOOP; cmd.unknownOption = NOOP;
cmd.description('link an existing application to the Strapi Studio'); cmd.description('link an existing application to the Strapi Studio');
cmd.action(require('./strapi-link')); cmd.action(require('./strapi-link'));
// `$ strapi config`
cmd = program.command('config');
cmd.unknownOption = NOOP;
cmd.description('extend the Strapi framework with custom generators');
cmd.action(require('./strapi-config'));
// `$ strapi update`
cmd = program.command('update');
cmd.unknownOption = NOOP;
cmd.description('pull the latest updates of your custom generators');
cmd.action(require('./strapi-update'));
// `$ strapi login` // `$ strapi login`
cmd = program.command('login'); cmd = program.command('login');
cmd.unknownOption = NOOP; cmd.unknownOption = NOOP;
@ -94,6 +90,22 @@ cmd.unknownOption = NOOP;
cmd.description('logout your account from the Strapi Studio'); cmd.description('logout your account from the Strapi Studio');
cmd.action(require('./strapi-logout')); cmd.action(require('./strapi-logout'));
/**
* Customization commands
*/
// `$ strapi config`
cmd = program.command('config');
cmd.unknownOption = NOOP;
cmd.description('extend the Strapi framework with custom generators');
cmd.action(require('./strapi-config'));
// `$ strapi update`
cmd = program.command('update');
cmd.unknownOption = NOOP;
cmd.description('pull the latest updates of your custom generators');
cmd.action(require('./strapi-update'));
/** /**
* Normalize help argument * Normalize help argument
*/ */

View File

@ -1,240 +0,0 @@
'use strict';
/**
* Module dependencies
*/
// Public node modules.
const _ = require('lodash');
module.exports = {
/**
* Populate the query according to the specified or default
* association attributes.
*
* @param {Object} query
* @param {Object} _ctx
* @param {Object} model
*
* @return {Object} populated query
*/
populateEach: function (query, _ctx, model) {
let shouldPopulate = strapi.config.blueprints.populate;
let aliasFilter = (_ctx.request.query && _ctx.request.query.populate) || (_ctx.request.body && _ctx.request.body.populate);
// Convert the string representation of the filter list to an array. We
// need this to provide flexibility in the request param. This way both
// list string representations are supported:
// /model?populate=alias1,alias2,alias3
// /model?populate=[alias1,alias2,alias3]
if (typeof aliasFilter === 'string') {
aliasFilter = aliasFilter.replace(/\[|\]/g, '');
aliasFilter = (aliasFilter) ? aliasFilter.split(',') : [];
}
return _(model.associations).reduce(function populateEachAssociation(query, association) {
// If an alias filter was provided, override the blueprint config.
if (aliasFilter) {
shouldPopulate = _.contains(aliasFilter, association.alias);
}
// Populate associations and set the according limit.
if (shouldPopulate) {
return query.populate(association.alias, {
limit: strapi.config.blueprints.defaultLimit || 30
});
} else {
return query;
}
}, query);
},
/**
* Parse the model to use
*
* @param {_ctx} _ctx
*
* @return {WLCollection}
*/
parseModel: function (_ctx) {
// Determine the model according to the context.
const model = _ctx.model || _ctx.params.model;
if (!model) {
throw new Error({
message: 'Please provide a valid model.'
});
}
// Select the Waterline model.
const Model = strapi.orm.collections[model];
if (!Model) {
throw new Error({
message: 'Invalid Model.'
});
}
Model.name = model;
return Model;
},
/**
* Parse `values` for a Waterline `create` or `update` from all
* request parameters.
*
* @param {Request} _ctx
*
* @return {Object}
*/
parseValues: function (_ctx) {
const values = _ctx.request.body || _ctx.request.query;
return values;
},
/**
* Parse `criteria` for a Waterline `find` or `update` from all
* request parameters.
*
* @param {Request} _ctx
*
* @return {Object} the where criteria object
*/
parseCriteria: function (_ctx) {
// List of properties to remove.
const blacklist = ['limit', 'skip', 'sort', 'populate'];
// Validate blacklist to provide a more helpful error msg.
if (blacklist && !_.isArray(blacklist)) {
throw new Error('Invalid `_ctx.options.criteria.blacklist`. Should be an array of strings (parameter names.)');
}
// Look for explicitly specified `where` parameter.
let where = _ctx.request.query.where;
// If `where` parameter is a string, try to interpret it as JSON.
if (_.isString(where)) {
try {
where = JSON.parse(where);
} catch (err) {
}
}
// If `where` has not been specified, but other unbound parameter variables
// are specified, build the `where` option using them.
if (!where) {
// Prune params which aren't fit to be used as `where` criteria
// to build a proper where query.
where = _ctx.request.body;
// Omit built-in runtime config (like query modifiers).
where = _.omit(where, blacklist || ['limit', 'skip', 'sort']);
// Omit any params with undefined values.
where = _.omit(where, function (p) {
if (_.isUndefined(p)) {
return true;
}
});
}
// Merge with `_ctx.options.where` and return.
where = _.merge({}, where) || undefined;
return where;
},
/**
* Parse primary key value
*
* @param {Object} _ctx
*
* @return {Integer|String} pk
*/
parsePk: function (_ctx) {
let pk = (_ctx.request.body && _ctx.request.body.where && _ctx.request.body.where.id) || _ctx.params.id;
// Exclude criteria on id field.
pk = _.isPlainObject(pk) ? undefined : pk;
return pk;
},
requirePk: function (_ctx) {
const pk = module.exports.parsePk(_ctx);
// Validate the required `id` parameter.
if (!pk) {
const err = new Error({
message: 'No `id` provided'
});
_ctx.status = 400;
throw err;
}
return pk;
},
/**
* Parse sort params.
*
* @param {Object} _ctx
*/
parseSort: function (_ctx) {
_ctx.options = _ctx.options || {};
let sort = _ctx.request.query.sort || _ctx.options.sort;
if (typeof sort === 'undefined') {
return undefined;
}
if (typeof sort === 'string') {
try {
sort = JSON.parse(sort);
} catch (err) {
}
}
return sort;
},
/**
* Parse limit params.
*
* @param {Object} _ctx
*/
parseLimit: function (_ctx) {
_ctx.options = _ctx.options || {};
let limit = Number(_ctx.request.query.limit) || strapi.config.blueprints.defaultLimit || 30;
if (limit) {
limit = +limit;
}
return limit;
},
/**
* Parse skip params.
*
* @param {Object} _ctx
*/
parseSkip: function (_ctx) {
_ctx.options = _ctx.options || {};
let skip = _ctx.request.query.skip || 0;
if (skip) {
skip = +skip;
}
return skip;
}
};

View File

@ -1,184 +0,0 @@
'use strict';
/**
* Module dependencies
*/
// Public node modules.
const _ = require('lodash');
const async = require('async');
// Local utils.
const actionUtil = require('../actionUtil');
/**
* Add an entry to a specific parent entry
*/
module.exports = function destroy(_ctx) {
const deferred = Promise.defer();
// Ensure a model and alias can be deduced from the request.
const Model = actionUtil.parseModel(_ctx);
const relation = _ctx.params.relation;
if (!relation) {
_ctx.status = 500;
return deferred.reject({
message: 'Missing required route option, `_ctx.params.relation`.'
});
}
// The primary key of the parent record.
const parentPk = _ctx.params.parentId;
// Find the alias key.
const associationAttr = _.findWhere(strapi.orm.collections[_ctx.model].associations, {alias: relation});
// Init the child model.
const ChildModel = strapi.orm.collections[associationAttr.collection];
const childPkAttr = ChildModel.primaryKey;
_ctx.options = _ctx.options || {};
// The child record to associate is defined by either...
// a primary key or an object of values.
let child;
const supposedChildPk = actionUtil.parsePk(_ctx);
if (supposedChildPk) {
child = {};
child[childPkAttr] = supposedChildPk;
} else {
_ctx.options.values = _ctx.options.values || {};
_ctx.options.values.blacklist = _ctx.options.values.blacklist || ['limit', 'skip', 'sort', 'id', 'parentId'];
child = actionUtil.parseValues(_ctx);
}
if (!child) {
_ctx.status = 400;
deferred.reject({
message: 'You must specify the record to add (either the primary key of an existing record to link, or a new object without a primary key which will be used to create a record then link it.)'
});
}
async.auto({
// Look up the parent record.
parent: function (cb) {
Model.findOne(parentPk).exec(function foundParent(err, parentRecord) {
if (err) {
return cb(err);
}
if (!parentRecord) {
return cb({status: 404});
}
if (!parentRecord[relation]) {
return cb({status: 404});
}
cb(null, parentRecord);
});
},
// If a primary key was specified in the `child` object we parsed
// from the request, look it up to make sure it exists. Send back its primary key value.
// This is here because, although you can do this with `.save()`, you can't actually
// get ahold of the created child record data, unless you create it first.
actualChildPkValue: ['parent', function (cb) {
// Below, we use the primary key attribute to pull out the primary key value
// (which might not have existed until now, if the .add() resulted in a `create()`).
// If the primary key was specified for the child record, we should try to find
// it before we create it.
// Otherwise, it must be referring to a new thing, so create it.
if (child[childPkAttr]) {
ChildModel.findOne(child[childPkAttr]).exec(function foundChild(err, childRecord) {
if (err) {
return cb(err);
}
// Didn't find it? Then try creating it.
if (!childRecord) {
return createChild();
}
// Otherwise use the one we found.
return cb(null, childRecord[childPkAttr]);
});
} else {
return createChild();
}
// Create a new instance and send out any required pub/sub messages.
function createChild() {
ChildModel.create(child).exec(function createdNewChild(err, newChildRecord) {
if (err) {
return cb(err);
}
return cb(null, newChildRecord[childPkAttr]);
});
}
}],
// Add the child record to the parent's collection.
add: ['parent', 'actualChildPkValue', function (cb, asyncData) {
// `collection` is the parent record's collection we
// want to add the child to.
try {
const collection = asyncData.parent[relation];
collection.add(asyncData.actualChildPkValue);
return cb();
} catch (err) {
if (err) {
return cb(err);
}
return cb();
}
}]
},
// Save the parent record.
function readyToSave(err, asyncData) {
if (err) {
_ctx.status = 400;
deferred.reject(err);
}
asyncData.parent.save(function saved(err) {
// Ignore `insert` errors for duplicate adds
// (but keep in mind, we should not `publishAdd` if this is the case...)
const isDuplicateInsertError = (err && typeof err === 'object' && err.length && err[0] && err[0].type === 'insert');
if (err && !isDuplicateInsertError) {
deferred.reject(err);
}
// Finally, look up the parent record again and populate the relevant collection.
let query = Model.findOne(parentPk);
query = actionUtil.populateEach(query, _ctx, Model);
query.populate(relation);
query.exec(function (err, matchingRecord) {
if (err) {
return deferred.reject(err);
}
if (!matchingRecord) {
return deferred.reject({
message: 'Matching record not found.'
});
}
if (!matchingRecord[relation]) {
return deferred.reject({
message: '`matchingRecord[relation]` not found.'
});
}
return deferred.resolve(matchingRecord);
});
});
});
return deferred.promise;
};

View File

@ -1,93 +0,0 @@
'use strict';
/**
* Module dependencies
*/
// Public node modules.
const _ = require('lodash');
// Local utils.
const actionUtil = require('../actionUtil');
const associationUtil = require('../associationUtil');
/**
* Create an entry
*/
module.exports = function create(_ctx) {
const deferred = Promise.defer();
// Return the model used.
const Model = actionUtil.parseModel(_ctx);
// Parse the values of the record to create.
const values = actionUtil.parseValues(_ctx);
// Associations validation.
const associationsValidationPromises = [];
// Check if the relations are existing for `OneWay` associations.
_.forEach(_.where(Model.associations, {nature: 'oneWay'}), function (association) {
if (values[association.alias] || association.required) {
associationsValidationPromises.push(associationUtil.doesRecordExist(association.model, values[association.alias]));
}
});
// Check if the relations are existing for `OneToOne` associations.
_.forEach(_.where(Model.associations, {nature: 'oneToOne'}), function (association) {
if (values[association.alias] || association.required) {
associationsValidationPromises.push(associationUtil.doesRecordExist(association.model, values[association.alias]));
}
});
Promise.all(associationsValidationPromises)
.then(function () {
Model.create(values).exec(function created(err, newInstance) {
if (err) {
_ctx.status = 400;
return deferred.reject(err);
}
// Update `oneToOneRelations`.
const relationPromises = [];
// Update the `oneToOne` relations.
_.forEach(_.where(Model.associations, {nature: 'oneToOne'}), function (relation) {
relationPromises.push(associationUtil.oneToOneRelationUpdated(_ctx.model || _ctx.params.model, newInstance.id, relation.model, newInstance[relation.alias]));
});
Promise.all(relationPromises)
// Related records updated.
.then(function () {
let query = Model.findOne(newInstance[Model.primaryKey]);
query = actionUtil.populateEach(query, _ctx, Model);
query.exec(function foundAgain(err, populatedRecord) {
if (err) {
_ctx.status = 500;
return deferred.reject(err);
}
// Entry created.
_ctx.status = 201;
deferred.resolve(populatedRecord);
});
})
// Error during related records update.
.catch(function (err) {
_ctx.status = 400;
deferred.reject(err);
});
});
})
// Error during related records check.
.catch(function (err) {
_ctx.status = 400;
deferred.reject(err);
});
return deferred.promise;
};

View File

@ -1,80 +0,0 @@
'use strict';
/**
* Module dependencies
*/
// Public node modules.
const _ = require('lodash');
// Local utils.
const actionUtil = require('../actionUtil');
const associationUtil = require('../associationUtil');
/**
* Destroy an entry
*/
module.exports = function destroy(_ctx) {
const deferred = Promise.defer();
// Return the model used.
const Model = actionUtil.parseModel(_ctx);
// Locate and validate the required `id` parameter.
const pk = actionUtil.requirePk(_ctx);
// First, check if the record exists.
const query = Model.findOne(pk);
query.exec(function foundRecord(err, record) {
if (err) {
_ctx.status = 500;
deferred.reject(err);
}
// Record not found.
if (!record) {
_ctx.status = 404;
deferred.reject({
message: 'No record found with the specified `id`.'
});
}
// Destroy the record.
Model.destroy(pk).exec(function destroyedRecord(err, deletedRecords) {
if (err) {
_ctx.status = 500;
return deferred.reject(err);
}
// Select the first object of the updated records.
const deletedRecord = deletedRecords[0];
// Update the `oneToOne` relations.
const relationPromises = [];
_.forEach(_.where(Model.associations, {nature: 'oneToOne'}), function (relation) {
relationPromises.push(associationUtil.removeRelationsOut(_ctx.model || _ctx.params.model, deletedRecord.id, relation.model));
});
// Update the `oneToMany` relations.
_.forEach(_.where(Model.associations, {nature: 'oneToMany'}), function (relation) {
relationPromises.push(associationUtil.removeRelationsOut(_ctx.model || _ctx.params.model, deletedRecord.id, relation.collection));
});
Promise.all(relationPromises)
// Related records updated.
.then(function () {
deferred.resolve(deletedRecord);
})
// Error during related records update.
.catch(function (err) {
_ctx.status = 500;
deferred.reject(err);
});
});
});
return deferred.promise;
};

View File

@ -1,44 +0,0 @@
'use strict';
/**
* Module dependencies
*/
// Local utils.
const actionUtil = require('../actionUtil');
/**
* Find entries
*/
module.exports = function find(_ctx) {
const deferred = Promise.defer();
// Use the `findOne` action if an `id` is specified.
if (actionUtil.parsePk(_ctx)) {
return require('./findOne')(_ctx);
}
// Look up the model.
const Model = actionUtil.parseModel(_ctx);
// Init the query.
let query = Model.find()
.where(actionUtil.parseCriteria(_ctx))
.limit(actionUtil.parseLimit(_ctx))
.skip(actionUtil.parseSkip(_ctx))
.sort(actionUtil.parseSort(_ctx));
query = actionUtil.populateEach(query, _ctx, Model);
query.exec(function found(err, matchingRecords) {
if (err) {
_ctx.status = 500;
deferred.reject(err);
}
// Records found.
deferred.resolve(matchingRecords);
});
return deferred.promise;
};

View File

@ -1,44 +0,0 @@
'use strict';
/**
* Module dependencies
*/
// Local utils.
const actionUtil = require('../actionUtil');
/**
* Find a specific entry
*/
module.exports = function destroy(_ctx) {
const deferred = Promise.defer();
// Return the model used.
const Model = actionUtil.parseModel(_ctx);
// Locate and validate the required `id` parameter.
const pk = actionUtil.requirePk(_ctx);
// Init the query.
let query = Model.findOne(pk);
query = actionUtil.populateEach(query, _ctx, Model);
query.exec(function found(err, matchingRecord) {
if (err) {
_ctx.status = 500;
deferred.reject(err);
}
if (!matchingRecord) {
_ctx.status = 404;
return deferred.reject({
message: 'No ' + Model.name + ' found with the specified `id`.'
});
}
// Record found.
deferred.resolve(matchingRecord);
});
return deferred.promise;
};

View File

@ -1,109 +0,0 @@
'use strict';
/**
* Module dependencies
*/
// Public node modules.
const _ = require('lodash');
// Local utils.
const actionUtil = require('../actionUtil');
const associationUtil = require('../associationUtil');
/**
* Remove an entry to a specific parent entry
*/
module.exports = function remove(_ctx) {
const deferred = Promise.defer();
// Ensure a model and alias can be deduced from the request.
const Model = actionUtil.parseModel(_ctx);
_ctx.options = _ctx.options || {};
const relation = _ctx.params.relation;
const associationAttr = _.findWhere(strapi.orm.collections[_ctx.model].associations, {alias: relation});
if (!associationAttr) {
_ctx.status = 500;
return deferred.reject({
message: 'Missing required route option, `_ctx.options.alias`.'
});
}
// The primary key of the parent record.
const parentPk = _ctx.params.parentId;
// The primary key of the child record to remove
// from the aliased collection.
let childPk = actionUtil.parsePk(_ctx);
// Check if the `childPk` is defined.
if (_.isUndefined(childPk)) {
_ctx.status = 400;
return deferred.reject({
message: 'Missing required child PK.'
});
}
// Find the parent object.
Model.findOne(parentPk)
.populate(relation)
.exec(function found(err, parentRecord) {
if (err) {
_ctx.status = 500;
return deferred.reject(err);
}
// Format `childPk` for the `findWhere` used next.
childPk = isNaN(childPk) ? childPk : Number(childPk);
if (!parentRecord || !parentRecord[relation] || (!_.findWhere(parentRecord[relation], {id: childPk})) && parentRecord[relation].id !== childPk) {
_ctx.status = 404;
return deferred.reject({
message: 'Not found'
});
}
const relationPromises = [];
if (parentRecord[relation].id === childPk) {
// Set to null
parentRecord[relation] = null;
relationPromises.push(associationUtil.removeRelationsOut(_ctx.model || _ctx.params.model, parentRecord.id, relation));
} else if (_.findWhere(parentRecord[relation], {id: childPk})) {
// Remove.
parentRecord[relation].remove(childPk);
}
// Save.
parentRecord.save(function (err) {
if (err) {
_ctx.status = 400;
return deferred.reject(err);
}
Promise.all(relationPromises)
.then(function () {
// New query to `findOne` and properly populate it.
let query = Model.findOne(parentPk);
query = actionUtil.populateEach(query, _ctx, Model);
query.exec(function found(err, parentRecord) {
if (err || !parentRecord) {
_ctx.status = 500;
return deferred.reject(err);
}
return deferred.resolve(parentRecord);
});
})
.catch(function (err) {
deferred.reject(err);
});
});
});
return deferred.promise;
};

View File

@ -1,119 +0,0 @@
'use strict';
/**
* Module dependencies
*/
// Public node modules.
const _ = require('lodash');
// Local utils.
const actionUtil = require('../actionUtil');
const associationUtil = require('../associationUtil');
/**
* Destroy an entry
*/
module.exports = function update(_ctx) {
const deferred = Promise.defer();
// Return the model used.
const Model = actionUtil.parseModel(_ctx);
// Locate and validate the required `id` parameter.
const pk = actionUtil.requirePk(_ctx);
// Parse the values of the record to update.
const values = actionUtil.parseValues(_ctx);
// No matter what, don't allow changing the `pk` via the update blueprint
// (you should just drop and re-add the record if that's what you really want).
if (typeof values[Model.primaryKey] !== 'undefined' && values[Model.primaryKey] !== pk) {
strapi.log.warn('Cannot change primary key via update action; ignoring value sent for `' + Model.primaryKey + '`');
}
// Make sure the primary key is unchanged.
values[Model.primaryKey] = pk;
Model.findOne(pk).exec(function found(err, matchingRecord) {
if (err) {
_ctx.status = 500;
return deferred.reject(err);
}
if (!matchingRecord) {
_ctx.status = 404;
return deferred.reject('Record not found');
}
// Associations validation.
const associationsValidationPromises = [];
// One way associations.
_.forEach(_.where(Model.associations, {nature: 'oneWay'}), function (association) {
if (values[association.alias] || association.required) {
associationsValidationPromises.push(associationUtil.doesRecordExist(association.model, values[association.alias]));
}
});
// One to one associations.
_.forEach(_.where(Model.associations, {nature: 'oneToOne'}), function (association) {
if (values[association.alias] || association.required) {
associationsValidationPromises.push(associationUtil.doesRecordExist(association.model, values[association.alias]));
}
});
// Check relations params.
Promise.all(associationsValidationPromises)
.then(function () {
Model.update(pk, values).exec(function updated(err, records) {
if (err) {
_ctx.status = 400;
return deferred.reject(err);
}
// Select the first and only one record.
const updatedRecord = records[0];
// Update `oneToOneRelations`.
const relationPromises = [];
_.forEach(_.where(Model.associations, {nature: 'oneToOne'}), function (relation) {
relationPromises.push(associationUtil.oneToOneRelationUpdated(_ctx.model || _ctx.params.model, pk, relation.model, updatedRecord[relation.alias]));
});
// Update the related records.
Promise.all(relationPromises)
.then(function () {
// Extra query to find and populate the updated record.
let query = Model.findOne(updatedRecord[Model.primaryKey]);
query = actionUtil.populateEach(query, _ctx, Model);
query.exec(function foundAgain(err, populatedRecord) {
if (err) {
_ctx.status = 500;
return deferred.reject(err);
}
deferred.resolve(populatedRecord);
});
})
// Error during related records update.
.catch(function (err) {
_ctx.status = 400;
deferred.reject(err);
});
});
})
// Error during the new related records check.
.catch(function (err) {
_ctx.status = 400;
deferred.reject(err);
});
});
return deferred.promise;
};

View File

@ -1,215 +0,0 @@
'use strict';
/**
* Module dependencies
*/
// Public node modules.
const _ = require('lodash');
module.exports = {
/**
* Helper which returns a promise and then
* the found record
*
* @param {Object} model
* @param {string|int} id
*
* @return {Function|promise}
*/
doesRecordExist: function doesRecordExist(model, id) {
const deferred = Promise.defer();
strapi.orm
.collections[model]
.findOne(id)
.exec(function (err, foundRecord) {
if (err) {
return deferred.reject(err);
}
if (!foundRecord) {
return deferred.reject({
message: 'No ' + model + ' found with the specified `id`.'
});
}
deferred.resolve(foundRecord);
});
return deferred.promise;
},
/**
* Helper which remove the relations of a specific entry and
* update the new relation if a relationId is specified
*
* @param originalModelAlias
* @param originalModelId
* @param relationModel
* @param relationId
*
* @return {Function|promise}
*/
oneToOneRelationUpdated: function oneToOneRelationUpdated(originalModelAlias, originalModelId, relationModel, relationId) {
const deferred = Promise.defer();
// First remove all relations
const promises = [];
// Update the relation of the origin model
promises.push(module.exports.removeRelationsOut(originalModelAlias, originalModelId, relationModel));
// Update the entries of the same collection
// of the original model.
promises.push(module.exports.removeRelationsIn(originalModelAlias, originalModelId, relationModel, relationId));
Promise.all(promises)
.then(function () {
// If a relationId is provided, update the new linked entry.
if (relationId) {
strapi.orm.collections[relationModel]
.findOne(relationId)
.exec(function (err, record) {
if (err) {
return deferred.reject(err);
}
if (!record) {
return deferred.reject({
message: 'Relation not found'
});
}
record[originalModelAlias] = originalModelId;
record.save(function (err, record) {
if (err) {
return deferred.reject(err);
}
deferred.resolve(record);
});
});
} else {
deferred.resolve();
}
})
.catch(function (err) {
deferred.reject(err);
});
return deferred.promise;
},
/**
* Helper which remove all the relations
* of a specific model
*
* @param originalModelAlias
* @param originalModelId
* @param relationModel
*
* @return {Function|promise}
*/
removeRelationsOut: function removeRelationsOut(originalModelAlias, originalModelId, relationModel) {
const deferred = Promise.defer();
if (!originalModelAlias) {
return deferred.reject({
message: 'originalModelAlias invalid.'
});
}
// Params object used for the `find`function.
const findParams = {};
findParams[originalModelAlias] = originalModelId;
// Find all the matching entries of the original model.
strapi.orm.collections[relationModel]
.find(findParams)
.exec(function (err, records) {
if (err) {
return deferred.reject(err);
}
// Init the array of promises.
const savePromises = [];
// Set the relation to null.
// Save the entry and add the promise in the array.
_.forEach(records, function (record) {
record[originalModelAlias] = null;
savePromises.push(record.save());
});
Promise.all(savePromises)
.then(function () {
deferred.resolve(records);
})
.catch(function (err) {
deferred.reject(err);
});
});
return deferred.promise;
},
/**
* Helper which remove all the relations
* of a specific model
*
* @param originalModelAlias
* @param originalModelId
* @param relationModel
* @param {number|string}relationId
*
* @return {Function|promise}
*/
removeRelationsIn: function removeRelationsIn(originalModelAlias, originalModelId, relationModel, relationId) {
const deferred = Promise.defer();
// Params object used for the `find` function.
const findParams = {};
findParams[relationModel] = relationId;
findParams.id = {
'!': originalModelId
};
// Find all the matching entries of the original model.
strapi.orm.collections[originalModelAlias]
.find(findParams)
.exec(function (err, records) {
if (err) {
return deferred.reject(err);
}
// Init the array of promises.
const savePromises = [];
_.forEach(records, function (record) {
// Set the relation to null
if (record[relationModel]) {
record[relationModel] = null;
}
// Save the entry and add the promise in the array
savePromises.push(record.save());
});
Promise.all(savePromises)
.then(function () {
deferred.resolve();
})
.catch(function (err) {
deferred.reject(err);
});
});
return deferred.promise;
}
};

View File

@ -1,48 +0,0 @@
'use strict';
/**
* Blueprints hook
*/
module.exports = function () {
const hook = {
/**
* Default options
*/
defaults: {
blueprints: {
defaultLimit: 30,
populate: true
}
},
/**
* Export functions
*/
// Utils
actionUtil: require('./actionUtil'),
associationUtil: require('./associationUtil'),
// Actions
find: require('./actions/find'),
findOne: require('./actions/findOne'),
create: require('./actions/create'),
update: require('./actions/update'),
destroy: require('./actions/destroy'),
remove: require('./actions/remove'),
add: require('./actions/add'),
/**
* Initialize the hook
*/
initialize: function (cb) {
cb();
}
};
return hook;
};

View File

@ -8,7 +8,6 @@ module.exports = {
_config: true, _config: true,
_api: true, _api: true,
responseTime: true, responseTime: true,
waterline: true,
bodyParser: true, bodyParser: true,
session: true, session: true,
grant: true, grant: true,
@ -22,7 +21,6 @@ module.exports = {
i18n: true, i18n: true,
cron: true, cron: true,
logger: true, logger: true,
blueprints: true,
views: true, views: true,
router: true, router: true,
static: true, static: true,

View File

@ -103,20 +103,6 @@ module.exports = function (strapi) {
} }
}); });
// Define GraphQL route with modified Waterline models to GraphQL schema
// or disable the global variable
if (strapi.config.graphql.enabled === true) {
// Wait GraphQL schemas generation
strapi.once('waterline:graphql:ready', function () {
strapi.router.get(strapi.config.graphql.route, strapi.middlewares.graphql({
schema: strapi.schemas,
pretty: true
}));
});
} else {
global.graphql = undefined;
}
// Let the router use our routes and allowed methods. // Let the router use our routes and allowed methods.
strapi.app.use(strapi.router.routes()); strapi.app.use(strapi.router.routes());
strapi.app.use(strapi.router.allowedMethods()); strapi.app.use(strapi.router.allowedMethods());

View File

@ -1,49 +0,0 @@
'use strict';
/**
* Module dependencies
*/
// Locale helpers.
const isManyToOneAssociation = require('./isManyToOneAssociation');
const isOneToOneAssociation = require('./isOneToOneAssociation');
const isOneWayAssociation = require('./isOneWayAssociation');
const isOneToManyAssociation = require('./isOneToManyAssociation');
const isManyToManyAssociation = require('./isManyToManyAssociation');
/**
* Helper which returns the association type of the attribute
*
* @param {Object} currentModel
* @param {Object} association
*
* @return {boolean}
*/
module.exports = {
getAssociationType: function (currentModel, association) {
let associationType;
if (association.type === 'model') {
if (isManyToOneAssociation(currentModel, association)) {
associationType = 'manyToOne';
} else if (isOneToOneAssociation(currentModel, association)) {
associationType = 'oneToOne';
} else if (isOneWayAssociation(currentModel, association)) {
associationType = 'oneWay';
} else {
associationType = 'unknown';
}
} else if (association.type === 'collection') {
if (isOneToManyAssociation(currentModel, association)) {
associationType = 'oneToMany';
} else if (isManyToManyAssociation(currentModel, association)) {
associationType = 'manyToMany';
} else {
associationType = 'unknown';
}
}
return associationType;
}
};

View File

@ -1,24 +0,0 @@
'use strict';
/**
* Module dependencies
*/
// Public node modules.
const _ = require('lodash');
/**
* Helper which returns a boolean. True if the type
* of the relation is `manyToMany`.
*
* @param {Object} currentModel
* @param {Object} association
*
* @return {boolean}
*/
module.exports = function isManyToManyAssociation(currentModel, association) {
return _.findWhere(strapi.models[association.collection] && strapi.models[association.collection].associations, {
collection: currentModel
});
};

View File

@ -1,24 +0,0 @@
'use strict';
/**
* Module dependencies
*/
// Public node modules.
const _ = require('lodash');
/**
* Helper which returns a boolean. True if the type
* of the relation is `manyToOne`.
*
* @param {Object} currentModel
* @param {Object} association
*
* @return {boolean}
*/
module.exports = function isManyToOneAssociation(currentModel, association) {
return _.findWhere(strapi.models[association.model] && strapi.models[association.model].associations, {
collection: currentModel
});
};

View File

@ -1,24 +0,0 @@
'use strict';
/**
* Module dependencies
*/
// Public node modules.
const _ = require('lodash');
/**
* Helper which returns a boolean. True if the type
* of the relation is `oneToMany`.
*
* @param {Object} currentModel
* @param {Object} association
*
* @return {boolean}
*/
module.exports = function isOneToManyAssociation(currentModel, association) {
return _.findWhere(strapi.models[association.collection] && strapi.models[association.collection].associations, {
model: currentModel
});
};

View File

@ -1,24 +0,0 @@
'use strict';
/**
* Module dependencies
*/
// Public node modules.
const _ = require('lodash');
/**
* Helper which returns a boolean. True if the type
* of the relation is `oneToOne`.
*
* @param {Object} currentModel
* @param {Object} association
*
* @return {boolean}
*/
module.exports = function isOneToOneAssociation(currentModel, association) {
return _.findWhere(strapi.models[association.model] && strapi.models[association.model].associations, {
model: currentModel
});
};

View File

@ -1,23 +0,0 @@
'use strict';
/**
* Module dependencies
*/
// Locale helpers.
const isOneToManyAssociation = require('./isOneToManyAssociation');
const isOneToOneAssociation = require('./isOneToOneAssociation');
/**
* Helper which returns a boolean. True if the type
* of the relation is `oneToOne`.
*
* @param {Object} currentModel
* @param {Object} association
*
* @return {boolean}
*/
module.exports = function isOneWayAssociation(currentModel, association) {
return !(isOneToManyAssociation(currentModel, association) || isOneToOneAssociation(currentModel, association));
};

View File

@ -1,302 +0,0 @@
'use strict';
/**
* Module dependencies
*/
// Node.js core.
const cluster = require('cluster');
const path = require('path');
const spawn = require('child_process').spawn;
// Public node modules.
const _ = require('lodash');
const async = require('async');
const Waterline = require('waterline');
const WaterlineGraphQL = require('waterline-graphql');
// Local utilities.
const helpers = require('./helpers/index');
/**
* Waterline ORM hook
*/
module.exports = function (strapi) {
const hook = {
/**
* Default options
*/
defaults: {
orm: {
adapters: {
disk: 'sails-disk'
},
defaultConnection: 'default',
connections: {
default: {
adapter: 'disk',
filePath: '.tmp/',
fileName: 'default.db',
migrate: 'alter'
},
permanent: {
adapter: 'disk',
filePath: './data/',
fileName: 'permanent.db',
migrate: 'alter'
}
}
},
globals: {
models: true
}
},
/**
* Initialize the hook
*/
initialize: function (cb) {
if (_.isPlainObject(strapi.config.orm) && !_.isEmpty(strapi.config.orm) && (((cluster.isWorker && strapi.config.reload.workers > 0) || (cluster.isMaster && strapi.config.reload.workers < 1)) || !strapi.config.reload && cluster.isMaster)) {
strapi.adapters = {};
strapi.collections = [];
// Expose a new instance of Waterline.
if (!strapi.orm) {
strapi.orm = new Waterline();
}
// Prefix every adapter and require them from the
// `node_modules` directory of the application.
_.forEach(strapi.config.orm.adapters, function (adapter, name) {
try {
strapi.adapters[name] = require(path.resolve(strapi.config.appPath, 'node_modules', adapter));
} catch (err) {
strapi.log.error('The adapter `' + adapter + '` is not installed.');
process.exit(1);
}
});
// Check if the adapter in every connections exists.
_.forEach(strapi.config.orm.connections, function (settings, name) {
if (!_.has(strapi.config.orm.adapters, settings.adapter)) {
strapi.log.error('Unknown adapter `' + settings.adapter + '` for connection `' + name + '`.');
process.exit(1);
}
});
// Parse each models.
_.forEach(strapi.models, function (definition, model) {
_.bindAll(definition);
// Make sure the model has a connection.
// If not, use the default connection.
if (_.isEmpty(definition.connection)) {
definition.connection = strapi.config.orm.defaultConnection;
}
// Make sure this connection exists.
if (!_.has(strapi.config.orm.connections, definition.connection)) {
strapi.log.error('The connection `' + definition.connection + '` specified in the `' + model + '` model does not exist.');
process.exit(1);
}
// Make sure this connection has an appropriate migrate strategy.
// If not, use the appropriate strategy.
if (!_.has(strapi.config.orm.connections[definition.connection], 'migrate')) {
if (strapi.config.environment === 'production') {
strapi.log.warn('Setting the migrate strategy of the `' + model + '` model to `safe`.');
strapi.config.orm.connections[definition.connection].migrate = 'safe';
} else {
strapi.log.warn('Setting the migrate strategy of the `' + model + '` model to `alter`.');
strapi.config.orm.connections[definition.connection].migrate = 'alter';
}
} else if (strapi.config.environment === 'production' && strapi.config.orm.connections[definition.connection].migrate === ('alter' || 'drop')) {
strapi.log.warn('Setting the migrate strategy of the `' + model + '` model to `safe`.');
strapi.config.orm.connections[definition.connection].migrate = 'safe';
}
// Apply the migrate strategy to the model.
definition.migrate = strapi.config.orm.connections[definition.connection].migrate;
// Derive information about this model's associations from its schema
// and attach/expose the metadata as `SomeModel.associations` (an array).
definition.associations = _.reduce(definition.attributes, function (associatedWith, attrDef, attrName) {
if (typeof attrDef === 'object' && (attrDef.model || attrDef.collection)) {
const assoc = {
alias: attrName,
type: attrDef.model ? 'model' : 'collection'
};
if (attrDef.model) {
assoc.model = attrDef.model;
}
if (attrDef.collection) {
assoc.collection = attrDef.collection;
}
if (attrDef.via) {
assoc.via = attrDef.via;
}
associatedWith.push(assoc);
}
return associatedWith;
}, []);
// Finally, load the collection in the Waterline instance.
try {
const collection = strapi.orm.loadCollection(Waterline.Collection.extend(definition));
if (_.isFunction(collection)) {
strapi.collections.push(collection);
}
} catch (err) {
strapi.log.error('Impossible to register the `' + model + '` model.');
process.exit(1);
}
});
// Finally, initialize the Waterline ORM and
// globally expose models.
strapi.orm.initialize({
adapters: strapi.adapters,
connections: strapi.config.orm.connections,
collections: strapi.collections,
defaults: {
connection: strapi.config.orm.defaultConnection
}
}, function () {
if (strapi.config.globals.models === true) {
_.forEach(strapi.models, function (definition, model) {
const globalName = _.capitalize(strapi.models[model].globalId);
global[globalName] = strapi.orm.collections[model];
});
}
// Parse each models and look for associations.
_.forEach(strapi.orm.collections, function (definition, model) {
_.forEach(definition.associations, function (association) {
association.nature = helpers.getAssociationType(model, association);
});
});
if (strapi.config.graphql.enabled === true) {
// Parse each models and add associations array
_.forEach(strapi.orm.collections, function (collection, key) {
if (strapi.models.hasOwnProperty(key)) {
collection.associations = strapi.models[key].associations || [];
}
});
// Expose the GraphQL schemas at `strapi.schemas`
WaterlineGraphQL.getGraphQLSchema({
collections: strapi.orm.collections,
usefulFunctions: true
}, function (schemas) {
strapi.schemas = schemas;
strapi.emit('waterline:graphql:ready');
});
}
cb();
});
} else {
cb();
}
},
/**
* Reload the hook
*/
reload: function () {
hook.teardown(function () {
delete strapi.orm;
hook.initialize(function (err) {
if (err) {
strapi.log.error('Failed to reinitialize the ORM hook.');
strapi.stop();
} else {
strapi.emit('hook:waterline:reloaded');
}
});
});
},
/**
* Teardown adapters
*/
teardown: function (cb) {
cb = cb || function (err) {
if (err) {
strapi.log.error('Failed to teardown ORM adapters.');
strapi.stop();
}
};
async.forEach(Object.keys(strapi.adapters || {}), function (name, next) {
if (strapi.adapters[name].teardown) {
strapi.adapters[name].teardown(null, next);
} else {
next();
}
}, cb);
},
/**
* Installation adapters
*/
installation: function () {
const done = _.after(_.size(strapi.config.orm.adapters), function () {
strapi.emit('hook:waterline:installed');
});
_.forEach(strapi.config.orm.adapters, function (adapter) {
try {
require(path.resolve(strapi.config.appPath, 'node_modules', adapter));
done();
} catch (err) {
if (strapi.config.environment === 'development') {
strapi.log.warn('Installing the `' + adapter + '` adapter, please wait...');
console.log();
const process = spawn('npm', ['install', adapter, '--save']);
process.on('error', function (error) {
strapi.log.error('The adapter `' + adapter + '` has not been installed.');
strapi.log.error(error);
process.exit(1);
});
process.on('close', function (code) {
if (code !== 0) {
strapi.log.error('The adapter `' + adapter + '` has not been installed.');
strapi.log.error('Code: ' + code);
process.exit(1);
}
strapi.log.info('`' + adapter + '` successfully installed');
done();
});
} else {
strapi.log.error('The adapter `' + adapter + '` is not installed.');
strapi.log.error('Execute `$ npm install ' + adapter + ' --save` to install it.');
process.exit(1);
}
}
});
}
};
return hook;
};

View File

@ -39,10 +39,8 @@ module.exports = function (strapi) {
// Remove undesired hooks when this is a `dry` application. // Remove undesired hooks when this is a `dry` application.
if (strapi.config.dry) { if (strapi.config.dry) {
delete hooks.blueprints;
delete hooks.grant; delete hooks.grant;
delete hooks.studio; delete hooks.studio;
delete hooks.waterline;
} }
// Handle folder-defined modules (default to `./lib/index.js`) // Handle folder-defined modules (default to `./lib/index.js`)
@ -130,7 +128,7 @@ module.exports = function (strapi) {
// Prepare all other hooks. // Prepare all other hooks.
prepare: function prepareHooks(cb) { prepare: function prepareHooks(cb) {
async.each(_.without(_.keys(hooks), '_config', '_api', 'studio', 'router', 'waterline'), function (id, cb) { async.each(_.without(_.keys(hooks), '_config', '_api', 'studio', 'router'), function (id, cb) {
prepareHook(id); prepareHook(id);
process.nextTick(cb); process.nextTick(cb);
}, cb); }, cb);
@ -138,7 +136,7 @@ module.exports = function (strapi) {
// Apply the default config for all other hooks. // Apply the default config for all other hooks.
defaults: function defaultConfigHooks(cb) { defaults: function defaultConfigHooks(cb) {
async.each(_.without(_.keys(hooks), '_config', '_api', 'studio', 'router', 'waterline'), function (id, cb) { async.each(_.without(_.keys(hooks), '_config', '_api', 'studio', 'router'), function (id, cb) {
const hook = hooks[id]; const hook = hooks[id];
applyDefaults(hook); applyDefaults(hook);
process.nextTick(cb); process.nextTick(cb);
@ -147,7 +145,7 @@ module.exports = function (strapi) {
// Load all other hooks. // Load all other hooks.
load: function loadOtherHooks(cb) { load: function loadOtherHooks(cb) {
async.each(_.without(_.keys(hooks), '_config', '_api', 'studio', 'router', 'waterline'), function (id, cb) { async.each(_.without(_.keys(hooks), '_config', '_api', 'studio', 'router'), function (id, cb) {
loadHook(id, cb); loadHook(id, cb);
}, cb); }, cb);
}, },
@ -160,16 +158,6 @@ module.exports = function (strapi) {
prepareHook('router'); prepareHook('router');
applyDefaults(hooks.router); applyDefaults(hooks.router);
loadHook('router', cb); loadHook('router', cb);
},
// Load the waterline hook.
waterline: function loadWaterlineHook(cb) {
if (!hooks.waterline) {
return cb();
}
prepareHook('waterline');
applyDefaults(hooks.waterline);
loadHook('waterline', cb);
} }
}, },

View File

@ -55,7 +55,6 @@ module.exports = cb => {
// Run adapters installation // Run adapters installation
if (cluster.isMaster) { if (cluster.isMaster) {
strapi.hooks.waterline.installation();
++count; ++count;
@ -72,44 +71,34 @@ module.exports = cb => {
console.log(); console.log();
} }
// Teardown Waterline adapters and // Reload the router.
// reload the Waterline ORM. strapi.after('hook:router:reloaded', () => {
strapi.after('hook:waterline:reloaded', () => { process.nextTick(() => cb());
strapi.after('hook:router:reloaded', () => {
process.nextTick(() => cb());
// Update `strapi` status. // Update `strapi` status.
strapi.reloaded = true; strapi.reloaded = true;
strapi.reloading = false; strapi.reloading = false;
// Finally inform the developer everything seems ok. // Finally inform the developer everything seems ok.
if (cluster.isMaster && _.isPlainObject(strapi.config.reload) && !_.isEmpty(strapi.config.reload) && strapi.config.reload.workers < 1) { if (cluster.isMaster && _.isPlainObject(strapi.config.reload) && !_.isEmpty(strapi.config.reload) && strapi.config.reload.workers < 1) {
strapi.log.info('Application\'s dictionnary updated'); strapi.log.info('Application\'s dictionnary updated');
strapi.log.warn('You still need to restart your server to fully enjoy changes...'); strapi.log.warn('You still need to restart your server to fully enjoy changes...');
} }
strapi.once('restart:done', function () { strapi.once('restart:done', function () {
strapi.log.info('Application successfully restarted'); strapi.log.info('Application successfully restarted');
});
if (cluster.isMaster) {
_.forEach(cluster.workers, worker => worker.on('message', () => strapi.emit('restart:done')));
}
// Kill every worker processes.
_.forEach(cluster.workers, () => process.kill(process.pid, 'SIGHUP'));
}); });
// Reloading the router. if (cluster.isMaster) {
strapi.hooks.router.reload(); _.forEach(cluster.workers, worker => worker.on('message', () => strapi.emit('restart:done')));
}
// Kill every worker processes.
_.forEach(cluster.workers, () => process.kill(process.pid, 'SIGHUP'));
}); });
// Reloading the ORM. // Reloading the router.
strapi.hooks.waterline.reload(); strapi.hooks.router.reload();
});
strapi.after('hook:waterline:installed', () => {
installed();
}); });
strapi.after('hook:views:installed', () => { strapi.after('hook:views:installed', () => {

View File

@ -22,7 +22,6 @@
"security", "security",
"socket.io", "socket.io",
"sockets", "sockets",
"waterline",
"websockets" "websockets"
], ],
"directories": { "directories": {
@ -67,9 +66,9 @@
"node-schedule": "~0.6.0", "node-schedule": "~0.6.0",
"prompt": "~0.2.14", "prompt": "~0.2.14",
"request": "~2.67.0", "request": "~2.67.0",
"sails-disk": "~0.10.8",
"socket.io": "~1.3.7", "socket.io": "~1.3.7",
"socket.io-client": "~1.3.7", "socket.io-client": "~1.3.7",
"strapi-bookshelf": "~1.5.0",
"strapi-generate": "~1.5.0", "strapi-generate": "~1.5.0",
"strapi-generate-admin": "~1.5.0", "strapi-generate-admin": "~1.5.0",
"strapi-generate-api": "~1.5.0", "strapi-generate-api": "~1.5.0",
@ -78,8 +77,6 @@
"strapi-generate-upload": "~1.5.0", "strapi-generate-upload": "~1.5.0",
"strapi-generate-users": "~1.5.0", "strapi-generate-users": "~1.5.0",
"unzip2": "~0.2.5", "unzip2": "~0.2.5",
"waterline": "~0.10.28",
"waterline-graphql": "~1.1.0",
"winston": "~2.1.1" "winston": "~2.1.1"
}, },
"devDependencies": { "devDependencies": {
@ -167,8 +164,8 @@
"url": "https://github.com/wistityhq/strapi/issues" "url": "https://github.com/wistityhq/strapi/issues"
}, },
"engines": { "engines": {
"node": ">= 0.12.0", "node": ">= 4.0.0",
"npm": ">= 2.0.0" "npm": ">= 3.0.0"
}, },
"preferGlobal": true, "preferGlobal": true,
"license": "MIT" "license": "MIT"