diff --git a/lib/configuration/hooks/_api/index.js b/lib/configuration/hooks/_api/index.js index dd39f39411..f529640e0b 100644 --- a/lib/configuration/hooks/_api/index.js +++ b/lib/configuration/hooks/_api/index.js @@ -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'); + } + }); } }; diff --git a/lib/configuration/hooks/_config/index.js b/lib/configuration/hooks/_config/index.js index e6514481da..d511748429 100644 --- a/lib/configuration/hooks/_config/index.js +++ b/lib/configuration/hooks/_config/index.js @@ -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'); + } + }); } }; diff --git a/lib/configuration/hooks/router/index.js b/lib/configuration/hooks/router/index.js index 790fc4bf7e..6a9d3c26bb 100644 --- a/lib/configuration/hooks/router/index.js +++ b/lib/configuration/hooks/router/index.js @@ -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'); + } + }); } }; diff --git a/lib/configuration/hooks/studio/index.js b/lib/configuration/hooks/studio/index.js index ce7f52a3a5..1bfd102d08 100644 --- a/lib/configuration/hooks/studio/index.js +++ b/lib/configuration/hooks/studio/index.js @@ -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); + }); + }); }, /** diff --git a/lib/configuration/hooks/waterline/index.js b/lib/configuration/hooks/waterline/index.js index 01f1a77450..00ada60b00 100644 --- a/lib/configuration/hooks/waterline/index.js +++ b/lib/configuration/hooks/waterline/index.js @@ -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); } }; diff --git a/lib/private/initialize.js b/lib/private/initialize.js index d5442bab6c..cbb31bbfc7 100755 --- a/lib/private/initialize.js +++ b/lib/private/initialize.js @@ -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'); + } }); } diff --git a/lib/private/loadHooks.js b/lib/private/loadHooks.js index 477273250c..1eb9522c0c 100755 --- a/lib/private/loadHooks.js +++ b/lib/private/loadHooks.js @@ -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); } }, diff --git a/lib/restart.js b/lib/restart.js index e10d4de529..fd195c95a7 100644 --- a/lib/restart.js +++ b/lib/restart.js @@ -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(); }); }; diff --git a/package.json b/package.json index 03987085e2..9bf8f27698 100644 --- a/package.json +++ b/package.json @@ -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, diff --git a/test/application/toJSON.js b/test/application/toJSON.js index a9e7511d72..9103b2b227 100755 --- a/test/application/toJSON.js +++ b/test/application/toJSON.js @@ -9,6 +9,7 @@ describe('app.toJSON()', function () { obj.should.eql({ subdomainOffset: 2, + proxy: false, env: 'test' }); }); diff --git a/test/response/type.js b/test/response/type.js index ff7aa89ade..7d4147a7b0 100755 --- a/test/response/type.js +++ b/test/response/type.js @@ -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 () {