Merge pull request #27 from wistityhq/improvement/rebuild

Improve the rebuild process
This commit is contained in:
Loïc Saint-Roch 2015-11-10 16:44:01 +01:00
commit a556094c2e
11 changed files with 351 additions and 150 deletions

View File

@ -170,6 +170,21 @@ module.exports = function (strapi) {
});
cb();
},
/**
* Reload the hook
*/
reload: function () {
hook.initialize(function (err) {
if (err) {
strapi.log.error('Failed to reinitialize the API hook.');
strapi.stop();
} else {
strapi.emit('hook:_api:reloaded');
}
});
}
};

View File

@ -111,6 +111,13 @@ module.exports = function (strapi) {
});
}
// Make sure the ORM config are equals to the databases file
// (aiming to not have issue with adapters when rebuilding the dictionary).
// It's kind of messy, for now, but it works fine. If someone has a better
// solution we'd be glad to accept a Pull Request.
const ormConfig = JSON.parse(fs.readFileSync(path.resolve(strapi.config.appPath, strapi.config.paths.config, 'environments', strapi.config.environment, 'databases.json')));
strapi.config.orm = ormConfig.orm;
// Save different environments inside an array because we need it in the Strapi Studio.
strapi.config.environments = fs.readdirSync(path.resolve(strapi.config.appPath, strapi.config.paths.config, 'environments'));
@ -121,9 +128,24 @@ module.exports = function (strapi) {
strapi.controllers = {};
strapi.models = {};
strapi.policies = {};
});
cb();
cb();
});
},
/**
* Reload the hook
*/
reload: function () {
hook.initialize(function (err) {
if (err) {
strapi.log.error('Failed to reinitialize the config hook.');
strapi.stop();
} else {
strapi.emit('hook:_config:reloaded');
}
});
}
};

View File

