diff --git a/.npmignore b/.npmignore index f81eb0f..048c6dd 100644 --- a/.npmignore +++ b/.npmignore @@ -4,6 +4,8 @@ **/*.DS_store coverage/ +examples/ +test/ ideas manual.txt diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3d90780 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 ifnode + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/core/Application.T.js b/core/Application.T.js index 4153123..b5544a9 100644 --- a/core/Application.T.js +++ b/core/Application.T.js @@ -1,8 +1,10 @@ /** * @typedef {Object} ApplicationOptions * - * @property {string} [app_config.alias] - * @property {string} [app_config.project_folder] - * @property {string} [app_config.projectFolder] - * @property {string} [app_config.environment] + * @property {string} [alias] + * @property {string} [project_folder] + * @property {string} [projectFolder] + * @property {string} [env] + * @property {string} [environment] + * @property {object|string} [configuration] */ diff --git a/core/Application.js b/core/Application.js index 39ccf72..19b5bf7 100644 --- a/core/Application.js +++ b/core/Application.js @@ -1,5 +1,6 @@ 'use strict'; +var _defaults = require('lodash/defaults'); var Path = require('path'); var Util = require('util'); var Express = require('express'); @@ -9,7 +10,7 @@ var Diread = require('diread'); var toArray = require('./helper/toArray'); var deepFreeze = require('./helper/deepFreeze'); var pathWithoutExtension = require('./helper/pathWithoutExtension'); -var tryCatch = require('./helper/tryCatch'); +var requireWithSkippingOfMissedModuleError = require('./helper/requireWithSkippingOfMissedModuleError'); var debug = require('debug')('ifnode:application'); // eslint-disable-line var Log = require('./Log'); @@ -53,7 +54,6 @@ function Application(options) { IFNodeVirtualSchema ]; this._models_builder = null; - this._components_builder = null; this._controllers_builder = null; this.id = UUID.v4(); @@ -61,9 +61,9 @@ function Application(options) { this.project_folder = this.projectFolder = this._project_folder; this.backend_folder = this.backendFolder = this._backend_folder; - this.config = this._initialize_config(options.env || options.environment); + this.config = this._initialize_config(options); deepFreeze(this.config); - + this.listener = this._initialize_listener(); this.connection = this._initialize_connection_server(); @@ -75,6 +75,7 @@ function Application(options) { this.models = {}; this.components = {}; + this._components_builder = new ComponentsBuilder(this.components, this.config.components); this.controllers = {}; } @@ -124,20 +125,42 @@ Application.prototype.register = function(module) { return this; }; +/** + * @typedef {Object} ModuleLoadOptions + * + * @property {RegExp} [include] + * @property {RegExp} [exclude] + */ + +/** + * @typedef {Object} ApplicationLoadOptions + * + * @property {boolean|ModuleLoadOptions} [controllers] + */ + /** * Loads all maintenance parts of application * + * @param {ApplicationLoadOptions} [options] * @returns {Application} */ -Application.prototype.load = function() { +Application.prototype.load = function(options) { + options = _defaults(options, { + controllers: true + }); + this.models = this._initialize_models(); Object.freeze(this.models); - this.components = this._initialize_components(); + this._initialize_components(); // Object.freeze(this.components); - this.controllers = this._initialize_controllers(); - Object.freeze(this.controllers); + if(options.controllers) { + this.controllers = options.controllers === true? + this._initialize_controllers() : + this._initialize_controllers(options.controllers); + Object.freeze(this.controllers); + } this._is_loaded = true; @@ -151,7 +174,7 @@ Application.prototype.load = function() { */ Application.prototype.extension = function(id) { var cache = this._extensions_cache; - + if(!(id in cache)) { var custom_folder = this.config.application.folders.extensions; var custom_full_path = Path.resolve(this._project_folder, custom_folder); @@ -173,10 +196,40 @@ Application.prototype.ext = Util.deprecate( 'Deprecated from 2.0.0 version. Needs to use app.extension() method' ); +/** + * + * @template T + * @param {string|Function} instance + * @returns {T} + */ +Application.prototype.inject = function(instance) { + if (typeof instance === 'string') { + return this.component(instance); + } else { + var name = instance.name; + + if(name in this.components) { + return this.components[name]; + } + + var components_configs = this.config.components; + var component = new instance({ + name: name, + config: (components_configs && components_configs[name]) || {} + }, this); + + var components_builder = this._components_builder; + components_builder.save_component(component, name); + components_builder.components_compiled[name] = true; + + return component; + } +} + /** * * @param {string} id - * @returns {Object} + * @returns {Component} */ Application.prototype.component = function(id) { if(id in this.components) { @@ -184,14 +237,21 @@ Application.prototype.component = function(id) { } var full_path = Path.resolve(this.config.application.folders.components, id); + var components_configs = this.config.components; + var components_builder = this._components_builder; + var component = components_builder.read_and_build_component(full_path, { + name: id, + config: (components_configs && components_configs[id]) || {} + }); - if(!(full_path in this.components)) { - this.components[full_path] = this._components_builder.read_and_build_component(full_path, { - name: id - }); + components_builder.compile(this, full_path); + + if (!this.components[id]) { + components_builder.save_component(component, id); + components_builder.components_compiled[id] = true; } - return this.components[full_path]; + return component; }; /** @@ -214,7 +274,7 @@ Application.prototype.Component = function(custom_component_config) { var builder = this._components_builder; return builder.make( - builder.build_component_config(custom_component_config, this.config.components) + builder.build_component_config(custom_component_config) ); }; @@ -263,21 +323,26 @@ Application.prototype.down = Util.deprecate(function(callback) { /** * Initialize application instance configuration * - * @param {string} environment * @private + * @param {ApplicationOptions} options */ -Application.prototype._initialize_config = function(environment) { - var config_path; +Application.prototype._initialize_config = function(options) { + var environment = options.env || options.environment; + var configuration = options.configuration; + var custom_configuration = null; if(environment) { - config_path = Path.resolve(this._project_folder, 'config/', environment); + custom_configuration = require(Path.resolve(this._project_folder, 'config/', environment)); + } else if (configuration) { + custom_configuration = typeof configuration === 'string' ? + require(Path.resolve(this._project_folder, configuration)) : + configuration; } return ConfigurationBuilder({ - environment: environment, project_folder: this._project_folder, backend_folder: this._backend_folder, - config_path: config_path + custom_configuration: custom_configuration }); }; @@ -374,7 +439,8 @@ Application.prototype._initialize_models = function _initialize_models() { * @private */ Application.prototype._initialize_components = function _initialize_components() { - var components_builder = this._components_builder = new ComponentsBuilder; + var self = this; + var components_builder = this._components_builder; var Component = this.Component.bind(this); var modules = this._modules; @@ -382,12 +448,16 @@ Application.prototype._initialize_components = function _initialize_components() var module = modules[i][PLUGIN_TYPES.COMPONENT]; if(module) { - module(this, Component); + var component = module(this, Component); + + if(component) { + components_builder.build_component(component, {}); + } + + components_builder.compile(this); } } - var components_config = this.config.components; - Diread({ src: this.config.application.folders.components, directories: true, @@ -402,8 +472,9 @@ Application.prototype._initialize_components = function _initialize_components() try { components_builder.read_and_build_component( component_path, - components_builder.build_component_config({}, components_config) + components_builder.build_component_config() ); + components_builder.compile(self, component_path); } catch(error) { /** * Errors inside component will not catch by this handle @@ -418,16 +489,15 @@ Application.prototype._initialize_components = function _initialize_components() } } }); - - return components_builder.compile(this); }; /** * * @private + * @param {ModuleLoadOptions} [options] */ -Application.prototype._initialize_controllers = function _initialize_controllers() { - var controllers_builder = this._controllers_builder = new ControllersBuilder(); +Application.prototype._initialize_controllers = function _initialize_controllers(options) { + var controllers_builder = this._controllers_builder = new ControllersBuilder(options); var modules = this._modules; for(var i = 0; i < modules.length; ++i) { @@ -454,9 +524,7 @@ Application.prototype._initialize_controllers = function _initialize_controllers Application.prototype._require_module = function(module_name) { var Module; - Module = tryCatch(function() { - return require(module_name); - }); + Module = requireWithSkippingOfMissedModuleError(module_name); if(Module) { return Module; @@ -464,9 +532,13 @@ Application.prototype._require_module = function(module_name) { var self = this; - Module = tryCatch(function() { - return self.extension(module_name); - }); + try { + Module = this.extension(module_name); + } catch (error) { + if (!/Cannot\sfind\sextension/.test(error.message)) { + throw error; + } + } if(Module) { return Module; diff --git a/core/Component.js b/core/Component.js index 2f312f1..55c6a28 100644 --- a/core/Component.js +++ b/core/Component.js @@ -1,16 +1,26 @@ 'use strict'; var UUID = require('uuid'); +var _isInheritsFrom = require('./helper/isInheritsFrom'); +var isIFNodeItem = require('./helper/isIFNodeItem'); +var isIFNodeItemInstance = require('./helper/isIFNodeItemInstance'); var toArray = require('./helper/toArray'); +var PLUGIN_TYPES = require('./PLUGIN_TYPES'); + +/** + * + * @typedef {Object} ComponentOptions + * + * @property {string} [name] + * @property {Array.} [alias=[]] + * @property {Object} [config={}] + */ /** * * @class Component * - * @param {Object} [options={}] - * @param {string} [options.name] - * @param {Array.} [options.alias=[]] - * @param {Object} [options.config={}] + * @param {ComponentOptions} [options={}] */ function Component(options) { options = options || {}; @@ -21,4 +31,32 @@ function Component(options) { this.alias = toArray(options.alias); } +Object.defineProperties(Component, { + __IFNODE_ITEM: { + value: PLUGIN_TYPES.COMPONENT + }, + + isInstanceOf: { + /** + * + * @param {*} object + * @returns {boolean} + */ + value: function isInstanceOf(object) { + return !!object && (object instanceof this || isIFNodeItemInstance(object, this.__IFNODE_ITEM)); + } + }, + + isInheritsFrom: { + /** + * + * @param {*} Base + * @returns {boolean} + */ + value: function isInheritsFrom(Base) { + return _isInheritsFrom(Base, this) || isIFNodeItem(Base, this.__IFNODE_ITEM); + } + } +}); + module.exports = Component; diff --git a/core/ConfigurationBuilder.js b/core/ConfigurationBuilder.js index 8a35617..b92bbc8 100644 --- a/core/ConfigurationBuilder.js +++ b/core/ConfigurationBuilder.js @@ -6,6 +6,7 @@ var _includes = require('lodash/includes'); // var debug = require('debug')('ifnode:config'); var path = require('path'); +var _cloneDeepPrimitives = require('./helper/cloneDeepPrimitives'); /** * @@ -286,13 +287,12 @@ function initialize_default_config(options) { function ConfigurationBuilder(options) { var default_config = initialize_default_config(options); - if(!options.config_path) { + if(!options.custom_configuration) { initialize_additional_site_config(default_config); return default_config; } - var config = require(options.config_path); - // var config = Object.create(ConfigPrototype); + var config = _cloneDeepPrimitives(options.custom_configuration); initialize_properties_config(config, default_config, options.project_folder); initialize_site_config(config, default_config, options.project_folder); diff --git a/core/Controller.js b/core/Controller.js index 3c739e3..dbfb42d 100644 --- a/core/Controller.js +++ b/core/Controller.js @@ -401,8 +401,17 @@ Controller.prototype._generate_url = function(method) { this.router[method](url, function(request, response, next_route) { eachSeries(callbacks, function(callback, next_callback, interrupt) { + /** + * + * @param {*|Error} error + */ + function unexpected_error_handler(error) { + interrupt(); + next_route(error); + } + try { - callback( + var result = callback( request, response, /** @@ -419,9 +428,23 @@ Controller.prototype._generate_url = function(method) { }, next_route ); - } catch(err) { - interrupt(); - next_route(err); + + if(result) { + var Class = result.constructor; + + /** + * Rough detection of Promise's instance + * + * @type {boolean} + */ + var is_promise = !!(Class.all && Class.race && result.then && result.catch); + + if(is_promise) { + result.catch(unexpected_error_handler); + } + } + } catch(error) { + unexpected_error_handler(error); } }, next_route); }); diff --git a/core/application/ComponentsBuilder.js b/core/application/ComponentsBuilder.js index 514bd1e..3ca2f16 100644 --- a/core/application/ComponentsBuilder.js +++ b/core/application/ComponentsBuilder.js @@ -2,7 +2,6 @@ var _defaults = require('lodash/defaults'); var Path = require('path'); -var isInheritsFrom = require('./../helper/isInheritsFrom'); var pathWithoutExtension = require('./../helper/pathWithoutExtension'); var Log = require('./../Log'); @@ -11,25 +10,30 @@ var Component = require('./../Component'); /** * * @class ComponentsBuilder + * + * @param {Object} components + * @param {Object} [components_configs] */ -function ComponentsBuilder() { +function ComponentsBuilder(components, components_configs) { /** * * @type {Object.} */ - this.components = {}; + this.components = components; + this.components_compiled = {}; + + this._components_configs = components_configs || {}; this._autoformed_config = null; } /** * - * @param {Object} custom_config - * @param {Object} [components_configs] + * @param {Object} [custom_config] * @returns {Object} */ -ComponentsBuilder.prototype.build_component_config = function build_component_config(custom_config, components_configs) { +ComponentsBuilder.prototype.build_component_config = function build_component_config(custom_config) { custom_config = _defaults(custom_config || {}, this._autoformed_config); - custom_config.config = (components_configs && components_configs[custom_config.name]) || {}; + custom_config.config = (this._components_configs[custom_config.name]) || {}; return custom_config; }; @@ -49,15 +53,23 @@ ComponentsBuilder.prototype.build_and_memorize_config = function build_and_memor return this._autoformed_config; }; + /** * * @param {string} component_path * @param {Object} component_config */ ComponentsBuilder.prototype.read_and_build_component = function read_and_build_component(component_path, component_config) { - var component = require(component_path); + return this.build_component(require(component_path), component_config); +}; - if(typeof component === 'function' && isInheritsFrom(component, Component)) { +/** + * + * @param {*} component + * @param {Object} component_config + */ +ComponentsBuilder.prototype.build_component = function build_component(component, component_config) { + if(typeof component === 'function' && Component.isInheritsFrom(component)) { var component_name = component_config.name; var saved_component = this.components[component_name]; @@ -69,46 +81,61 @@ ComponentsBuilder.prototype.read_and_build_component = function read_and_build_c return saved_component; } + component_config.name = component_config.name || component.name; + component_config.config = _defaults( + component_config.config || {}, + (this._components_configs[component_config.name]) + ); + component = new component(component_config); } - return component instanceof Component ? - this._save_component(component, component.name) : + return Component.isInstanceOf(component) ? + this.save_component(component, component.name) : component; }; /** * - * @param {Object} component_config + * @param {ComponentOptions} component_config * @returns {Component} */ ComponentsBuilder.prototype.make = function make(component_config) { - return this._save_component(new Component(component_config), component_config.name); + return this.save_component(new Component(component_config), component_config.name); }; /** * * @param {Application} app + * @param {string} [component_path] * @returns {Object.} */ -ComponentsBuilder.prototype.compile = function compile(app) { +ComponentsBuilder.prototype.compile = function compile(app, component_path) { var self = this; var components = this.components; + var components_compiled = this.components_compiled; Object.keys(components).forEach(function(unique_name) { + if (components_compiled[unique_name]) { + return; + } + if(unique_name in app) { Log.error('application', 'Alias [' + unique_name + '] already busy in application instance.'); } var component = components[unique_name]; - app[unique_name] = component; if(component.initialize) { component.initialize(component.config); } + components_compiled[unique_name] = true; + app[unique_name] = component; + component.alias.forEach(function(alias) { - self._save_component(component, alias); + self.save_component(component, alias); + components_compiled[alias] = true; if(alias in app) { Log.error('application', 'Alias [' + alias + '] already busy in application instance.'); @@ -116,6 +143,11 @@ ComponentsBuilder.prototype.compile = function compile(app) { app[alias] = component; }); + + if(component_path) { + self.save_component(component, component_path); + components_compiled[component_path] = true; + } }); return components; @@ -123,12 +155,11 @@ ComponentsBuilder.prototype.compile = function compile(app) { /** * - * @private * @param {Component} component * @param {string} key * @returns {Component} */ -ComponentsBuilder.prototype._save_component = function(component, key) { +ComponentsBuilder.prototype.save_component = function(component, key) { var saved_component = this.components[key]; if(!saved_component) { diff --git a/core/application/ControllersBuilder.js b/core/application/ControllersBuilder.js index a82f3d4..298b3f0 100644 --- a/core/application/ControllersBuilder.js +++ b/core/application/ControllersBuilder.js @@ -26,10 +26,15 @@ function is_directory(root_path, rest_path) { /** * * @class ControllersBuilder + * @param {ModuleLoadOptions} [options] */ -function ControllersBuilder() { +function ControllersBuilder(options) { this._controllers = {}; this._autoformed_config = null; + this._options = _defaults(options, { + include: null, + exclude: null + }); } ControllersBuilder.FIRST_LOADED_FILE = '!'; @@ -139,6 +144,7 @@ ControllersBuilder.prototype._read_controllers = function _read_controllers(main var Class = this.constructor; var FIRST_LOADED_FILE = Class.FIRST_LOADED_FILE; var LAST_LOADED_FILE = Class.LAST_LOADED_FILE; + var self = this; /** * @@ -162,12 +168,18 @@ ControllersBuilder.prototype._read_controllers = function _read_controllers(main list.forEach(function(file_name) { if(is_directory(directory_path, file_name)) { regularized.directories.push(file_name); - } else if(FIRST_LOADED_FILE === pathWithoutExtension(Path.basename(file_name))) { - regularized.start = file_name; - } else if(LAST_LOADED_FILE === pathWithoutExtension(Path.basename(file_name))) { - regularized.end = file_name; } else { - regularized.files.push(file_name); + if(Path.extname(file_name) !== '.js') { + return; + } + + if(FIRST_LOADED_FILE === pathWithoutExtension(Path.basename(file_name))) { + regularized.start = file_name; + } else if(LAST_LOADED_FILE === pathWithoutExtension(Path.basename(file_name))) { + regularized.end = file_name; + } else { + regularized.files.push(file_name); + } } }); @@ -181,6 +193,13 @@ ControllersBuilder.prototype._read_controllers = function _read_controllers(main * @param {function} finder */ function read_file(root_path, full_file_path, finder) { + if( + self._options.include && !self._options.include.test(full_file_path) || + self._options.exclude && self._options.exclude.test(full_file_path) + ) { + return; + } + finder(full_file_path, full_file_path.replace(root_path, '')); } diff --git a/core/application/DAOList.js b/core/application/DAOList.js index 385608b..ae8493a 100644 --- a/core/application/DAOList.js +++ b/core/application/DAOList.js @@ -64,7 +64,11 @@ DAOList.prototype._initialize_schemas = function _initialize_schemas(db) { } if(schema_driver.driver) { - var driver = schema_driver.driver(db_config.config); + var config = db_config.config; + var driver = schema_driver.driver(typeof config === 'function' ? + config.bind(db_config) : + config + ); if(driver) { schema_driver.fn._driver = driver; diff --git a/core/application/Extension.js b/core/application/Extension.js index 6405322..e2ceb06 100644 --- a/core/application/Extension.js +++ b/core/application/Extension.js @@ -1,6 +1,7 @@ 'use strict'; var Path = require('path'); +var requireWithSkippingOfMissedModuleError = require('./../helper/requireWithSkippingOfMissedModuleError'); var Log = require('./../Log'); /** @@ -20,16 +21,13 @@ function Extension(start_load_point) { */ Extension.prototype.require = function(id) { var extension_path = Path.resolve(this._start_load_point, id); + var extension = requireWithSkippingOfMissedModuleError(extension_path); - try { - return require(extension_path); - } catch(error) { - if(error.message.indexOf(extension_path) === -1) { - throw error; - } else { - Log.error('extensions', 'Cannot find extension by [' + id + '].'); - } + if(!extension) { + Log.error('extensions', 'Cannot find extension by [' + id + '].'); } + + return extension; }; module.exports = Extension; diff --git a/core/helper/cloneDeepPrimitives.js b/core/helper/cloneDeepPrimitives.js new file mode 100644 index 0000000..43dabbf --- /dev/null +++ b/core/helper/cloneDeepPrimitives.js @@ -0,0 +1,23 @@ +var _isPlainObject = require('lodash/isPlainObject'); + +/** + * + * @param {object} object + * @returns {object} + */ +function cloneDeepPrimitives(object) { + var cloned = {}; + var keys = Object.keys(object); + + keys.forEach(function(key) { + var value = object[key]; + + cloned[key] = _isPlainObject(value) ? + cloneDeepPrimitives(value) : + value; + }); + + return cloned; +} + +module.exports = cloneDeepPrimitives; diff --git a/core/helper/isIFNodeItem.js b/core/helper/isIFNodeItem.js new file mode 100644 index 0000000..2e22cf1 --- /dev/null +++ b/core/helper/isIFNodeItem.js @@ -0,0 +1,24 @@ +/** + * + * @param {Function} Base + * @param {string} item_type + * @returns {boolean} + */ +function isIFNodeItem(Base, item_type) { + if (Base.__IFNODE_ITEM === item_type) { + return true; + } + + /** + * Check of "util.inherits" classes + */ + for (var proto = Base.super_; proto; proto = proto.super_) { + if (proto.__IFNODE_ITEM === item_type) { + return true; + } + } + + return false; +} + +module.exports = isIFNodeItem; diff --git a/core/helper/isIFNodeItemInstance.js b/core/helper/isIFNodeItemInstance.js new file mode 100644 index 0000000..48a4473 --- /dev/null +++ b/core/helper/isIFNodeItemInstance.js @@ -0,0 +1,19 @@ +var isIFNodeItem = require('./isIFNodeItem'); + +/** + * + * @param {Object} object + * @param {string} item_type + * @returns {boolean} + */ +function isIFNodeItemInstance(object, item_type) { + for (var constructor = object.constructor; constructor !== Function; constructor = constructor.constructor) { + if (isIFNodeItem(constructor, item_type)) { + return true; + } + } + + return false; +} + +module.exports = isIFNodeItemInstance; diff --git a/core/helper/requireWithSkippingOfMissedModuleError.js b/core/helper/requireWithSkippingOfMissedModuleError.js new file mode 100644 index 0000000..32be692 --- /dev/null +++ b/core/helper/requireWithSkippingOfMissedModuleError.js @@ -0,0 +1,22 @@ +/** + * + * @param {function} path + * @returns {*} + * @throws {Error} + */ +function requireWithSkippingOfMissedModuleError(path) { + try { + return require(path); + } catch (error) { + if ( + error instanceof Error && + error.code === 'MODULE_NOT_FOUND' + ) { + return undefined; + } else { + throw error; + } + } +} + +module.exports = requireWithSkippingOfMissedModuleError; diff --git a/core/helper/tryCatch.js b/core/helper/tryCatch.js deleted file mode 100644 index 242bc5d..0000000 --- a/core/helper/tryCatch.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * - * @param {function} to_invoke - * @param {*} [default_result] - * @returns {*} - */ -function tryCatch(to_invoke, default_result) { - try { - return to_invoke(); - } catch (e) { - return default_result; - } -} - -module.exports = tryCatch; diff --git a/examples/components/protected/components/ClassES5.js b/examples/components/protected/components/ClassES5.js index c38d60f..f4b3652 100644 --- a/examples/components/protected/components/ClassES5.js +++ b/examples/components/protected/components/ClassES5.js @@ -1,7 +1,5 @@ 'use strict'; -var setPrototypeOf = require('setprototypeof'); - /** * * @class @@ -10,17 +8,21 @@ function ClassES5() { this._plain = 'ClassES5#plain'; } -var ClassES5Statics = { - STATIC: 'ClassES5.STATIC', - getStatic: function() { - return this.STATIC; +Object.defineProperties(ClassES5, { + STATIC: { + value: 'ClassES5.STATIC' + }, + getStatic: { + value: function() { + return this.STATIC; + } }, - getName: function() { - return this.name; + getName: { + value: function() { + return this.name; + } } -}; - -setPrototypeOf(ClassES5, ClassES5Statics); +}); ClassES5.prototype.plain = function() { return this._plain; diff --git a/examples/components/protected/components/ClassES5DoubleInherits.js b/examples/components/protected/components/ClassES5DoubleInherits.js new file mode 100644 index 0000000..bbdfa35 --- /dev/null +++ b/examples/components/protected/components/ClassES5DoubleInherits.js @@ -0,0 +1,21 @@ +'use strict'; +var Util = require('util'); +var ClassES5 = require('./ClassES5'); + +/** + * + * @class + */ +function ClassES5DoubleInherits() { + ClassES5.call(this); + + this._plain2 = 'ClassES5DoubleInherits#plain'; +} + +Util.inherits(ClassES5DoubleInherits, ClassES5); + +ClassES5DoubleInherits.prototype.plain2 = function() { + return this._plain2; +}; + +module.exports = ClassES5DoubleInherits; diff --git a/examples/config/config/local.js b/examples/config/config/local.js index 155991a..3fb6b3f 100644 --- a/examples/config/config/local.js +++ b/examples/config/config/local.js @@ -1,3 +1,10 @@ +/** + * + * @class + */ +function SomeClass() { +} + module.exports = { site: { local: { @@ -23,5 +30,10 @@ module.exports = { 'testable-middleware': function() { } } + }, + + some: { + function: SomeClass, + instance: new SomeClass } }; diff --git a/examples/controllers/config/controllers-partial.js b/examples/controllers/config/controllers-partial.js new file mode 100644 index 0000000..3b7c83c --- /dev/null +++ b/examples/controllers/config/controllers-partial.js @@ -0,0 +1,7 @@ +module.exports = { + application: { + folders: { + controllers: 'protected/controllers_partial/' + } + } +}; diff --git a/examples/controllers/config/controllers-with-promised-handlers.js b/examples/controllers/config/controllers-with-promised-handlers.js new file mode 100644 index 0000000..311c4ba --- /dev/null +++ b/examples/controllers/config/controllers-with-promised-handlers.js @@ -0,0 +1,7 @@ +module.exports = { + application: { + folders: { + controllers: 'protected/controllers_with_promised_handlers/' + } + } +}; diff --git a/examples/controllers/protected/controllers/.gitignore b/examples/controllers/protected/controllers/.gitignore new file mode 100644 index 0000000..7857f00 --- /dev/null +++ b/examples/controllers/protected/controllers/.gitignore @@ -0,0 +1 @@ +some-non-js.file diff --git a/examples/controllers/protected/controllers_partial/api/v1/skip.js b/examples/controllers/protected/controllers_partial/api/v1/skip.js new file mode 100644 index 0000000..3cec42a --- /dev/null +++ b/examples/controllers/protected/controllers_partial/api/v1/skip.js @@ -0,0 +1 @@ +throw new Error("Shouldn't be loaded"); diff --git a/examples/controllers/protected/controllers_partial/api/v1/user.js b/examples/controllers/protected/controllers_partial/api/v1/user.js new file mode 100644 index 0000000..8a1f8cf --- /dev/null +++ b/examples/controllers/protected/controllers_partial/api/v1/user.js @@ -0,0 +1,10 @@ +/** + * + * @type {Application} + */ +var app = require('./../../../../../../')('controllers-partial-loading'); +var router = app.Controller({ + root: '/api/v1/user' +}); + +router.end(); diff --git a/examples/controllers/protected/controllers_with_promised_handlers/~.js b/examples/controllers/protected/controllers_with_promised_handlers/~.js new file mode 100644 index 0000000..6697348 --- /dev/null +++ b/examples/controllers/protected/controllers_with_promised_handlers/~.js @@ -0,0 +1,34 @@ +var Promise = require('es6-promise'); +var app = require('../../../../')('controllers-with-promised-handlers'); +var controller = app.Controller({ + name: 'with_promised_handlers', + root: '/with-promised-handlers' +}); + +controller.get('/resolved-with-returning-non-promise', function(request, response) { + Promise.resolve('resolved').then(function(data) { + response.ok(data); + }); + + return 'uninfluenced-retured-data'; +}); + +controller.get('/resolved-with-returning-promise', function(request, response) { + return Promise.resolve('resolved').then(function(data) { + response.ok(data); + }); +}); + +controller.get('/rejected-caught-by-handler', function(request, response) { + return Promise.reject('rejected').catch(function(data) { + response.error(data); + }); +}); + +controller.get('/rejected-caught-by-ifnode', function() { + return Promise.reject('rejected'); +}); + +controller.error(function(error, request, response) { + response.error('default-error-handler:' + error) +}); diff --git a/examples/extensions/protected/extensions/with-syntax-error.js b/examples/extensions/protected/extensions/with-syntax-error.js new file mode 100644 index 0000000..06b7587 --- /dev/null +++ b/examples/extensions/protected/extensions/with-syntax-error.js @@ -0,0 +1 @@ +var 123some_syntax_error; diff --git a/examples/models_custom_schema/config/custom-schema.js b/examples/models_custom_schema/config/custom-schema.js index ef044eb..736dafd 100644 --- a/examples/models_custom_schema/config/custom-schema.js +++ b/examples/models_custom_schema/config/custom-schema.js @@ -2,6 +2,10 @@ module.exports = { db: { first_database: { schema: 'custom-schema', + correct_this_test: function correct_this_test() {}, + config: function config() { + return this.correct_this_test(); + }, default: true }, second_database: { diff --git a/examples/models_custom_schema/protected/extensions/custom-schema.js b/examples/models_custom_schema/protected/extensions/custom-schema.js index f71764d..473d32d 100644 --- a/examples/models_custom_schema/protected/extensions/custom-schema.js +++ b/examples/models_custom_schema/protected/extensions/custom-schema.js @@ -2,7 +2,8 @@ var SCHEMA = require('./../../../../core/PLUGIN_TYPES').SCHEMA; module.exports[SCHEMA] = function(app, CustomSchema) { CustomSchema.schema = 'custom-schema'; - CustomSchema.driver = function driver() { + CustomSchema.driver = function driver(config) { + config(); return {}; }; diff --git a/examples/plugins/config/plugins.js b/examples/plugins/config/plugins.js new file mode 100644 index 0000000..bb46a1a --- /dev/null +++ b/examples/plugins/config/plugins.js @@ -0,0 +1,10 @@ +module.exports = { + components: { + PluginComponent: { + key1: 'value1' + }, + PluginClassComponent: { + key2: 'value2' + } + } +}; diff --git a/examples/plugins/protected/components/ApplicationComponent.js b/examples/plugins/protected/components/ApplicationComponent.js new file mode 100644 index 0000000..a2695b8 --- /dev/null +++ b/examples/plugins/protected/components/ApplicationComponent.js @@ -0,0 +1,28 @@ +'use strict'; + +var Util = require('util'); +var Component = require('./../../../../core/Component'); +/** + * + * @type {Application} + */ +var app = require('./../../../..')('plugins'); +var PluginComponent = app.component('PluginComponent'); + +/** + * + * @class + * @extends Component + * + * @param options + */ +function ApplicationComponent(options) { + Component.call(this, options); +} +Util.inherits(ApplicationComponent, Component); + +ApplicationComponent.prototype.get_plugin_component = function() { + return PluginComponent; +}; + +module.exports = ApplicationComponent; diff --git a/examples/plugins/protected/extensions/internal-another-ifnode-component-class/Component.js b/examples/plugins/protected/extensions/internal-another-ifnode-component-class/Component.js new file mode 100644 index 0000000..41ecbc5 --- /dev/null +++ b/examples/plugins/protected/extensions/internal-another-ifnode-component-class/Component.js @@ -0,0 +1,26 @@ +'use strict'; + +var UUID = require('uuid'); +var toArray = require('./../../../../../core/helper/toArray'); +var PLUGIN_TYPES = require('./../../../../../core/PLUGIN_TYPES'); + +/** + * + * @class Component + * + * @param {ComponentOptions} options + */ +function Component(options) { + this.id = UUID.v4(); + this.name = options.name; + this.config = options.config; + this.alias = toArray(options.alias); +} + +Object.defineProperties(Component, { + __IFNODE_ITEM: { + value: PLUGIN_TYPES.COMPONENT + } +}); + +module.exports = Component; diff --git a/examples/plugins/protected/extensions/internal-another-ifnode-component-class/index.js b/examples/plugins/protected/extensions/internal-another-ifnode-component-class/index.js new file mode 100644 index 0000000..18703ec --- /dev/null +++ b/examples/plugins/protected/extensions/internal-another-ifnode-component-class/index.js @@ -0,0 +1,28 @@ +var _defaults = require('lodash/defaults'); +var Util = require('util'); +var PLUGIN_TYPES = require('./../../../../../core/PLUGIN_TYPES'); + +/** + * Copy of "core/Component". Simulates situation when component inherits another ifnode's Component class + * + * @class {Component} + */ +var Component = require('./Component'); + +module.exports[PLUGIN_TYPES.COMPONENT] = function() { + /** + * + * @class + * @extends Component + * + * @param {Object} options + */ + function PluginClassAnotherIFNodeComponent(options) { + Component.call(this, _defaults(options, { + alias: 'plugin_class_another_ifnode_component' + })); + } + Util.inherits(PluginClassAnotherIFNodeComponent, Component); + + return PluginClassAnotherIFNodeComponent; +}; diff --git a/examples/plugins/protected/extensions/internal-another-ifnode-component-es6-class/index.js b/examples/plugins/protected/extensions/internal-another-ifnode-component-es6-class/index.js new file mode 100644 index 0000000..c378f14 --- /dev/null +++ b/examples/plugins/protected/extensions/internal-another-ifnode-component-es6-class/index.js @@ -0,0 +1,32 @@ +var _defaults = require('lodash/defaults'); +var Util = require('util'); +var PLUGIN_TYPES = require('./../../../../../core/PLUGIN_TYPES'); +var Component = require('./../internal-another-ifnode-component-class/Component'); + +module.exports[PLUGIN_TYPES.COMPONENT] = function() { + /** + * + * @class + * @extends Component + * + * @param {Object} options + */ + function PluginES6ClassAnotherIFNodeComponent(options) { + Component.call(this, _defaults(options, { + alias: 'plugin_es6_class_another_ifnode_component' + })); + } + Util.inherits(PluginES6ClassAnotherIFNodeComponent, Component); + + /** + * Simulates access of static Component's member accessing + * + * @private + * @type {string} + */ + Object.defineProperty(PluginES6ClassAnotherIFNodeComponent, '__IFNODE_ITEM', { + value: PLUGIN_TYPES.COMPONENT + }); + + return PluginES6ClassAnotherIFNodeComponent; +}; diff --git a/examples/plugins/protected/extensions/internal-component-class/index.js b/examples/plugins/protected/extensions/internal-component-class/index.js new file mode 100644 index 0000000..4369abc --- /dev/null +++ b/examples/plugins/protected/extensions/internal-component-class/index.js @@ -0,0 +1,23 @@ +var _defaults = require('lodash/defaults'); +var Util = require('util'); +var Component = require('../../../../../core/Component'); + +module.exports = { + component: function() { + /** + * + * @class + * @extends Component + * + * @param {Object} options + */ + function PluginClassComponent(options) { + Component.call(this, _defaults(options, { + alias: 'plugin_class_component' + })); + } + Util.inherits(PluginClassComponent, Component); + + return PluginClassComponent; + } +}; diff --git a/examples/plugins/protected/extensions/internal-component/index.js b/examples/plugins/protected/extensions/internal-component/index.js index 0d955c1..6207501 100644 --- a/examples/plugins/protected/extensions/internal-component/index.js +++ b/examples/plugins/protected/extensions/internal-component/index.js @@ -1,4 +1,8 @@ module.exports = { component: function(app, Component) { + Component({ + name: 'PluginComponent', + alias: 'plugin_component' + }); } }; diff --git a/package.json b/package.json index 1ab757c..303ba6d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ifnode", - "version": "1.7.0", + "version": "1.12.0", "description": "Node.js MVC Framework", "main": "index.js", "scripts": { @@ -19,22 +19,25 @@ "author": "Ilya Frolov ", "license": "MIT", "dependencies": { - "debug": "3.0.0", + "debug": "3.2.7", "diread": "0.2.0", - "express": "4.15.4", - "lodash": "4.17.4", - "uuid": "3.1.0" + "express": "4.17.1", + "lodash": "4.17.21", + "uuid": "3.4.0" }, "devDependencies": { - "body-parser": "1.17.2", + "body-parser": "1.19.0", "coveralls": "2.13.1", + "es6-promise": "4.2.4", "eslint": "4.5.0", "eslint-plugin-require-jsdoc": "1.0.4", "istanbul": "0.4.5", "mocha": "3.5.0", - "serve-static": "1.12.4", - "setprototypeof": "1.0.3", + "serve-static": "1.14.1", "should": "11.2.1", - "supertest": "3.0.0" + "supertest": "2.0.1" + }, + "engines": { + "node": ">=0.10.0" } } diff --git a/test/01. application.js b/test/01. application.js index e1eb1c7..3bbbf14 100644 --- a/test/01. application.js +++ b/test/01. application.js @@ -1,7 +1,9 @@ 'use strict'; +var assert = require('assert'); var Path = require('path'); var Should = require('should'); +var Component = require("../core/Component"); var IFNode = require('../'); describe('Application', function() { @@ -95,9 +97,21 @@ describe('Application', function() { it('shouldn\'t load non-exists plugin', function() { var app = IFNode(); - (function() { + try { app.register('non-exists-plugin'); - }).should.throw(); + } catch (error) { + assert.ok(/Cannot\sfind\snode\smodule\sor\sextension/.test(error.message)); + } + }); + + it('should throw original error', function() { + var app = require('../examples/extensions/app'); + + try { + app.register('with-syntax-error'); + } catch (error) { + assert.ok(error instanceof SyntaxError); + } }); it('load by extension name', function(done) { @@ -170,6 +184,52 @@ describe('Application', function() { }); }); + describe('app.inject(instance)', function() { + it('should inject component by id', function() { + var app = IFNode({ + project_folder: Path.resolve(__dirname, '../examples/components'), + }).load(); + + app.inject('first').should.be.an.Object(); + }); + + it('should inject component by class', function(done) { + function Simple(options) { + Component.call(this, options); + } + + function WithConfig(options, injected_app) { + Component.call(this, options); + Should.deepEqual(this.config, { + passed: 'options' + }); + Should.equal(injected_app, app); + } + + WithConfig.prototype.finish_test = function() { + done(); + } + + var app = IFNode({ + configuration: { + components: { + WithConfig: { + passed: 'options' + } + } + } + }).load(); + + var component = app.inject(Simple); + + Should.equal(component.name, 'Simple'); + Should.equal(app.inject(Simple), component); + + component = app.inject(WithConfig) + component.finish_test(); + }) + }) + describe('app.load()', function() { var app = IFNode({ alias: 'app-load-test' diff --git a/test/03. components.js b/test/03. components.js index 1bf184a..20feb86 100644 --- a/test/03. components.js +++ b/test/03. components.js @@ -22,6 +22,7 @@ describe('Components', function() { app.component('component-in-folder').should.be.a.String(); app.component('ClassES5').should.be.a.Function(); + app.component('ClassES5DoubleInherits').should.be.a.Function(); app.component('ClassES5Component').should.be.not.a.Function(); }); @@ -103,6 +104,11 @@ describe('Components', function() { Should.equal(ClassES5.getName(), 'ClassES5'); Should.equal((new ClassES5).plain(), 'ClassES5#plain'); + + var ClassES5DoubleInherits = app.component('ClassES5DoubleInherits'); + + Should.equal((new ClassES5DoubleInherits).plain(), 'ClassES5#plain'); + Should.equal((new ClassES5DoubleInherits).plain2(), 'ClassES5DoubleInherits#plain'); }); it('should attach component from class', function() { diff --git a/test/05. controllers.js b/test/05. controllers.js index f947025..2b55662 100644 --- a/test/05. controllers.js +++ b/test/05. controllers.js @@ -43,6 +43,46 @@ describe('Controllers', function() { Should.exist(app.controllers.from_custom_folder); }); + + it('should disable controllers loading', function() { + var app = IFNode({ + project_folder: Path.resolve(__dirname, '../examples/controllers'), + alias: 'controllers-not-loaded', + environment: 'controllers-partial' + }).load({ + controllers: false + }); + + app.controllers.should.be.empty(); + }); + + it('should load controllers partially (exclude)', function() { + var app = IFNode({ + project_folder: Path.resolve(__dirname, '../examples/controllers'), + alias: 'controllers-partial-loading', + environment: 'controllers-partial' + }).load({ + controllers: { + exclude: /api\/v1\/skip/ + } + }); + + Should.not.exist(app.controllers['api/v1/skip']); + }); + + it('should load controllers partially (include)', function() { + var app = IFNode({ + project_folder: Path.resolve(__dirname, '../examples/controllers'), + alias: 'controllers-partial-loading', + environment: 'controllers-partial' + }).load({ + controllers: { + include: /api\/v1\/user/ + } + }); + + Should.not.exist(app.controllers['api/v1/skip']); + }); }); describe('app.Controller(options?: Object)', function() { @@ -346,4 +386,40 @@ describe('Controllers', function() { .expect(500, done); }) }); + + describe('Controller with promised handlers', function() { + /** + * + * @type {Application} + */ + var app = IFNode({ + project_folder: Path.resolve(__dirname, '../examples/controllers'), + alias: 'controllers-with-promised-handlers', + environment: 'controllers-with-promised-handlers' + }).load(); + + it('should return "resolved" with 200 status (non-promise is returned from handler)', function(done) { + SuperTest(app.listener) + .get('/with-promised-handlers/resolved-with-returning-non-promise') + .expect(200, 'resolved', done); + }); + + it('should return "resolved" with 200 status (promise is returned from handler)', function(done) { + SuperTest(app.listener) + .get('/with-promised-handlers/resolved-with-returning-promise') + .expect(200, 'resolved', done); + }); + + it('should return "rejected" with 500 status', function(done) { + SuperTest(app.listener) + .get('/with-promised-handlers/rejected-caught-by-handler') + .expect(500, 'rejected', done); + }); + + it('should return "default-error:rejected" with 500 status by common controller\'s handler', function(done) { + SuperTest(app.listener) + .get('/with-promised-handlers/rejected-caught-by-ifnode') + .expect(500, 'default-error-handler:rejected', done); + }); + }); }); diff --git a/test/application-configuration.js b/test/application-configuration.js index 31bca5e..df49637 100644 --- a/test/application-configuration.js +++ b/test/application-configuration.js @@ -11,28 +11,65 @@ var app = application_builder({ environment: 'local' }); -describe('Application configuration', function() { - it('should be valid configuration', function() { - var config = app.config; - var site_config = config.site; +/** + * + * @param {Application} app + */ +function validate_local_application(app) { + var config = app.config; + var site_config = config.site; + + config.should.be.an.Object(); + + config.env.should.be.equal('local'); + config.environment.should.be.equal('local'); + + site_config.local.origin.should.be.equal('https://localhost:8080'); + site_config.local.url('/some/path').should.be.equal('https://localhost:8080/some/path'); - config.should.be.an.Object(); + site_config.global.ssl.key.should.be.equal(Path.resolve(app.project_folder, 'path/to/key')); - config.env.should.be.equal('local'); - config.environment.should.be.equal('local'); + site_config.global.origin.should.be.equal('https://ifnode.com:3000'); + site_config.global.url('some/path').should.be.equal('https://ifnode.com:3000/some/path'); - site_config.local.origin.should.be.equal('https://localhost:8080'); - site_config.local.url('/some/path').should.be.equal('https://localhost:8080/some/path'); + config.application.express.should.be.an.Object(); + config.application.folders.should.be.an.Object(); - site_config.global.ssl.key.should.be.equal(Path.resolve(app.project_folder, 'path/to/key')); + config.db.virtual.schema.should.be.equal('virtual'); - site_config.global.origin.should.be.equal('https://ifnode.com:3000'); - site_config.global.url('some/path').should.be.equal('https://ifnode.com:3000/some/path'); + config.some.function.should.be.Function(); + config.some.instance.should.be.instanceOf(config.some.function); +} - config.application.express.should.be.an.Object(); - config.application.folders.should.be.an.Object(); +describe('Application configuration', function() { + it('should be valid configuration', function() { + validate_local_application(app); + }); - config.db.virtual.schema.should.be.equal('virtual'); + describe('"configuration" option', function() { + it("should be valid configuration by relative path", function() { + var app = application_builder({ + project_folder: Path.resolve(__dirname, '../examples/config'), + configuration: 'config/local' + }); + validate_local_application(app); + }); + it("should be valid configuration by full path", function() { + var project_path = Path.resolve(__dirname, '../examples/config'); + var app = application_builder({ + project_folder: project_path, + configuration: Path.resolve(project_path, './config/local') + }); + validate_local_application(app); + }); + it("should be valid configuration by object", function() { + var project_path = Path.resolve(__dirname, '../examples/config'); + var app = application_builder({ + project_folder: project_path, + configuration: require(Path.resolve(project_path, './config/local')) + }); + validate_local_application(app); + }); }); it("should be non-editable configuration", function() { diff --git a/test/plugins.js b/test/plugins.js index 2f274db..5b70a6a 100644 --- a/test/plugins.js +++ b/test/plugins.js @@ -2,15 +2,21 @@ require('should'); var Path = require('path'); +var Should = require('should'); var SuperTest = require('supertest'); -var IFNode = require('../'); +var IFNode = require('..'); var app = IFNode({ project_folder: Path.resolve(__dirname, '../examples/plugins'), - alias: 'plugins' + alias: 'plugins', + environment: 'plugins' }); app.register([ + 'internal-component', + 'internal-component-class', + 'internal-another-ifnode-component-class', + 'internal-another-ifnode-component-es6-class', 'defined-controller-plugin' ]); app.load(); @@ -20,5 +26,46 @@ describe('Plugins', function() { SuperTest(app.listener) .get('/defined-controller-plugin') .expect('controller_plugin_option', done); - }) + }); + + it('should has loaded plugin component', function() { + Should.ok( + app.component('PluginComponent') === app.PluginComponent && + app.PluginComponent === app.component('plugin_component') && + app.plugin_component === app.component('PluginComponent') + ); + }); + + it('should has loaded plugin class component', function() { + Should.ok( + app.component('PluginClassComponent') === app.plugin_class_component && + app.PluginClassComponent === app.component('plugin_class_component') && + app.plugin_class_component === app.component('PluginClassComponent') + ); + }); + + it('should has loaded plugin another ifnode class component', function() { + Should.ok( + app.component('PluginClassAnotherIFNodeComponent') === app.plugin_class_another_ifnode_component && + app.PluginClassAnotherIFNodeComponent === app.component('plugin_class_another_ifnode_component') && + app.plugin_class_another_ifnode_component === app.component('PluginClassAnotherIFNodeComponent') + ); + Should.ok( + app.component('PluginES6ClassAnotherIFNodeComponent') === app.plugin_es6_class_another_ifnode_component && + app.PluginES6ClassAnotherIFNodeComponent === app.component('plugin_es6_class_another_ifnode_component') && + app.plugin_es6_class_another_ifnode_component === app.component('PluginES6ClassAnotherIFNodeComponent') + ); + }); + + it('should load and compile components before application ones', function() { + Should.equal( + app.component('ApplicationComponent').get_plugin_component(), + app.component('PluginComponent') + ); + }); + + it('should has correct configurations for plugins', function() { + Should.deepEqual(app.component('PluginComponent').config, app.config.components.PluginComponent); + Should.deepEqual(app.component('PluginClassComponent').config, app.config.components.PluginClassComponent); + }); });