From 51e977de9bf101202cbcd3a1367fa07d96ff5be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 6 Jun 2014 10:28:21 +0200 Subject: [PATCH 1/8] lib/loopback: fix jsdoc comments Use @property {Object} [properties] instead of @property {Object=} properties for optional properties. Use `**example**` instead of `@example`, since strong-docs don't support the latter. --- lib/loopback.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/loopback.js b/lib/loopback.js index d360c0e2..85c41493 100644 --- a/lib/loopback.js +++ b/lib/loopback.js @@ -210,7 +210,7 @@ loopback.createModel = function (name, properties, options) { /** * Create a model as described by the configuration object. * - * @example + * **Example** * * ```js * loopback.createModelFromConfig({ @@ -230,8 +230,8 @@ loopback.createModel = function (name, properties, options) { * * @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 + * @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' } }`. */ @@ -270,7 +270,7 @@ function buildModelOptionsFromConfig(config) { * @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. + * @property {Object} [relations] Model relations to add/update. */ loopback.configureModel = function(ModelCtor, config) { var settings = ModelCtor.settings; From 1b1106099187c0b23d622410feb4823b01729142 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 6 Jun 2014 10:30:03 +0200 Subject: [PATCH 2/8] Remove assertIsModel and isDataSource Use `instanceof` operator instead: ModelCtor.prototype instanceof loopback.Model dataSource instanceof loopback.DataSource --- lib/application.js | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/lib/application.js b/lib/application.js index 4197d102..2f972f3c 100644 --- a/lib/application.js +++ b/lib/application.js @@ -115,7 +115,8 @@ app.disuse = function (route) { app.model = function (Model, config) { if(arguments.length === 1) { - assertIsModel(Model); + assert(Model.prototype instanceof loopback.Model, + 'Model must be a descendant of loopback.Model'); if(Model.sharedClass) { this.remotes().addClass(Model.sharedClass); } @@ -165,14 +166,6 @@ app.model = function (Model, config) { }; -function assertIsModel(Model) { - assert(typeof Model === 'function', - 'Model must be a function / constructor'); - assert(Model.modelName, 'Model must have a "modelName" property'); - assert(Model.prototype instanceof loopback.Model, - 'Model must be a descendant of loopback.Model'); -} - /** * Get the models exported by the app. Returns only models defined using `app.model()` * @@ -605,7 +598,8 @@ function dataSourcesFromConfig(config, connectorRegistry) { } function configureModel(ModelCtor, config, app) { - assertIsModel(ModelCtor); + assert(ModelCtor.prototype instanceof loopback.Model, + 'Model must be a descendant of loopback.Model'); var dataSource = config.dataSource; @@ -613,7 +607,7 @@ function configureModel(ModelCtor, config, app) { dataSource = app.dataSources[dataSource]; } - assert(isDataSource(dataSource), + assert(dataSource instanceof DataSource, ModelCtor.modelName + ' is referencing a dataSource that does not exist: "' + config.dataSource +'"'); @@ -692,10 +686,6 @@ function tryReadDir() { } } -function isDataSource(obj) { - return obj instanceof DataSource; -} - function tryReadConfig(cwd, fileName) { try { return require(path.join(cwd, fileName + '.json')); From eac231df99943c31bd1e77697c2941ebe1bdd5a6 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 3/8] 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 | 14 +- lib/loopback.js | 319 ++-------------------------------- lib/models/model.js | 4 +- lib/models/persisted-model.js | 4 +- lib/registry.js | 318 +++++++++++++++++++++++++++++++++ lib/runtime.js | 22 +++ 7 files changed, 366 insertions(+), 317 deletions(-) create mode 100644 lib/registry.js create mode 100644 lib/runtime.js diff --git a/docs.json b/docs.json index 6df5005f..5822b153 100644 --- a/docs.json +++ b/docs.json @@ -3,6 +3,8 @@ "content": [ "lib/application.js", "lib/loopback.js", + "lib/runtime.js", + "lib/registry.js", "lib/middleware/token.js", "lib/models/access-token.js", "lib/models/access-context.js", diff --git a/lib/application.js b/lib/application.js index 2f972f3c..88f0f1ff 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') @@ -115,7 +115,7 @@ app.disuse = function (route) { app.model = function (Model, config) { if(arguments.length === 1) { - assert(Model.prototype instanceof loopback.Model, + assert(Model.prototype instanceof registry.Model, 'Model must be a descendant of loopback.Model'); if(Model.sharedClass) { this.remotes().addClass(Model.sharedClass); @@ -141,7 +141,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 85c41493..12d46314 100644 --- a/lib/loopback.js +++ b/lib/loopback.js @@ -3,10 +3,10 @@ */ var express = require('express') + , proto = require('./application') , fs = require('fs') , ejs = require('ejs') , path = require('path') - , DataSource = require('loopback-datasource-juggler').DataSource , merge = require('util')._extend , assert = require('assert'); @@ -26,24 +26,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. - */ - -loopback.version = require('../package.json').version; - /** * Expose mime. */ @@ -65,9 +47,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 @@ -90,16 +69,21 @@ 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 static express methods like `express.errorHandler`. */ -for (var key in express) { - Object.defineProperty( - loopback - , key - , Object.getOwnPropertyDescriptor(express, key)); -} +mixin(express); /*! * Expose additional middleware like session as loopback.* @@ -110,13 +94,7 @@ for (var key in express) { if (loopback.isServer) { var middlewares = require('./express-middleware'); - - for (var key in middlewares) { - Object.defineProperty( - loopback - , key - , Object.getOwnPropertyDescriptor(middlewares, key)); - } + mixin(middlewares); } /*! @@ -143,155 +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') { - var baseName = BaseModel; - BaseModel = loopback.getModel(BaseModel); - - if (BaseModel === undefined) { - if (baseName === 'DataModel') { - console.warn('Model `%s` is extending deprecated `DataModel. ' + - 'Use `PeristedModel` instead.', name); - BaseModel = loopback.PersistedModel; - } else { - console.warn('Model `%s` is extending an unknown model `%s`. ' + - 'Using `PersistedModel` as the base.', name, baseName); - } - } - } - - BaseModel = BaseModel || loopback.PersistedModel; - - 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 @@ -324,132 +153,10 @@ 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.PersistedModel = require('./models/persisted-model'); - -// temporary alias to simplify migration of code based on <=2.0.0-beta3 -Object.defineProperty(loopback, 'DataModel', { - get: function() { - var stackLines = new Error().stack.split('\n'); - console.warn('loopback.DataModel is deprecated, ' + - 'use loopback.PersistedModel instead.'); - // Log the location where loopback.DataModel was called - console.warn(stackLines[2]); - return loopback.PersistedModel; - } -}); - 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 374cd43e..fa94aeaa 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 juggler = require('loopback-datasource-juggler'); var ModelBuilder = juggler.ModelBuilder; @@ -218,7 +218,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/models/persisted-model.js b/lib/models/persisted-model.js index 540eb29b..020398d1 100644 --- a/lib/models/persisted-model.js +++ b/lib/models/persisted-model.js @@ -3,7 +3,7 @@ */ var Model = require('./model'); -var loopback = require('../loopback'); +var runtime = require('../runtime'); var RemoteObjects = require('strong-remoting'); var assert = require('assert'); var async = require('async'); @@ -893,7 +893,7 @@ PersistedModel.enableChangeTracking = function() { Model.on('deletedAll', cleanup); - if(loopback.isServer) { + if(runtime.isServer) { // initial cleanup cleanup(); diff --git a/lib/registry.js b/lib/registry.js new file mode 100644 index 00000000..bb905da2 --- /dev/null +++ b/lib/registry.js @@ -0,0 +1,318 @@ +/* + * 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.PersistedModel; + + 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.PersistedModel = require('./models/persisted-model'); + +// temporary alias to simplify migration of code based on <=2.0.0-beta3 +Object.defineProperty(registry, 'DataModel', { + get: function() { + var stackLines = new Error().stack.split('\n'); + console.warn('loopback.DataModel is deprecated, ' + + 'use loopback.PersistedModel instead.'); + // Log the location where loopback.DataModel was called + console.warn(stackLines[2]); + return this.PersistedModel; + } +}); + 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; + From 1de6325a80a05d3cd9acdb2ca704c8900aa00f8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 6 Jun 2014 14:51:13 +0200 Subject: [PATCH 4/8] test: add debug logs Add debug logs to troubleshoot two unit tests failing on the CI server only. --- test/access-control.integration.js | 5 +++++ test/relations.integration.js | 9 ++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/test/access-control.integration.js b/test/access-control.integration.js index 1abb320e..0ef59c7b 100644 --- a/test/access-control.integration.js +++ b/test/access-control.integration.js @@ -6,6 +6,7 @@ var app = require(path.join(ACCESS_CONTROL_APP, 'app.js')); var assert = require('assert'); var USER = {email: 'test@test.test', password: 'test'}; var CURRENT_USER = {email: 'current@test.test', password: 'test'}; +var debug = require('debug')('loopback:test:access-control.integration'); describe('access control - integration', function () { @@ -96,6 +97,10 @@ describe('access control - integration', function () { lt.describe.whenCalledRemotely('GET', '/api/users/:id', function() { lt.it.shouldBeAllowed(); it('should not include a password', function() { + debug('GET /api/users/:id response: %s\nheaders: %j\nbody string: %s', + this.res.statusCode, + this.res.headers, + this.res.text); var user = this.res.body; assert.equal(user.password, undefined); }); diff --git a/test/relations.integration.js b/test/relations.integration.js index 9d9ae16a..863e2877 100644 --- a/test/relations.integration.js +++ b/test/relations.integration.js @@ -5,6 +5,7 @@ var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-integration-app'); var app = require(path.join(SIMPLE_APP, 'app.js')); var assert = require('assert'); var expect = require('chai').expect; +var debug = require('debug')('loopback:test:relations.integration'); describe('relations - integration', function () { @@ -28,13 +29,19 @@ describe('relations - integration', function () { this.url = '/api/stores/' + this.store.id + '/widgets'; }); lt.describe.whenCalledRemotely('GET', '/api/stores/:id/widgets', function() { + it('should succeed with statusCode 200', function() { assert.equal(this.res.statusCode, 200); }); describe('widgets (response.body)', function() { beforeEach(function() { + debug('GET /api/stores/:id/widgets response: %s' + + '\nheaders: %j\nbody string: %s', + this.res.statusCode, + this.res.headers, + this.res.text); this.widgets = this.res.body; - this.widget = this.res.body[0]; + this.widget = this.res.body && this.res.body[0]; }); it('should be an array', function() { assert(Array.isArray(this.widgets)); From 63843b41fceedb92641eac838b41d86f131f352b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 9 Jun 2014 07:46:32 +0200 Subject: [PATCH 5/8] lib/registry fix jsdoc comments Add missing names. --- lib/registry.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/registry.js b/lib/registry.js index bb905da2..c1d21082 100644 --- a/lib/registry.js +++ b/lib/registry.js @@ -78,7 +78,7 @@ registry.createModel = function (name, properties, options) { * }); * ``` * - * @options {Object} model configuration + * @options {Object} config model configuration * @property {String} name Unique name. * @property {Object} [properties] Model properties * @property {Object} [options] Model options. Options can be specified on the @@ -121,7 +121,7 @@ function buildModelOptionsFromConfig(config) { /** * Alter an existing Model class. * @param {Model} ModelCtor The model constructor to alter. - * @options {Object} Additional configuration to apply + * @options {Object} config Additional configuration to apply * @property {DataSource} dataSource Attach the model to a dataSource. * @property {Object} [relations] Model relations to add/update. * @@ -185,9 +185,10 @@ registry.getModelByType = function(modelType) { * Create a data source with passing the provided options to the connector. * * @param {String} name Optional name. - * @options {Object} Data Source options + * @options {Object} options Data Source options * @property {Object} connector LoopBack connector. - * @property {*} Other properties See the relevant connector documentation. + * @property {*} [*] Other connector properties. + * See the relevant connector documentation. * * @header loopback.createDataSource(name, options) */ From 56aab8dbdd9e662f55e94fd20cda7319615a8f61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 9 Jun 2014 11:18:52 +0200 Subject: [PATCH 6/8] Merge createModelFromConfig with createModel Merge the two methods `loopback.createModel` and `loopback.createModelFromConfig` into a single method `createModel`. --- lib/application.js | 2 +- lib/registry.js | 105 +++++++++++++++++++++++++----------------- test/loopback.test.js | 8 ++-- 3 files changed, 68 insertions(+), 47 deletions(-) diff --git a/lib/application.js b/lib/application.js index 88f0f1ff..1e89f6b0 100644 --- a/lib/application.js +++ b/lib/application.js @@ -141,7 +141,7 @@ app.model = function (Model, config) { // modeller does not understand `dataSource` option delete modelConfig.dataSource; - Model = registry.createModelFromConfig(modelConfig); + Model = registry.createModel(modelConfig); // delete config options already applied ['relations', 'base', 'acls', 'hidden'].forEach(function(prop) { diff --git a/lib/registry.js b/lib/registry.js index c1d21082..311d0b08 100644 --- a/lib/registry.js +++ b/lib/registry.js @@ -18,6 +18,59 @@ var registry = module.exports; * Create a named vanilla JavaScript class constructor with an attached * set of properties and options. * + * This function comes with two variants: + * * `loopback.createModel(name, properties, options)` + * * `loopback.createModel(config)` + * + * In the second variant, the parameters `name`, `properties` and `options` + * are provided in the config object. Any additional config entries are + * interpreted as `options`, i.e. the following two configs are identical: + * + * ```js + * { name: 'Customer', base: 'User' } + * { name: 'Customer', options: { base: 'User' } } + * ``` + * + * **Example** + * + * Create an `Author` model using the three-parameter variant: + * + * ```js + * loopback.createModel( + * 'Author', + * { + * firstName: 'string', + * lastName: 'string + * }, + * { + * relations: { + * books: { + * model: 'Book', + * type: 'hasAndBelongsToMany' + * } + * } + * } + * ); + * ``` + * + * Create the same model using a config object: + * + * ```js + * loopback.createModel({ + * name: 'Author', + * properties: { + * firstName: 'string', + * lastName: 'string + * }, + * relations: { + * books: { + * model: 'Book', + * type: 'hasAndBelongsToMany' + * } + * } + * }); + * ``` + * * @param {String} name Unique name. * @param {Object} properties * @param {Object} options (optional) @@ -26,6 +79,16 @@ var registry = module.exports; */ registry.createModel = function (name, properties, options) { + if (arguments.length === 1 && typeof name === 'object') { + var config = name; + name = config.name; + properties = config.properties; + options = buildModelOptionsFromConfig(config); + + assert(typeof name === 'string', + 'The model-config property `name` must be a string'); + } + options = options || {}; var BaseModel = options.base || options.super; @@ -57,48 +120,6 @@ registry.createModel = function (name, properties, options) { 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} config 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) { diff --git a/test/loopback.test.js b/test/loopback.test.js index 5cb28098..bb0ef95a 100644 --- a/test/loopback.test.js +++ b/test/loopback.test.js @@ -127,9 +127,9 @@ describe('loopback', function() { }); }); - describe('loopback.createModelFromConfig(config)', function() { + describe('loopback.createModel(config)', function() { it('creates the model', function() { - var model = loopback.createModelFromConfig({ + var model = loopback.createModel({ name: uniqueModelName }); @@ -137,7 +137,7 @@ describe('loopback', function() { }); it('interprets extra first-level keys as options', function() { - var model = loopback.createModelFromConfig({ + var model = loopback.createModel({ name: uniqueModelName, base: 'User' }); @@ -146,7 +146,7 @@ describe('loopback', function() { }); it('prefers config.options.key over config.key', function() { - var model = loopback.createModelFromConfig({ + var model = loopback.createModel({ name: uniqueModelName, base: 'User', options: { From 09cc57c0613617f43e93c657a96bca589ef00ac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 9 Jun 2014 16:15:56 +0200 Subject: [PATCH 7/8] registry: fix non-unique default dataSources Fix the problem where `registry.defaultDataSources` has two instances: - `require('loopback').defaultDataSources` used by `loopback.autoAttach()` - `require('./registry').defaultDataSources` used by `app.dataSource`. I am intentionally leaving out unit-tests as the whole `autoAttach` feature is going to be deleted before 2.0 is released. --- lib/registry.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/registry.js b/lib/registry.js index 311d0b08..0945af7d 100644 --- a/lib/registry.js +++ b/lib/registry.js @@ -13,6 +13,7 @@ var DataSource = require('loopback-datasource-juggler').DataSource; var registry = module.exports; +registry.defaultDataSources = {}; /** * Create a named vanilla JavaScript class constructor with an attached @@ -264,8 +265,7 @@ registry.memory = function (name) { */ registry.setDefaultDataSourceForType = function(type, dataSource) { - var defaultDataSources = this.defaultDataSources || - (this.defaultDataSources = {}); + var defaultDataSources = this.defaultDataSources; if(!(dataSource instanceof DataSource)) { dataSource = this.createDataSource(dataSource); From a90e24c013570c1d78047a53f6f3ca02f6c5b032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 9 Jun 2014 16:25:35 +0200 Subject: [PATCH 8/8] registry: export DataSource class Expose the juggler's DataSource constructor as `loopback.DataSource`. The DataSource constructor is most useful to check for `instanceof DataSource`, but it also makes the loopback API more consistent, since the API is already exposing all pre-built Models. --- lib/registry.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/registry.js b/lib/registry.js index 0945af7d..b3666c0b 100644 --- a/lib/registry.js +++ b/lib/registry.js @@ -318,6 +318,8 @@ registry.autoAttachModel = function(ModelCtor) { } }; +registry.DataSource = DataSource; + /* * Core models * @private