From 7d674779e18c04c9838954bbc0eba5e6840a2f9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 6 Jun 2014 11:47:25 +0200 Subject: [PATCH] refactor: extract runtime and registry Move isBrowser and isServer from lib/loopback to a new file lib/runtime. Move all Model and DataSource related methods like `createModel` and `createDataSource` to lib/registry. Remove the circular dependency between lib/application and lib/loopback, by loading lib/registry and/or lib/runtime instead of lib/loopback where appropriate This commit is only moving the code around, the functionality should not be changed at all. --- docs.json | 2 + lib/application.js | 12 +- lib/loopback.js | 279 ++-------------------------------------- lib/models/model.js | 4 +- lib/registry.js | 305 ++++++++++++++++++++++++++++++++++++++++++++ lib/runtime.js | 22 ++++ 6 files changed, 350 insertions(+), 274 deletions(-) create mode 100644 lib/registry.js create mode 100644 lib/runtime.js diff --git a/docs.json b/docs.json index f05d25c8..4517a139 100644 --- a/docs.json +++ b/docs.json @@ -3,6 +3,8 @@ "content": [ "lib/application.js", "lib/loopback.js", + "lib/runtime.js", + "lib/registry.js", { "title": "Base model", "depth": 2 }, "lib/models/model.js", "lib/models/data-model.js", diff --git a/lib/application.js b/lib/application.js index ae96bd19..92efb49a 100644 --- a/lib/application.js +++ b/lib/application.js @@ -3,7 +3,7 @@ */ var DataSource = require('loopback-datasource-juggler').DataSource - , loopback = require('../') + , registry = require('./registry') , compat = require('./compat') , assert = require('assert') , fs = require('fs') @@ -128,7 +128,7 @@ app.model = function (Model, config) { // modeller does not understand `dataSource` option delete modelConfig.dataSource; - Model = loopback.createModelFromConfig(modelConfig); + Model = registry.createModelFromConfig(modelConfig); // delete config options already applied ['relations', 'base', 'acls', 'hidden'].forEach(function(prop) { @@ -531,7 +531,7 @@ app.boot = function(options) { // try to attach models to dataSources by type try { - require('./loopback').autoAttach(); + registry.autoAttach(); } catch(e) { if(e.name === 'AssertionError') { console.warn(e); @@ -594,11 +594,11 @@ function dataSourcesFromConfig(config, connectorRegistry) { } } - return require('./loopback').createDataSource(config); + return registry.createDataSource(config); } function configureModel(ModelCtor, config, app) { - assert(ModelCtor.prototype instanceof loopback.Model, + assert(ModelCtor.prototype instanceof registry.Model, 'Model must be a descendant of loopback.Model'); var dataSource = config.dataSource; @@ -614,7 +614,7 @@ function configureModel(ModelCtor, config, app) { config = extend({}, config); config.dataSource = dataSource; - loopback.configureModel(ModelCtor, config); + registry.configureModel(ModelCtor, config); } function requireDir(dir, basenames) { diff --git a/lib/loopback.js b/lib/loopback.js index aa228fe7..f6fbb293 100644 --- a/lib/loopback.js +++ b/lib/loopback.js @@ -3,6 +3,7 @@ */ var express = require('express') + , proto = require('./application') , fs = require('fs') , ejs = require('ejs') , EventEmitter = require('events').EventEmitter @@ -29,18 +30,6 @@ var express = require('express') var loopback = exports = module.exports = createApplication; -/** - * True if running in a browser environment; false otherwise. - */ - -loopback.isBrowser = typeof window !== 'undefined'; - -/** - * True if running in a server environment; false otherwise. - */ - -loopback.isServer = !loopback.isBrowser; - /** * Framework version. */ @@ -68,9 +57,6 @@ loopback.compat = require('./compat'); function createApplication() { var app = express(); - // Defer loading of `./application` until all `loopback` static methods - // are defined, because `./application` depends on loopback. - var proto = require('./application'); merge(app, proto); // Create a new instance of models registry per each app instance @@ -93,17 +79,23 @@ function createApplication() { return app; } +function mixin(source) { + for (var key in source) { + var desc = Object.getOwnPropertyDescriptor(source, key); + Object.defineProperty(loopback, key, desc); + } +} + +mixin(require('./runtime')); +mixin(require('./registry')); + /*! * Expose express.middleware as loopback.* * for example `loopback.errorHandler` etc. */ -for (var key in express) { - Object.defineProperty( - loopback - , key - , Object.getOwnPropertyDescriptor(express, key)); -} +mixin(express); + /*! * Expose additional loopback middleware @@ -129,143 +121,6 @@ if (loopback.isServer) { loopback.errorHandler.title = 'Loopback'; -/** - * Create a data source with passing the provided options to the connector. - * - * @param {String} name Optional name. - * @options {Object} Data Source options - * @property {Object} connector LoopBack connector. - * @property {*} Other properties See the relevant connector documentation. - */ - -loopback.createDataSource = function (name, options) { - var ds = new DataSource(name, options, loopback.Model.modelBuilder); - ds.createModel = function (name, properties, settings) { - var ModelCtor = loopback.createModel(name, properties, settings); - ModelCtor.attachTo(ds); - return ModelCtor; - }; - - if(ds.settings && ds.settings.defaultForType) { - loopback.setDefaultDataSourceForType(ds.settings.defaultForType, ds); - } - - return ds; -}; - -/** - * Create a named vanilla JavaScript class constructor with an attached set of properties and options. - * - * @param {String} name Unique name. - * @param {Object} properties - * @param {Object} options (optional) - */ - -loopback.createModel = function (name, properties, options) { - options = options || {}; - var BaseModel = options.base || options.super; - - if(typeof BaseModel === 'string') { - BaseModel = loopback.getModel(BaseModel); - } - - BaseModel = BaseModel || loopback.Model; - - var model = BaseModel.extend(name, properties, options); - - // try to attach - try { - loopback.autoAttachModel(model); - } catch(e) {} - - return model; -}; - -/** - * Create a model as described by the configuration object. - * - * @example - * - * ```js - * loopback.createModelFromConfig({ - * name: 'Author', - * properties: { - * firstName: 'string', - * lastName: 'string - * }, - * relations: { - * books: { - * model: 'Book', - * type: 'hasAndBelongsToMany' - * } - * } - * }); - * ``` - * - * @options {Object} model configuration - * @property {String} name Unique name. - * @property {Object=} properties Model properties - * @property {Object=} options Model options. Options can be specified on the - * top level config object too. E.g. `{ base: 'User' }` is the same as - * `{ options: { base: 'User' } }`. - */ -loopback.createModelFromConfig = function(config) { - var name = config.name; - var properties = config.properties; - var options = buildModelOptionsFromConfig(config); - - assert(typeof name === 'string', - 'The model-config property `name` must be a string'); - - return loopback.createModel(name, properties, options); -}; - -function buildModelOptionsFromConfig(config) { - var options = merge({}, config.options); - for (var key in config) { - if (['name', 'properties', 'options'].indexOf(key) !== -1) { - // Skip items which have special meaning - continue; - } - - if (options[key] !== undefined) { - // When both `config.key` and `config.options.key` are set, - // use the latter one - continue; - } - - options[key] = config[key]; - } - return options; -} - -/** - * Alter an existing Model class. - * @param {Model} ModelCtor The model constructor to alter. - * @options {Object} Additional configuration to apply - * @property {DataSource} dataSource Attach the model to a dataSource. - * @property {Object} relations Model relations to add/update. - */ -loopback.configureModel = function(ModelCtor, config) { - var settings = ModelCtor.settings; - - if (config.relations) { - var relations = settings.relations = settings.relations || {}; - Object.keys(config.relations).forEach(function(key) { - relations[key] = merge(relations[key] || {}, config.relations[key]); - }); - } - - // It's important to attach the datasource after we have updated - // configuration, so that the datasource picks up updated relations - if (config.dataSource) { - assert(config.dataSource instanceof DataSource, - 'Cannot configure ' + ModelCtor.modelName + - ': config.dataSource must be an instance of loopback.DataSource'); - ModelCtor.attachTo(config.dataSource); - } -}; - /** * Add a remote method to a model. * @param {Function} fn @@ -298,119 +153,11 @@ loopback.template = function (file) { return ejs.compile(str); }; -/** - * Get an in-memory data source. Use one if it already exists. - * - * @param {String} [name] The name of the data source. If not provided, the `'default'` is used. - */ - -loopback.memory = function (name) { - name = name || 'default'; - var memory = ( - this._memoryDataSources - || (this._memoryDataSources = {}) - )[name]; - - if(!memory) { - memory = this._memoryDataSources[name] = loopback.createDataSource({ - connector: loopback.Memory - }); - } - - return memory; -}; - -/** - * Look up a model class by name from all models created by loopback.createModel() - * @param {String} modelName The model name - * @returns {Model} The model class - */ -loopback.getModel = function(modelName) { - return loopback.Model.modelBuilder.models[modelName]; -}; - -/** - * Look up a model class by the base model class. The method can be used by LoopBack - * to find configured models in models.json over the base model. - * @param {Model} The base model class - * @returns {Model} The subclass if found or the base class - */ -loopback.getModelByType = function(modelType) { - assert(typeof modelType === 'function', 'The model type must be a constructor'); - var models = loopback.Model.modelBuilder.models; - for(var m in models) { - if(models[m].prototype instanceof modelType) { - return models[m]; - } - } - return modelType; -}; - -/** - * Set the default `dataSource` for a given `type`. - * @param {String} type The datasource type - * @param {Object|DataSource} dataSource The data source settings or instance - * @returns {DataSource} The data source instance - */ - -loopback.setDefaultDataSourceForType = function(type, dataSource) { - var defaultDataSources = this.defaultDataSources || (this.defaultDataSources = {}); - - if(!(dataSource instanceof DataSource)) { - dataSource = this.createDataSource(dataSource); - } - - defaultDataSources[type] = dataSource; - return dataSource; -}; - -/** - * Get the default `dataSource` for a given `type`. - * @param {String} type The datasource type - * @returns {DataSource} The data source instance - */ - -loopback.getDefaultDataSourceForType = function(type) { - return this.defaultDataSources && this.defaultDataSources[type]; -}; - -/** - * Attach any model that does not have a dataSource to - * the default dataSource for the type the Model requests - */ - -loopback.autoAttach = function() { - var models = this.Model.modelBuilder.models; - assert.equal(typeof models, 'object', 'Cannot autoAttach without a models object'); - - Object.keys(models).forEach(function(modelName) { - var ModelCtor = models[modelName]; - - // Only auto attach if the model doesn't have an explicit data source - if(ModelCtor && (!(ModelCtor.dataSource instanceof DataSource))) { - loopback.autoAttachModel(ModelCtor); - } - }); -}; - -loopback.autoAttachModel = function(ModelCtor) { - if(ModelCtor.autoAttach) { - var ds = loopback.getDefaultDataSourceForType(ModelCtor.autoAttach); - - assert(ds instanceof DataSource, 'cannot autoAttach model "' - + ModelCtor.modelName - + '". No dataSource found of type ' + ModelCtor.autoAttach); - - ModelCtor.attachTo(ds); - } -}; /*! * Built in models / services */ -loopback.Model = require('./models/model'); -loopback.DataModel = require('./models/data-model'); loopback.Email = require('./models/email'); loopback.User = require('./models/user'); loopback.Application = require('./models/application'); diff --git a/lib/models/model.js b/lib/models/model.js index 7a9dc296..ab1053c9 100644 --- a/lib/models/model.js +++ b/lib/models/model.js @@ -1,7 +1,7 @@ /*! * Module Dependencies. */ -var loopback = require('../loopback'); +var registry = require('../registry'); var compat = require('../compat'); var ModelBuilder = require('loopback-datasource-juggler').ModelBuilder; var modeler = new ModelBuilder(); @@ -128,7 +128,7 @@ Model._ACL = function getACL(ACL) { return _aclModel; } var aclModel = require('./acl').ACL; - _aclModel = loopback.getModelByType(aclModel); + _aclModel = registry.getModelByType(aclModel); return _aclModel; }; diff --git a/lib/registry.js b/lib/registry.js new file mode 100644 index 00000000..53221f44 --- /dev/null +++ b/lib/registry.js @@ -0,0 +1,305 @@ +/* + * This file exports methods and objects for manipulating + * Models and DataSources. + * + * It is an internal file that should not be used outside of loopback. + * All exported entities can be accessed via the `loopback` object. + * @private + */ + +var assert = require('assert'); +var extend = require('util')._extend; +var DataSource = require('loopback-datasource-juggler').DataSource; + +var registry = module.exports; + + +/** + * Create a named vanilla JavaScript class constructor with an attached + * set of properties and options. + * + * @param {String} name Unique name. + * @param {Object} properties + * @param {Object} options (optional) + * + * @header loopback.createModel + */ + +registry.createModel = function (name, properties, options) { + options = options || {}; + var BaseModel = options.base || options.super; + + if(typeof BaseModel === 'string') { + var baseName = BaseModel; + BaseModel = this.getModel(BaseModel); + + if (BaseModel === undefined) { + if (baseName === 'DataModel') { + console.warn('Model `%s` is extending deprecated `DataModel. ' + + 'Use `PeristedModel` instead.', name); + BaseModel = this.PersistedModel; + } else { + console.warn('Model `%s` is extending an unknown model `%s`. ' + + 'Using `PersistedModel` as the base.', name, baseName); + } + } + } + + BaseModel = BaseModel || this.Model; + + var model = BaseModel.extend(name, properties, options); + + // try to attach + try { + this.autoAttachModel(model); + } catch(e) {} + + return model; +}; + +/** + * Create a model as described by the configuration object. + * + * **Example** + * + * ```js + * loopback.createModelFromConfig({ + * name: 'Author', + * properties: { + * firstName: 'string', + * lastName: 'string + * }, + * relations: { + * books: { + * model: 'Book', + * type: 'hasAndBelongsToMany' + * } + * } + * }); + * ``` + * + * @options {Object} model configuration + * @property {String} name Unique name. + * @property {Object} [properties] Model properties + * @property {Object} [options] Model options. Options can be specified on the + * top level config object too. E.g. `{ base: 'User' }` is the same as + * `{ options: { base: 'User' } }`. + * + * @header loopback.createModelFromConfig(config) + */ + +registry.createModelFromConfig = function(config) { + var name = config.name; + var properties = config.properties; + var options = buildModelOptionsFromConfig(config); + + assert(typeof name === 'string', + 'The model-config property `name` must be a string'); + + return this.createModel(name, properties, options); +}; + +function buildModelOptionsFromConfig(config) { + var options = extend({}, config.options); + for (var key in config) { + if (['name', 'properties', 'options'].indexOf(key) !== -1) { + // Skip items which have special meaning + continue; + } + + if (options[key] !== undefined) { + // When both `config.key` and `config.options.key` are set, + // use the latter one + continue; + } + + options[key] = config[key]; + } + return options; +} + +/** + * Alter an existing Model class. + * @param {Model} ModelCtor The model constructor to alter. + * @options {Object} Additional configuration to apply + * @property {DataSource} dataSource Attach the model to a dataSource. + * @property {Object} [relations] Model relations to add/update. + * + * @header loopback.configureModel(ModelCtor, config) + */ + +registry.configureModel = function(ModelCtor, config) { + var settings = ModelCtor.settings; + + if (config.relations) { + var relations = settings.relations = settings.relations || {}; + Object.keys(config.relations).forEach(function(key) { + relations[key] = extend(relations[key] || {}, config.relations[key]); + }); + } + + // It's important to attach the datasource after we have updated + // configuration, so that the datasource picks up updated relations + if (config.dataSource) { + assert(config.dataSource instanceof DataSource, + 'Cannot configure ' + ModelCtor.modelName + + ': config.dataSource must be an instance of DataSource'); + ModelCtor.attachTo(config.dataSource); + } +}; + +/** + * Look up a model class by name from all models created by + * `loopback.createModel()` + * @param {String} modelName The model name + * @returns {Model} The model class + * + * @header loopback.getModel(modelName) + */ +registry.getModel = function(modelName) { + return this.Model.modelBuilder.models[modelName]; +}; + +/** + * Look up a model class by the base model class. + * The method can be used by LoopBack + * to find configured models in models.json over the base model. + * @param {Model} modelType The base model class + * @returns {Model} The subclass if found or the base class + * + * @header loopback.getModelByType(modelType) + */ +registry.getModelByType = function(modelType) { + assert(typeof modelType === 'function', + 'The model type must be a constructor'); + var models = this.Model.modelBuilder.models; + for(var m in models) { + if(models[m].prototype instanceof modelType) { + return models[m]; + } + } + return modelType; +}; + +/** + * Create a data source with passing the provided options to the connector. + * + * @param {String} name Optional name. + * @options {Object} Data Source options + * @property {Object} connector LoopBack connector. + * @property {*} Other properties See the relevant connector documentation. + * + * @header loopback.createDataSource(name, options) + */ + +registry.createDataSource = function (name, options) { + var loopback = this; + var ds = new DataSource(name, options, loopback.Model.modelBuilder); + ds.createModel = function (name, properties, settings) { + var ModelCtor = loopback.createModel(name, properties, settings); + ModelCtor.attachTo(ds); + return ModelCtor; + }; + + if(ds.settings && ds.settings.defaultForType) { + this.setDefaultDataSourceForType(ds.settings.defaultForType, ds); + } + + return ds; +}; + +/** + * Get an in-memory data source. Use one if it already exists. + * + * @param {String} [name] The name of the data source. + * If not provided, the `'default'` is used. + * + * @header loopback.memory() + */ + +registry.memory = function (name) { + name = name || 'default'; + var memory = ( + this._memoryDataSources || (this._memoryDataSources = {}) + )[name]; + + if(!memory) { + memory = this._memoryDataSources[name] = this.createDataSource({ + connector: loopback.Memory + }); + } + + return memory; +}; + +/** + * Set the default `dataSource` for a given `type`. + * @param {String} type The datasource type + * @param {Object|DataSource} dataSource The data source settings or instance + * @returns {DataSource} The data source instance + * + * @header loopback.setDefaultDataSourceForType(type, dataSource) + */ + +registry.setDefaultDataSourceForType = function(type, dataSource) { + var defaultDataSources = this.defaultDataSources || + (this.defaultDataSources = {}); + + if(!(dataSource instanceof DataSource)) { + dataSource = this.createDataSource(dataSource); + } + + defaultDataSources[type] = dataSource; + return dataSource; +}; + +/** + * Get the default `dataSource` for a given `type`. + * @param {String} type The datasource type + * @returns {DataSource} The data source instance + * @header loopback.getDefaultDataSourceForType() + */ + +registry.getDefaultDataSourceForType = function(type) { + return this.defaultDataSources && this.defaultDataSources[type]; +}; + +/** + * Attach any model that does not have a dataSource to + * the default dataSource for the type the Model requests + * @header loopback.autoAttach() + */ + +registry.autoAttach = function() { + var models = this.Model.modelBuilder.models; + assert.equal(typeof models, 'object', 'Cannot autoAttach without a models object'); + + Object.keys(models).forEach(function(modelName) { + var ModelCtor = models[modelName]; + + // Only auto attach if the model doesn't have an explicit data source + if(ModelCtor && (!(ModelCtor.dataSource instanceof DataSource))) { + this.autoAttachModel(ModelCtor); + } + }, this); +}; + +registry.autoAttachModel = function(ModelCtor) { + if(ModelCtor.autoAttach) { + var ds = this.getDefaultDataSourceForType(ModelCtor.autoAttach); + + assert(ds instanceof DataSource, 'cannot autoAttach model "' + + ModelCtor.modelName + + '". No dataSource found of type ' + ModelCtor.autoAttach); + + ModelCtor.attachTo(ds); + } +}; + +/* + * Core models + * @private + */ + +registry.Model = require('./models/model'); +registry.DataModel = require('./models/data-model'); diff --git a/lib/runtime.js b/lib/runtime.js new file mode 100644 index 00000000..f179c533 --- /dev/null +++ b/lib/runtime.js @@ -0,0 +1,22 @@ +/* + * This is an internal file that should not be used outside of loopback. + * All exported entities can be accessed via the `loopback` object. + * @private + */ + +var runtime = exports; + +/** + * True if running in a browser environment; false otherwise. + * @header loopback.isBrowser + */ + +runtime.isBrowser = typeof window !== 'undefined'; + +/** + * True if running in a server environment; false otherwise. + * @header loopback.isServer + */ + +runtime.isServer = !runtime.isBrowser; +