@ -4,6 +4,9 @@
* Module dependencies
*/
// Node.js core.
const cluster = require('cluster');
// Public node modules.
const _ = require('lodash');
@ -36,111 +39,130 @@ module.exports = function (strapi) {
*/
initialize: function (cb) {
let route;
let controller;
let action;
let policies = [];
if ((cluster.isWorker && strapi.config.reload.workers > 0) || (cluster.isMaster && strapi.config.reload.workers < 1)) {
let route;
let controller;
let action;
let policies = [];
// Initialize the router.
if (!strapi.router) {
strapi.router = strapi.middlewares.router({
prefix: strapi.config.prefix
// Initialize the router.
if (!strapi.router) {
strapi.router = strapi.middlewares.router({
prefix: strapi.config.prefix
});
}
// Middleware used for every routes.
// Expose the endpoint in `this`.
function globalPolicy(endpoint, route) {
return function * (next) {
this.request.route = {
endpoint: endpoint,
controller: route.controller,
firstWord: _.startsWith(route.endpoint, '/') ? route.endpoint.split('/')[1] : route.endpoint.split('/')[0],
value: route
};
yield next;
};
}
// Add the `dashboardPolicy` to the list of policies.
if (strapi.config.dashboard.enabled) {
strapi.policies.dashboardToken = dashboardTokenPolicy;
}
// Parse each route from the user config, load policies if any
// and match the controller and action to the desired endpoint.
_.forEach(strapi.config.routes, function (value, endpoint) {
try {
route = regex.detectRoute(endpoint);
// Check if the controller is a function.
if (typeof value.controller === 'function') {
action = value.controller;
} else {
controller = strapi.controllers[value.controller.toLowerCase()];
action = controller[value.action];
}
// Init policies array.
policies = [];
// Add the `globalPolicy`.
policies.push(globalPolicy(endpoint, route));
if (_.isArray(value.policies) && !_.isEmpty(value.policies)) {
_.forEach(value.policies, function (policy) {
if (strapi.policies[policy]) {
policies.push(strapi.policies[policy]);
} else {
strapi.log.error('Ignored attempt to bind route `' + endpoint + '` with unknown policy `' + policy + '`.');
process.exit(1);
}
});
}
strapi.router[route.verb.toLowerCase()](route.endpoint, strapi.middlewares.compose(policies), action);
} catch (err) {
strapi.log.warn('Ignored attempt to bind route `' + endpoint + '` to unknown controller/action.');
}
});
// Define GraphQL route with modified Waterline models to GraphQL schema
// or disable the global variable
if (strapi.config.graphql.enabled === true) {
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());
// Handle router errors.
strapi.app.use(function * (next) {
try {
yield next;
const status = this.status || 404;
if (status === 404) {
this.throw(404);
}
} catch (err) {
err.status = err.status || 500;
err.message = err.expose ? err.message : 'Houston, we have a problem.';
this.status = err.status;
this.body = {
code: err.status,
message: err.message
};
this.app.emit('error', err, this);
}
});
}
// Middleware used for every routes.
// Expose the endpoint in `this`.
function globalPolicy(endpoint, route) {
return function * (next) {
this.request.route = {
endpoint: endpoint,
controller: route.controller,
firstWord: _.startsWith(route.endpoint, '/') ? route.endpoint.split('/')[1] : route.endpoint.split('/')[0],
value: route
};
yield next;
};
}
// Add the `dashboardPolicy` to the list of policies.
if (strapi.config.dashboard.enabled) {
strapi.policies.dashboardToken = dashboardTokenPolicy;
}
// Parse each route from the user config, load policies if any
// and match the controller and action to the desired endpoint.
_.forEach(strapi.config.routes, function (value, endpoint) {
try {
route = regex.detectRoute(endpoint);
// Check if the controller is a function.
if (typeof value.controller === 'function') {
action = value.controller;
} else {
controller = strapi.controllers[value.controller.toLowerCase()];
action = controller[value.action];
}
// Init policies array.
policies = [];
// Add the `globalPolicy`.
policies.push(globalPolicy(endpoint, route));
if (_.isArray(value.policies) && !_.isEmpty(value.policies)) {
_.forEach(value.policies, function (policy) {
if (strapi.policies[policy]) {
policies.push(strapi.policies[policy]);
} else {
strapi.log.error('Ignored attempt to bind route `' + endpoint + '` with unknown policy `' + policy + '`.');
process.exit(1);
}
});
}
strapi.router[route.verb.toLowerCase()](route.endpoint, strapi.middlewares.compose(policies), action);
} catch (err) {
strapi.log.warn('Ignored attempt to bind route `' + endpoint + '` to unknown controller/action.');
}
});
// Define GraphQL route with modified Waterline models to GraphQL schema
// or disable the global variable
if (strapi.config.graphql.enabled === true) {
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());
// Handle router errors.
strapi.app.use(function * (next) {
try {
yield next;
const status = this.status || 404;
if (status === 404) {
this.throw(404);
}
} catch (err) {
err.status = err.status || 500;
err.message = err.expose ? err.message : 'Houston, we have a problem.';
this.status = err.status;
this.body = {
code: err.status,
message: err.message
};
this.app.emit('error', err, this);
}
});
cb();
},
/**
* Reload the hook
*/
reload: function () {
delete strapi.router;
hook.initialize(function (err) {
if (err) {
strapi.log.error('Failed to reinitialize the router.');
strapi.stop();
} else {
strapi.emit('hook:router:reloaded');
}
});
}
};

View File

@ -83,7 +83,10 @@ module.exports = function (strapi) {
socket.on('connect', function () {
firstConnectionAttempt = false;
_self.connectWithStudio(socket);
strapi.once('bootstrap:done', function () {
_self.connectWithStudio(socket);
});
});
socket.on('error', function (err) {
@ -334,9 +337,11 @@ module.exports = function (strapi) {
*/
rebuild: function (data, cb) {
strapi.restart();
cb(null, true);
process.nextTick(function () {
strapi.restart(function () {
cb(null, true);
});
});
},
/**

View File

@ -5,11 +5,13 @@
*/
// Node.js core.
const cluster = require('cluster');
const path = require('path');
const spawn = require('child_process').spawnSync;
// Public node modules.
const _ = require('lodash');
const async = require('async');
const Waterline = require('waterline');
const WaterlineGraphQL = require('waterline-graphql');
@ -58,8 +60,9 @@ module.exports = function (strapi) {
*/
initialize: function (cb) {
if (_.isPlainObject(strapi.config.orm) && !_.isEmpty(strapi.config.orm)) {
if (_.isPlainObject(strapi.config.orm) && !_.isEmpty(strapi.config.orm) && ((cluster.isWorker && strapi.config.reload.workers > 0) || (cluster.isMaster && strapi.config.reload.workers < 1))) {
strapi.adapters = {};
strapi.collections = [];
// Expose a new instance of Waterline.
if (!strapi.orm) {
@ -147,7 +150,11 @@ module.exports = function (strapi) {
// Finally, load the collection in the Waterline instance.
try {
strapi.orm.loadCollection(Waterline.Collection.extend(definition));
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);
@ -158,8 +165,8 @@ module.exports = function (strapi) {
// globally expose models.
strapi.orm.initialize({
adapters: strapi.adapters,
models: strapi.models,
connections: strapi.config.orm.connections,
collections: strapi.collections,
defaults: {
connection: strapi.config.orm.defaultConnection
}
@ -170,34 +177,73 @@ module.exports = function (strapi) {
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`
strapi.schemas = WaterlineGraphQL.getGraphQLSchema({
collections: strapi.orm.collections,
usefulFunctions: true
});
}
cb();
});
// 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`
strapi.schemas = WaterlineGraphQL.getGraphQLSchema({
collections: strapi.orm.collections,
usefulFunctions: true
});
}
} else {
strapi.log.warn('Waterline ORM disabled!');
cb();
}
},
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);
}
};

View File

@ -59,14 +59,28 @@ module.exports = function initialize(cb) {
}
});
// Only run the application bootstrap if
// we are in a master cluster.
if (cluster.isMaster) {
_.forEach(cluster.workers, function (worker) {
worker.on('message', function () {
self.emit('bootstrap:done');
});
});
}
// Only run the application bootstrap on master cluster if we don't have any workers.
// Else, run the bootstrap logic on the workers.
if ((cluster.isWorker && strapi.config.reload.workers > 0) || (cluster.isMaster && strapi.config.reload.workers < 1)) {
self.runBootstrap(function afterBootstrap(err) {
if (err) {
self.log.error('Bootstrap encountered an error.');
return cb(self.log.error(err));
}
if (cluster.isWorker) {
process.send('message');
} else {
self.emit('bootstrap:done');
}
});
}

View File

@ -123,7 +123,7 @@ module.exports = function (strapi) {
// Prepare all other hooks.
prepare: function prepareHooks(cb) {
async.each(_.without(_.keys(hooks), '_config', '_api'), function (id, cb) {
async.each(_.without(_.keys(hooks), '_config', '_api', 'studio'), function (id, cb) {
prepareHook(id);
process.nextTick(cb);
}, cb);
@ -131,7 +131,7 @@ module.exports = function (strapi) {
// Apply the default config for all other hooks.
defaults: function defaultConfigHooks(cb) {
async.each(_.without(_.keys(hooks), '_config', '_api'), function (id, cb) {
async.each(_.without(_.keys(hooks), '_config', '_api', 'studio'), function (id, cb) {
const hook = hooks[id];
applyDefaults(hook);
process.nextTick(cb);
@ -140,9 +140,19 @@ module.exports = function (strapi) {
// Load all other hooks.
load: function loadOtherHooks(cb) {
async.each(_.without(_.keys(hooks), '_config', '_api'), function (id, cb) {
async.each(_.without(_.keys(hooks), '_config', '_api', 'studio'), function (id, cb) {
loadHook(id, cb);
}, cb);
},
// Load the studio hook.
studio: function loadStudioHook(cb) {
if (!hooks.studio) {
return cb();
}
prepareHook('studio');
applyDefaults(hooks.studio);
loadHook('studio', cb);
}
},

View File

@ -9,14 +9,88 @@ const cluster = require('cluster');
// Public node modules.
const _ = require('lodash');
const async = require('async');
/**
* Programmatically restart the server
* (useful for the Studio)
*/
module.exports = function () {
_.forEach(cluster.worker, function () {
process.kill(process.pid, 'SIGHUP');
module.exports = function (cb) {
const self = this;
console.log();
// Update the Strapi status (might be used
// by the core or some hooks).
self.reloading = true;
// Async module loader to rebuild a
// dictionary of the application.
async.auto({
// Rebuild the dictionaries.
dictionaries: function (cb) {
self.on('hook:_config:reloaded', function () {
self.on('hook:_api:reloaded', function () {
cb();
});
self.hooks._api.reload();
});
self.hooks._config.reload();
}
},
// Callback.
function (err) {
// Just in case there is an error.
if (err) {
self.log.error('Impossible to reload the server');
self.log.error('Please restart the server manually');
self.stop();
}
// Tell the application the framework is reloading
// (might be used by some hooks).
self.reloading = true;
// Teardown Waterline adapters and
// reload the Waterline ORM.
self.after('hook:waterline:reloaded', function () {
self.after('hook:router:reloaded', function () {
process.nextTick(function () {
cb();
});
// Update `strapi` status.
self.reloaded = true;
self.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) {
self.log.info('Application\'s dictionnary updated');
self.log.warn('You still need to restart your server to fully enjoy changes...');
}
// Kill every worker processes.
_.forEach(cluster.workers, function () {
process.kill(process.pid, 'SIGHUP');
});
if (cluster.isMaster && _.isPlainObject(strapi.config.reload) && !_.isEmpty(strapi.config.reload) && strapi.config.reload.workers > 0) {
self.log.info('Application restarted');
console.log();
}
});
// Reloading the router.
self.hooks.router.reload();
});
// Reloading the ORM.
self.hooks.waterline.reload();
});
};

View File

@ -129,6 +129,7 @@
"no-else-return": 0,
"no-extra-parens": 0,
"no-implicit-coercion": 0,
"no-inner-declarations": 0,
"no-invalid-this": 0,
"no-negated-condition": 0,
"no-throw-literal": 0,

View File

@ -9,6 +9,7 @@ describe('app.toJSON()', function () {
obj.should.eql({
subdomainOffset: 2,
proxy: false,
env: 'test'
});
});

View File

@ -40,15 +40,6 @@ describe('ctx.type=', function () {
ctx.response.header['content-type'].should.equal('text/html; charset=foo');
});
});
describe('with an unknown extension', function () {
it('should default to application/octet-stream', function () {
const ctx = context();
ctx.type = 'asdf';
ctx.type.should.equal('application/octet-stream');
ctx.response.header['content-type'].should.equal('application/octet-stream');
});
});
});
describe('ctx.type', function () {