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)
@ -55,7 +61,6 @@ This will generate a Strapi application without:
- the built-in `user`, `email` and `upload` APIs,
- the `grant` hook,
- the open-source admin panel,
- the Waterline ORM (`waterline` and `blueprints` hooks 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.

View File

@ -39,6 +39,10 @@ cmd = program.command('version');
cmd.description('output your version of Strapi');
cmd.action(program.versionInformation);
/**
* Basic commands
*/
// `$ strapi new <name>`
cmd = program.command('new');
cmd.unknownOption = NOOP;
@ -64,24 +68,16 @@ cmd.unknownOption = NOOP;
cmd.description('open the Strapi framework console');
cmd.action(require('./strapi-console'));
/**
* Commands for the Strapi Studio
*/
// `$ strapi link`
cmd = program.command('link');
cmd.unknownOption = NOOP;
cmd.description('link an existing application to the Strapi Studio');
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`
cmd = program.command('login');
cmd.unknownOption = NOOP;
@ -94,6 +90,22 @@ cmd.unknownOption = NOOP;
cmd.description('logout your account from the Strapi Studio');
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
*/

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,
_api: true,
responseTime: true,
waterline: true,
bodyParser: true,
session: true,
grant: true,
@ -22,7 +21,6 @@ module.exports = {
i18n: true,
cron: true,
logger: true,
blueprints: true,
views: true,
router: 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.
strapi.app.use(strapi.router.routes());
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.
if (strapi.config.dry) {
delete hooks.blueprints;
delete hooks.grant;
delete hooks.studio;
delete hooks.waterline;
}
// Handle folder-defined modules (default to `./lib/index.js`)
@ -130,7 +128,7 @@ module.exports = function (strapi) {
// Prepare all other hooks.
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);
process.nextTick(cb);
}, cb);
@ -138,7 +136,7 @@ module.exports = function (strapi) {
// Apply the default config for all other hooks.
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];
applyDefaults(hook);
process.nextTick(cb);
@ -147,7 +145,7 @@ module.exports = function (strapi) {
// Load all other hooks.
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);
}, cb);
},
@ -160,16 +158,6 @@ module.exports = function (strapi) {
prepareHook('router');
applyDefaults(hooks.router);
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
if (cluster.isMaster) {
strapi.hooks.waterline.installation();
++count;
@ -72,44 +71,34 @@ module.exports = cb => {
console.log();
}
// Teardown Waterline adapters and
// reload the Waterline ORM.
strapi.after('hook:waterline:reloaded', () => {
strapi.after('hook:router:reloaded', () => {
process.nextTick(() => cb());
// Reload the router.
strapi.after('hook:router:reloaded', () => {
process.nextTick(() => cb());
// Update `strapi` status.
strapi.reloaded = true;
strapi.reloading = false;
// Update `strapi` status.
strapi.reloaded = true;
strapi.reloading = false;
// Finally inform the developer everything seems ok.
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.warn('You still need to restart your server to fully enjoy changes...');
}
// Finally inform the developer everything seems ok.
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.warn('You still need to restart your server to fully enjoy changes...');
}
strapi.once('restart:done', function () {
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'));
strapi.once('restart:done', function () {
strapi.log.info('Application successfully restarted');
});
// Reloading the router.
strapi.hooks.router.reload();
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 ORM.
strapi.hooks.waterline.reload();
});
strapi.after('hook:waterline:installed', () => {
installed();
// Reloading the router.
strapi.hooks.router.reload();
});
strapi.after('hook:views:installed', () => {

View File

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