From 439aa883ec07010be852b3485e7d8691607f137d Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Mon, 9 Jun 2014 15:14:28 -0700 Subject: [PATCH 01/30] Bump version so that we can republish --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1c7b6e29..42c3bff9 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "mobile", "mBaaS" ], - "version": "1.8.7", + "version": "1.8.8", "scripts": { "test": "mocha -R spec" }, From 1694ee2d835fe476fcf07ff15e398ec5be5f8fe3 Mon Sep 17 00:00:00 2001 From: crandmck Date: Mon, 9 Jun 2014 15:36:08 -0700 Subject: [PATCH 02/30] JSDoc cleanup --- lib/models/acl.js | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/lib/models/acl.js b/lib/models/acl.js index b29b3e63..a10cfbbc 100644 --- a/lib/models/acl.js +++ b/lib/models/acl.js @@ -61,6 +61,7 @@ var ACLSchema = { /** * Name of the access type - READ/WRITE/EXEC + * @property accessType {String} Name of the access type - READ/WRITE/EXEC */ accessType: String, @@ -114,7 +115,7 @@ ACL.SCOPE = Principal.SCOPE; * Calculate the matching score for the given rule and request * @param {ACL} rule The ACL entry * @param {AccessRequest} req The request - * @returns {number} + * @returns {Number} */ ACL.getMatchingScore = function getMatchingScore(rule, req) { var props = ['model', 'property', 'accessType']; @@ -296,14 +297,12 @@ ACL.getStaticACLs = function getStaticACLs(model, property) { /** * Check if the given principal is allowed to access the model/property - * @param {String} principalType The principal type - * @param {String} principalId The principal id - * @param {String} model The model name - * @param {String} property The property/method/relation name - * @param {String} accessType The access type - * @param {Function} callback The callback function - * - * @callback callback + * @param {String} principalType The principal type. + * @param {String} principalId The principal ID. + * @param {String} model The model name. + * @param {String} property The property/method/relation name. + * @param {String} accessType The access type. + * @callback {Function} callback Callback function. * @param {String|Error} err The error object * @param {AccessRequest} result The access permission */ @@ -364,14 +363,14 @@ ACL.prototype.debug = function() { } /** - * Check if the request has the permission to access - * @param {Object} context - * @property {Object[]} principals An array of principals - * @property {String|Model} model The model name or model class - * @property {*} id The model instance id - * @property {String} property The property/method/relation name - * @property {String} accessType The access type - * @param {Function} callback + * Check if the request has the permission to access. + * @options {Object} context See below. + * @property {Object[]} principals An array of principals. + * @property {String|Model} model The model name or model class. + * @property {*} id The model instance ID. + * @property {String} property The property/method/relation name. + * @property {String} accessType The access type: READE, WRITE, or EXEC. + * @param {Function} callback Callback function */ ACL.checkAccessForContext = function (context, callback) { @@ -452,8 +451,7 @@ ACL.checkAccessForContext = function (context, callback) { * @param {String} model The model name * @param {*} modelId The model id * @param {String} method The method name - * @end - * @callback {Function} callback + * @callback {Function} callback Callback function * @param {String|Error} err The error object * @param {Boolean} allowed is the request allowed */ From 6af69e322adfd3378d2dbe9527c67f251362c0d3 Mon Sep 17 00:00:00 2001 From: crandmck Date: Mon, 9 Jun 2014 15:50:04 -0700 Subject: [PATCH 03/30] JSDoc cleanup --- lib/models/application.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/models/application.js b/lib/models/application.js index f537559e..b562d278 100644 --- a/lib/models/application.js +++ b/lib/models/application.js @@ -46,7 +46,7 @@ var PushNotificationSettingSchema = { gcm: GcmSettingsSchema }; -/** +/*! * Data model for Application */ var ApplicationSchema = { @@ -133,10 +133,10 @@ Application.beforeCreate = function (next) { /** * Register a new application - * @param owner Owner's user id - * @param name Name of the application - * @param options Other options - * @param cb Callback function + * @param {String} owner Owner's user ID. + * @param {String} name Name of the application + * @param {Object} options Other options + * @param {Function} callback Callback function */ Application.register = function (owner, name, options, cb) { assert(owner, 'owner is required'); From 815ad53aa4ccb99b806f158ff81b5b91c24dfb84 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Mon, 9 Jun 2014 16:31:33 -0700 Subject: [PATCH 04/30] Register existing model to app.models during app.model() --- lib/application.js | 12 ++++++++---- test/app.test.js | 7 +++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/lib/application.js b/lib/application.js index 0c373fed..0489593e 100644 --- a/lib/application.js +++ b/lib/application.js @@ -105,26 +105,30 @@ app.disuse = function (route) { */ app.model = function (Model, config) { + var modelName = Model; if(arguments.length === 1) { assert(typeof Model === 'function', 'app.model(Model) => Model must be a function / constructor'); - assert(Model.modelName, 'Model must have a "modelName" property'); + modelName = Model.modelName; + assert(modelName, 'Model must have a "modelName" property'); var remotingClassName = compat.getClassNameForRemoting(Model); this.remotes().exports[remotingClassName] = Model; this.models().push(Model); + this.models[modelName] = + this.models[classify(modelName)] = + this.models[camelize(modelName)] = Model; clearHandlerCache(this); Model.shared = true; Model.app = this; Model.emit('attached', this); return; } - var modelName = Model; config = config || {}; assert(typeof modelName === 'string', 'app.model(name, config) => "name" name must be a string'); - Model = + Model = modelFromConfig(modelName, config, this); this.models[modelName] = this.models[classify(modelName)] = - this.models[camelize(modelName)] = modelFromConfig(modelName, config, this); + this.models[camelize(modelName)] = Model; if(config.public !== false) { this.model(Model); diff --git a/test/app.test.js b/test/app.test.js index 74ea8923..866d2405 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -29,6 +29,13 @@ describe('app', function() { expect(app.remotes().exports).to.eql({ color: Color }); }); + it('registers existing models to app.models', function() { + var Color = db.createModel('color', {name: String}); + app.model(Color); + expect(app.models.color).to.eql(Color); + expect(app.models.Color).to.eql(Color); + }); + it('updates REST API when a new model is added', function(done) { app.use(loopback.rest()); request(app).get('/colors').expect(404, function(err, res) { From 7769174d58daa693ea02717e13eaa0a7e454e781 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Mon, 9 Jun 2014 23:53:01 -0700 Subject: [PATCH 05/30] Tidy up app.model() to remove duplicate & recusrive call --- lib/application.js | 44 ++++++++++++++++++++++---------------------- test/app.test.js | 24 ++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/lib/application.js b/lib/application.js index 0489593e..271c4bae 100644 --- a/lib/application.js +++ b/lib/application.js @@ -105,37 +105,37 @@ app.disuse = function (route) { */ app.model = function (Model, config) { - var modelName = Model; - if(arguments.length === 1) { + var modelName = null; + var isPublic = true; + if (arguments.length > 1) { + config = config || {}; + modelName = Model; + assert(typeof modelName === 'string', 'app.model(name, config) => "name" name must be a string'); + Model = modelFromConfig(modelName, config, this); + isPublic = config.public !== false; + } else { assert(typeof Model === 'function', 'app.model(Model) => Model must be a function / constructor'); modelName = Model.modelName; assert(modelName, 'Model must have a "modelName" property'); + } + + this.models[modelName] = + this.models[classify(modelName)] = + this.models[camelize(modelName)] = Model; + + this.models().push(Model); + + if (isPublic) { var remotingClassName = compat.getClassNameForRemoting(Model); this.remotes().exports[remotingClassName] = Model; - this.models().push(Model); - this.models[modelName] = - this.models[classify(modelName)] = - this.models[camelize(modelName)] = Model; clearHandlerCache(this); - Model.shared = true; - Model.app = this; - Model.emit('attached', this); - return; - } - config = config || {}; - assert(typeof modelName === 'string', 'app.model(name, config) => "name" name must be a string'); - - Model = modelFromConfig(modelName, config, this); - this.models[modelName] = - this.models[classify(modelName)] = - this.models[camelize(modelName)] = Model; - - if(config.public !== false) { - this.model(Model); } + Model.shared = isPublic; // The base Model has shared = true + Model.app = this; + Model.emit('attached', this); return Model; -} +}; /** * Get the models exported by the app. Returns only models defined using `app.model()` diff --git a/test/app.test.js b/test/app.test.js index 866d2405..e3e73c81 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -32,8 +32,10 @@ describe('app', function() { it('registers existing models to app.models', function() { var Color = db.createModel('color', {name: String}); app.model(Color); - expect(app.models.color).to.eql(Color); - expect(app.models.Color).to.eql(Color); + expect(Color.app).to.be.equal(app); + expect(Color.shared).to.equal(true); + expect(app.models.color).to.equal(Color); + expect(app.models.Color).to.equal(Color); }); it('updates REST API when a new model is added', function(done) { @@ -114,6 +116,24 @@ describe('app', function() { expect(app.models.foo.definition.settings.base).to.equal('Application'); }); + + it('honors config.public options', function() { + app.model('foo', { + dataSource: 'db', + public: false + }); + expect(app.models.foo.app).to.equal(app); + expect(app.models.foo.shared).to.equal(false); + }); + + it('defaults config.public to be true', function() { + app.model('foo', { + dataSource: 'db' + }); + expect(app.models.foo.app).to.equal(app); + expect(app.models.foo.shared).to.equal(true); + }); + }); describe('app.models', function() { From 6490447fac37704fbe078f3980bf7cc232e34451 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 13 May 2014 13:09:15 -0700 Subject: [PATCH 06/30] Set the role id to be generated See https://github.com/strongloop/loopback-connector-postgresql/issues/5 --- lib/models/role.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/models/role.js b/lib/models/role.js index 072b7a76..5e733b15 100644 --- a/lib/models/role.js +++ b/lib/models/role.js @@ -7,7 +7,7 @@ var AccessContext = require('./access-context').AccessContext; // Role model var RoleSchema = { - id: {type: String, id: true}, // Id + id: {type: String, id: true, generated: true}, // Id name: {type: String, required: true}, // The name of a role description: String, // Description @@ -20,8 +20,8 @@ var RoleSchema = { * Map principals to roles */ var RoleMappingSchema = { - id: {type: String, id: true}, // Id - roleId: String, // The role id + id: {type: String, id: true, generated: true}, // Id + // roleId: String, // The role id, to be injected by the belongsTo relation principalType: String, // The principal type, such as user, application, or role principalId: String // The principal id }; From e72ff61d99d044054515ec514c25c0d3f076ebce Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 10 Jun 2014 16:39:32 -0700 Subject: [PATCH 07/30] Add a test case --- test/role.test.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/role.test.js b/test/role.test.js index 3c43bc9e..9252743e 100644 --- a/test/role.test.js +++ b/test/role.test.js @@ -77,6 +77,40 @@ describe('role model', function () { }); + + it("should automatically generate role id", function () { + + User.create({name: 'Raymond', email: 'x@y.com', password: 'foobar'}, function (err, user) { + // console.log('User: ', user.id); + Role.create({name: 'userRole'}, function (err, role) { + assert(role.id); + role.principals.create({principalType: RoleMapping.USER, principalId: user.id}, function (err, p) { + assert(p.id); + assert.equal(p.roleId, role.id); + Role.find(function (err, roles) { + assert(!err); + assert.equal(roles.length, 1); + assert.equal(roles[0].name, 'userRole'); + }); + role.principals(function (err, principals) { + assert(!err); + // console.log(principals); + assert.equal(principals.length, 1); + assert.equal(principals[0].principalType, RoleMapping.USER); + assert.equal(principals[0].principalId, user.id); + }); + role.users(function (err, users) { + assert(!err); + assert.equal(users.length, 1); + assert.equal(users[0].principalType, RoleMapping.USER); + assert.equal(users[0].principalId, user.id); + }); + }); + }); + }); + + }); + it("should support getRoles() and isInRole()", function () { User.create({name: 'Raymond', email: 'x@y.com', password: 'foobar'}, function (err, user) { // console.log('User: ', user.id); From ad347e517c1da8992a44d6d7ff6d977b09db0704 Mon Sep 17 00:00:00 2001 From: crandmck Date: Wed, 11 Jun 2014 14:55:47 -0700 Subject: [PATCH 08/30] JSDoc fixes --- lib/models/model.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/models/model.js b/lib/models/model.js index fdeea317..7a9dc296 100644 --- a/lib/models/model.js +++ b/lib/models/model.js @@ -136,13 +136,11 @@ Model._ACL = function getACL(ACL) { * Check if the given access token can invoke the method * * @param {AccessToken} token The access token - * @param {*} modelId The model id - * @param {SharedMethod} sharedMethod - * @param callback The callback function - * - * @callback {Function} callback + * @param {*} modelId The model ID. + * @param {SharedMethod} sharedMethod The method in question + * @callback {Function} callback The callback function * @param {String|Error} err The error object - * @param {Boolean} allowed is the request allowed + * @param {Boolean} allowed True if the request is allowed; false otherwise. */ Model.checkAccess = function(token, modelId, sharedMethod, callback) { var ANONYMOUS = require('./access-token').ANONYMOUS; From 3deadcd180373551a8ac8a9201a81a4da76b7b6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 9 Jun 2014 09:53:05 +0200 Subject: [PATCH 09/30] test: fix ACL integration tests Change the tests creating new users so that they send valid user data, in order to prevent 422 "validation failed" responses. Upgrade loopback-testing to 0.2.0. --- package.json | 2 +- test/access-control.integration.js | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 42c3bff9..bc49dbbf 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "strong-task-emitter": "0.0.x", "supertest": "~0.12.1", "chai": "~1.9.1", - "loopback-testing": "~0.1.2", + "loopback-testing": "~0.2.0", "browserify": "~4.1.5", "grunt": "~0.4.5", "grunt-browserify": "~2.1.0", diff --git a/test/access-control.integration.js b/test/access-control.integration.js index 1abb320e..c0705adc 100644 --- a/test/access-control.integration.js +++ b/test/access-control.integration.js @@ -69,8 +69,11 @@ describe('access control - integration', function () { lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', urlForUser); lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER,'GET', urlForUser); - lt.it.shouldBeAllowedWhenCalledAnonymously('POST', '/api/users'); - lt.it.shouldBeAllowedWhenCalledByUser(CURRENT_USER, 'POST', '/api/users'); + lt.it.shouldBeAllowedWhenCalledAnonymously( + 'POST', '/api/users', newUserData()); + + lt.it.shouldBeAllowedWhenCalledByUser( + CURRENT_USER, 'POST', '/api/users', newUserData()); lt.it.shouldBeAllowedWhenCalledByUser(CURRENT_USER, 'POST', '/api/users/logout'); @@ -112,6 +115,15 @@ describe('access control - integration', function () { function urlForUser() { return '/api/users/' + this.randomUser.id; } + + var userCounter; + function newUserData() { + userCounter = userCounter ? ++userCounter : 1; + return { + email: 'new-' + userCounter + '@test.test', + password: 'test' + }; + } }); describe('/banks', function () { From e7884dce50eb480cf970547c3dcbb725eca1fc85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 9 Jun 2014 09:54:38 +0200 Subject: [PATCH 10/30] package: upgrade Mocha to 1.20 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bc49dbbf..5a45d3a9 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ }, "devDependencies": { "loopback-datasource-juggler": ">=1.4.0 <1.6.0", - "mocha": "~1.18.0", + "mocha": "~1.20.1", "strong-task-emitter": "0.0.x", "supertest": "~0.12.1", "chai": "~1.9.1", From 4259a3862aa52ae048df41d8c12638f698a3981c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 3 Jun 2014 10:39:54 +0200 Subject: [PATCH 11/30] Make app.get/app.set available in browser Implement settings object and methods in browser-express. --- lib/browser-express.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/browser-express.js b/lib/browser-express.js index 386e8159..82aba2fa 100644 --- a/lib/browser-express.js +++ b/lib/browser-express.js @@ -1,7 +1,25 @@ module.exports = browserExpress; function browserExpress() { - return {}; + return new BrowserExpress(); } browserExpress.errorHandler = {}; + +function BrowserExpress() { + this.settings = {}; +} + +BrowserExpress.prototype.set = function(key, value) { + if (arguments.length == 1) { + return this.get(key); + } + + this.settings[key] = value; + + return this; // fluent API +}; + +BrowserExpress.prototype.get = function(key) { + return this.settings[key]; +}; From fc0fad4a9fbbff2b372f38a0302fcc457272b46c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 5 Jun 2014 17:41:12 +0200 Subject: [PATCH 12/30] Add createModelFromConfig and configureModel() Add new API allowing developers to split the model definition and configuration into two steps: 1. Build models from JSON config, export them for re-use: ```js var Customer = loopback.createModelFromConfig({ name: 'Customer', base: 'User', properties: { address: 'string' } }); ``` 2. Attach existing models to a dataSource and a loopback app, modify certain model aspects like relations: ```js loopback.configureModel(Customer, { dataSource: db, relations: { /* ... */ } }); ``` Rework `app.model` to use `loopback.configureModel` under the hood. Here is the new usage: ```js var Customer = require('./models').Customer; app.model(Customer, { dataSource: 'db', relations: { /* ... */ } }); ``` In order to preserve backwards compatibility, `app.model(name, config)` calls both `createModelFromConfig` and `configureModel`. --- lib/application.js | 110 +++++++++++++++++++++++------------------- lib/loopback.js | 89 +++++++++++++++++++++++++++++++++- test/app.test.js | 14 ++++++ test/loopback.test.js | 98 +++++++++++++++++++++++++++++++++++++ 4 files changed, 261 insertions(+), 50 deletions(-) diff --git a/lib/application.js b/lib/application.js index 271c4bae..9e369fc5 100644 --- a/lib/application.js +++ b/lib/application.js @@ -3,7 +3,7 @@ */ var DataSource = require('loopback-datasource-juggler').DataSource - , ModelBuilder = require('loopback-datasource-juggler').ModelBuilder + , loopback = require('../') , compat = require('./compat') , assert = require('assert') , fs = require('fs') @@ -82,43 +82,69 @@ app.disuse = function (route) { } /** - * Define and attach a model to the app. The `Model` will be available on the + * Attach a model to the app. The `Model` will be available on the * `app.models` object. * * ```js - * var Widget = app.model('Widget', {dataSource: 'db'}); - * Widget.create({name: 'pencil'}); - * app.models.Widget.find(function(err, widgets) { - * console.log(widgets[0]); // => {name: 'pencil'} + * // Attach an existing model + * var User = loopback.User; + * app.model(User); + * + * // Attach an existing model, alter some aspects of the model + * var User = loopback.User; + * app.model(User, { dataSource: 'db' }); + * + * // The old way: create and attach a new model (deprecated) + * var Widget = app.model('Widget', { + * dataSource: 'db', + * properties: { + * name: 'string' + * } * }); * ``` * - * @param {String} modelName The name of the model to define. + * @param {Object|String} Model The model to attach. * @options {Object} config The model's configuration. - * @property {String|DataSource} dataSource The `DataSource` to which to attach the model. - * @property {Object} [options] an object containing `Model` options. - * @property {ACL[]} [options.acls] an array of `ACL` definitions. - * @property {String[]} [options.hidden] **experimental** an array of properties to hide when accessed remotely. - * @property {Object} [properties] object defining the `Model` properties in [LoopBack Definition Language](http://docs.strongloop.com/loopback-datasource-juggler/#loopback-definition-language). + * @property {String|DataSource} dataSource The `DataSource` to which to + * attach the model. + * @property {Boolean} [public] whether the model should be exposed via REST API + * @property {Object} [relations] relations to add/update * @end * @returns {ModelConstructor} the model class */ app.model = function (Model, config) { - var modelName = null; var isPublic = true; if (arguments.length > 1) { config = config || {}; - modelName = Model; - assert(typeof modelName === 'string', 'app.model(name, config) => "name" name must be a string'); - Model = modelFromConfig(modelName, config, this); + if (typeof Model === 'string') { + // create & attach the model - backwards compatibility + + // create config for loopback.modelFromConfig + var modelConfig = extend({}, config); + modelConfig.options = extend({}, config.options); + modelConfig.name = Model; + + // modeller does not understand `dataSource` option + delete modelConfig.dataSource; + + Model = loopback.createModelFromConfig(modelConfig); + + // delete config options already applied + ['relations', 'base', 'acls', 'hidden'].forEach(function(prop) { + delete config[prop]; + if (config.options) delete config.options[prop]; + }); + delete config.properties; + } + + configureModel(Model, config, this); isPublic = config.public !== false; } else { - assert(typeof Model === 'function', 'app.model(Model) => Model must be a function / constructor'); - modelName = Model.modelName; - assert(modelName, 'Model must have a "modelName" property'); + assertIsModel(Model); } + var modelName = Model.modelName; this.models[modelName] = this.models[classify(modelName)] = this.models[camelize(modelName)] = Model; @@ -137,6 +163,14 @@ app.model = function (Model, config) { return Model; }; +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()` * @@ -570,41 +604,23 @@ function dataSourcesFromConfig(config, connectorRegistry) { return require('./loopback').createDataSource(config); } -function modelFromConfig(name, config, app) { - var options = buildModelOptionsFromConfig(config); - var properties = config.properties; +function configureModel(ModelCtor, config, app) { + assertIsModel(ModelCtor); - var ModelCtor = require('./loopback').createModel(name, properties, options); var dataSource = config.dataSource; if(typeof dataSource === 'string') { dataSource = app.dataSources[dataSource]; } - assert(isDataSource(dataSource), name + ' is referencing a dataSource that does not exist: "'+ config.dataSource +'"'); + assert(isDataSource(dataSource), + ModelCtor.modelName + ' is referencing a dataSource that does not exist: "' + + config.dataSource +'"'); - ModelCtor.attachTo(dataSource); - return ModelCtor; -} + config = extend({}, config); + config.dataSource = dataSource; -function buildModelOptionsFromConfig(config) { - var options = extend({}, config.options); - for (var key in config) { - if (['properties', 'options', 'dataSource'].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 to preserve backwards compatibility - // with loopback 1.x - continue; - } - - options[key] = config[key]; - } - return options; + loopback.configureModel(ModelCtor, config); } function requireDir(dir, basenames) { @@ -676,10 +692,6 @@ function tryReadDir() { } } -function isModelCtor(obj) { - return typeof obj === 'function' && obj.modelName && obj.name === 'ModelCtor'; -} - function isDataSource(obj) { return obj instanceof DataSource; } diff --git a/lib/loopback.js b/lib/loopback.js index d2017ee7..aa228fe7 100644 --- a/lib/loopback.js +++ b/lib/loopback.js @@ -7,7 +7,6 @@ var express = require('express') , ejs = require('ejs') , EventEmitter = require('events').EventEmitter , path = require('path') - , proto = require('./application') , DataSource = require('loopback-datasource-juggler').DataSource , ModelBuilder = require('loopback-datasource-juggler').ModelBuilder , i8n = require('inflection') @@ -69,6 +68,9 @@ 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 @@ -179,6 +181,91 @@ loopback.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} 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 diff --git a/test/app.test.js b/test/app.test.js index e3e73c81..803fcdaf 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -136,6 +136,20 @@ describe('app', function() { }); + describe('app.model(ModelCtor, config)', function() { + it('attaches the model to a datasource', function() { + app.dataSource('db', { connector: 'memory' }); + var TestModel = loopback.Model.extend('TestModel'); + // TestModel was most likely already defined in a different test, + // thus TestModel.dataSource may be already set + delete TestModel.dataSource; + + app.model(TestModel, { dataSource: 'db' }); + + expect(app.models.TestModel.dataSource).to.equal(app.dataSources.db); + }); + }); + describe('app.models', function() { it('is unique per app instance', function() { app.dataSource('db', { connector: 'memory' }); diff --git a/test/loopback.test.js b/test/loopback.test.js index 7988fdb4..5cb28098 100644 --- a/test/loopback.test.js +++ b/test/loopback.test.js @@ -1,4 +1,11 @@ describe('loopback', function() { + var nameCounter = 0; + var uniqueModelName; + + beforeEach(function() { + uniqueModelName = 'TestModel-' + (++nameCounter); + }); + describe('exports', function() { it('ValidationError', function() { expect(loopback.ValidationError).to.be.a('function') @@ -119,4 +126,95 @@ describe('loopback', function() { }); }); }); + + describe('loopback.createModelFromConfig(config)', function() { + it('creates the model', function() { + var model = loopback.createModelFromConfig({ + name: uniqueModelName + }); + + expect(model.prototype).to.be.instanceof(loopback.Model); + }); + + it('interprets extra first-level keys as options', function() { + var model = loopback.createModelFromConfig({ + name: uniqueModelName, + base: 'User' + }); + + expect(model.prototype).to.be.instanceof(loopback.User); + }); + + it('prefers config.options.key over config.key', function() { + var model = loopback.createModelFromConfig({ + name: uniqueModelName, + base: 'User', + options: { + base: 'Application' + } + }); + + expect(model.prototype).to.be.instanceof(loopback.Application); + }); + }); + + describe('loopback.configureModel(ModelCtor, config)', function() { + it('adds new relations', function() { + var model = loopback.Model.extend(uniqueModelName); + + loopback.configureModel(model, { + relations: { + owner: { + type: 'belongsTo', + model: 'User' + } + } + }); + + expect(model.settings.relations).to.have.property('owner'); + }); + + it('updates existing relations', function() { + var model = loopback.Model.extend(uniqueModelName, {}, { + relations: { + owner: { + type: 'belongsTo', + model: 'User' + } + } + }); + + loopback.configureModel(model, { + relations: { + owner: { + model: 'Application' + } + } + }); + + expect(model.settings.relations.owner).to.eql({ + type: 'belongsTo', + model: 'Application' + }); + }); + + it('updates relations before attaching to a dataSource', function() { + var db = loopback.createDataSource({ connector: loopback.Memory }); + var model = loopback.Model.extend(uniqueModelName); + + loopback.configureModel(model, { + dataSource: db, + relations: { + owner: { + type: 'belongsTo', + model: 'User' + } + } + }); + + var owner = model.prototype.owner; + expect(owner, 'model.prototype.owner').to.be.a('function'); + expect(owner._targetClass).to.equal('User'); + }); + }); }); From 19425b8fd9c79e73f52c93ac635f1ea8f39d8f0a 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 13/30] 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 9e369fc5..ae96bd19 100644 --- a/lib/application.js +++ b/lib/application.js @@ -141,7 +141,8 @@ app.model = function (Model, config) { configureModel(Model, config, this); isPublic = config.public !== false; } else { - assertIsModel(Model); + assert(Model.prototype instanceof loopback.Model, + 'Model must be a descendant of loopback.Model'); } var modelName = Model.modelName; @@ -163,14 +164,6 @@ app.model = function (Model, config) { return Model; }; -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 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 14/30] 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; + From f0ff40d3f72f148a6817a738e519df0791e5c333 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 15/30] 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 c0705adc..575baebd 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 () { @@ -99,6 +100,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 0c3df8df..f4f667c1 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 7cd6e09790a0b3884d7453f8a5ec17a5476ba5ee 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 16/30] 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 53221f44..f36cbfc2 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 362cceb70abebf81186f9896e8d8442e452fc31e 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 17/30] 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 92efb49a..1873547d 100644 --- a/lib/application.js +++ b/lib/application.js @@ -128,7 +128,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 f36cbfc2..a5f01532 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 f05291ca931da6596150da172daef6cf986f677e 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 18/30] 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 a5f01532..80dc712b 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 02d1c5e3c2161988bca19ccf25f0fa60c49767fe 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 19/30] 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 80dc712b..1c48e24c 100644 --- a/lib/registry.js +++ b/lib/registry.js @@ -318,6 +318,8 @@ registry.autoAttachModel = function(ModelCtor) { } }; +registry.DataSource = DataSource; + /* * Core models * @private From c4db83ad43bb6d2ddf6f21e4e55b5888838d26e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 13 Jun 2014 10:27:23 +0200 Subject: [PATCH 20/30] Expose loopback as `app.loopback` The primary intention is to allow loopback plugins to determine the version of the loopback framework from the `app` object. --- lib/loopback.js | 2 ++ test/app.test.js | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/lib/loopback.js b/lib/loopback.js index f6fbb293..cdc9d296 100644 --- a/lib/loopback.js +++ b/lib/loopback.js @@ -59,6 +59,8 @@ function createApplication() { merge(app, proto); + app.loopback = loopback; + // Create a new instance of models registry per each app instance app.models = function() { return proto.models.apply(this, arguments); diff --git a/test/app.test.js b/test/app.test.js index 803fcdaf..139e24ea 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -585,4 +585,9 @@ describe('app', function() { expect(app.connectors.FOOBAR).to.equal(loopback.Memory); }); }); + + it('exposes loopback as a property', function() { + var app = loopback(); + expect(app.loopback).to.equal(loopback); + }); }); From a90a5c7e58aecb49b47cb4e733a94ddba883b82a Mon Sep 17 00:00:00 2001 From: Karl Mikkelsen Date: Fri, 6 Jun 2014 11:53:30 +1000 Subject: [PATCH 21/30] Allow customization of ACL http status emulate existing error on 404 new tests for model and app settings Signed-off-by: Karl Mikkelsen --- lib/application.js | 18 ++++++++++++-- test/access-token.test.js | 52 +++++++++++++++++++++++++++++++++++---- 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/lib/application.js b/lib/application.js index 0c373fed..3b75e231 100644 --- a/lib/application.js +++ b/lib/application.js @@ -295,6 +295,7 @@ app.dataSources = app.datasources = {}; app.enableAuth = function() { var remotes = this.remotes(); + var app = this; remotes.before('**', function(ctx, next, method) { var req = ctx.req; @@ -302,6 +303,12 @@ app.enableAuth = function() { var modelInstance = ctx.instance; var modelId = modelInstance && modelInstance.id || req.param('id'); + var modelSettings = Model.settings || {}; + var errStatusCode = modelSettings.aclErrorStatus || app.get('aclErrorStatus') || 401; + if(!req.accessToken){ + errStatusCode = 401; + } + if(Model.checkAccess) { // Pause the request before checking access // See https://github.com/strongloop/loopback-storage-service/issues/7 @@ -319,8 +326,15 @@ app.enableAuth = function() { } else if(allowed) { next(); } else { - var e = new Error('Access Denied'); - e.statusCode = 401; + + var messages = { + 403:'Access Denied', + 404: ('could not find a model with id ' + modelId), + 401:'Authorization Required' + }; + + var e = new Error(messages[errStatusCode] || messages[403]); + e.statusCode = errStatusCode; next(e); } } diff --git a/test/access-token.test.js b/test/access-token.test.js index 50cf4d9a..1f4e7344 100644 --- a/test/access-token.test.js +++ b/test/access-token.test.js @@ -81,13 +81,38 @@ describe('app.enableAuth()', function() { beforeEach(createTestingToken); - it('should prevent remote method calls if the accessToken doesnt have access', function (done) { + it('prevents remote call with 401 status on denied ACL', function (done) { createTestAppAndRequest(this.token, done) .del('/tests/123') .expect(401) .set('authorization', this.token.id) .end(done); }); + + it('prevent remote call with app setting status on denied ACL', function (done) { + createTestAppAndRequest(this.token, {app:{aclErrorStatus:403}}, done) + .del('/tests/123') + .expect(403) + .set('authorization', this.token.id) + .end(done); + }); + + it('prevent remote call with app setting status on denied ACL', function (done) { + createTestAppAndRequest(this.token, {model:{aclErrorStatus:404}}, done) + .del('/tests/123') + .expect(404) + .set('authorization', this.token.id) + .end(done); + }); + + it('prevent remote call if the accessToken is missing and required', function (done) { + createTestAppAndRequest(null, done) + .del('/tests/123') + .expect(401) + .set('authorization', null) + .end(done); + }); + }); function createTestingToken(done) { @@ -99,12 +124,19 @@ function createTestingToken(done) { }); } -function createTestAppAndRequest(testToken, done) { - var app = createTestApp(testToken, done); +function createTestAppAndRequest(testToken, settings, done) { + var app = createTestApp(testToken, settings, done); return request(app); } -function createTestApp(testToken, done) { +function createTestApp(testToken, settings, done) { + done = arguments[arguments.length-1]; + if(settings == done) settings = {}; + settings = settings || {}; + + var appSettings = settings.app || {}; + var modelSettings = settings.model || {}; + var app = loopback(); app.use(loopback.cookieParser('secret')); @@ -125,7 +157,11 @@ function createTestApp(testToken, done) { app.use(loopback.rest()); app.enableAuth(); - var TestModel = loopback.Model.extend('test', {}, { + Object.keys(appSettings).forEach(function(key){ + app.set(key, appSettings[key]); + }); + + var modelOptions = { acls: [ { principalType: "ROLE", @@ -135,8 +171,14 @@ function createTestApp(testToken, done) { property: 'removeById' } ] + }; + + Object.keys(modelSettings).forEach(function(key){ + modelOptions[key] = modelSettings[key]; }); + var TestModel = loopback.Model.extend('test', {}, modelOptions); + TestModel.attachTo(loopback.memory()); app.model(TestModel); From 843e09342c5bd1d63a0b8a1eaca4ec94c06aa0b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 16 Jun 2014 09:52:11 +0200 Subject: [PATCH 22/30] lib/application: Remove forgotten `loopback` ref Use `registry.Model` instead of `loopback.Model`. --- lib/application.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/application.js b/lib/application.js index 4f1e439e..a94a5faa 100644 --- a/lib/application.js +++ b/lib/application.js @@ -141,7 +141,7 @@ app.model = function (Model, config) { configureModel(Model, config, this); isPublic = config.public !== false; } else { - assert(Model.prototype instanceof loopback.Model, + assert(Model.prototype instanceof registry.Model, 'Model must be a descendant of loopback.Model'); } From f20236c7159b87cb3c7504dc873fa5471032b00a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 16 Jun 2014 21:01:31 +0200 Subject: [PATCH 23/30] package: the next version will be a minor version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5a45d3a9..a7170b42 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "mobile", "mBaaS" ], - "version": "1.8.8", + "version": "1.9.0-pre", "scripts": { "test": "mocha -R spec" }, From afd8de0c31eebabdbfc750589aeaedb226000ca9 Mon Sep 17 00:00:00 2001 From: Samuel Reed Date: Fri, 20 Jun 2014 16:39:28 -0500 Subject: [PATCH 24/30] Fix a slowdown caused by mutation of an incoming accessToken option. --- lib/models/access-token.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/models/access-token.js b/lib/models/access-token.js index f5f66d20..2171016e 100644 --- a/lib/models/access-token.js +++ b/lib/models/access-token.js @@ -191,11 +191,9 @@ function tokenIdForRequest(req, options) { var length; var id; - params.push('access_token'); - headers.push('X-Access-Token'); - headers.push('authorization'); - cookies.push('access_token'); - cookies.push('authorization'); + params = params.concat(['access_token']); + headers = headers.concat(['X-Access-Token', 'authorization']); + cookies = cookies.concat(['access_token', 'authorization']); for(length = params.length; i < length; i++) { id = req.param(params[i]); From 9b5d7417a2bb34cb1d687a375aff6aa9ea66c793 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Mon, 16 Jun 2014 13:48:34 -0700 Subject: [PATCH 25/30] Remove relationNameFor --- test/data-source.test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/data-source.test.js b/test/data-source.test.js index eb0e1537..f5d9c6b7 100644 --- a/test/data-source.test.js +++ b/test/data-source.test.js @@ -23,7 +23,6 @@ describe('DataSource', function() { assert.isFunc(Color, 'destroyAll'); assert.isFunc(Color, 'count'); assert.isFunc(Color, 'include'); - assert.isFunc(Color, 'relationNameFor'); assert.isFunc(Color, 'hasMany'); assert.isFunc(Color, 'belongsTo'); assert.isFunc(Color, 'hasAndBelongsToMany'); @@ -53,7 +52,6 @@ describe('DataSource', function() { existsAndShared('destroyAll', false); existsAndShared('count', true); existsAndShared('include', false); - existsAndShared('relationNameFor', false); existsAndShared('hasMany', false); existsAndShared('belongsTo', false); existsAndShared('hasAndBelongsToMany', false); From 0d0a8658e56e28720ea6ecadf0d39208381ecb29 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 20 Jun 2014 21:24:14 -0700 Subject: [PATCH 26/30] Update juggler dep --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a7170b42..a4f691a6 100644 --- a/package.json +++ b/package.json @@ -44,10 +44,10 @@ "async": "~0.9.0" }, "peerDependencies": { - "loopback-datasource-juggler": ">=1.4.0 <1.6.0" + "loopback-datasource-juggler": ">=1.4.0 <1.7.0" }, "devDependencies": { - "loopback-datasource-juggler": ">=1.4.0 <1.6.0", + "loopback-datasource-juggler": ">=1.4.0 <1.7.0", "mocha": "~1.20.1", "strong-task-emitter": "0.0.x", "supertest": "~0.12.1", From bca972219695ea1f567bc7d8145724ec257ff6a6 Mon Sep 17 00:00:00 2001 From: Rand McKinney Date: Tue, 24 Jun 2014 16:14:01 -0700 Subject: [PATCH 27/30] Update link to doc --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3f7bb033..a8beab86 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ as illustrated below: ## Resources - * [Documentation](http://docs.strongloop.com/display/DOC/LoopBack). + * [Documentation](http://docs.strongloop.com/display/LB/LoopBack). * [API documentation](http://apidocs.strongloop.com/loopback). * [LoopBack Google Group](https://groups.google.com/forum/#!forum/loopbackjs). * [GitHub issues](https://github.com/strongloop/loopback/issues). From 70615696cd2626d0dd3ffa8c4e0ff0d5d655da80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Wed, 25 Jun 2014 13:44:07 +0200 Subject: [PATCH 28/30] Mark `app.boot` as deprecated. --- lib/application.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/application.js b/lib/application.js index a94a5faa..3b6d9dc6 100644 --- a/lib/application.js +++ b/lib/application.js @@ -167,6 +167,9 @@ app.model = function (Model, config) { /** * Get the models exported by the app. Returns only models defined using `app.model()` * + * **Deprecated. Use the package + * [loopback-boot](https://github.com/strongloop/loopback-boot) instead.** + * There are two ways to access models: * * 1. Call `app.models()` to get a list of all models. From 70c1cb5ce0971a627ffe23f2c79be1e964adf36f Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Wed, 25 Jun 2014 09:09:04 -0700 Subject: [PATCH 29/30] Update debug setting --- lib/connectors/mail.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/connectors/mail.js b/lib/connectors/mail.js index c0337f46..5660adcc 100644 --- a/lib/connectors/mail.js +++ b/lib/connectors/mail.js @@ -4,9 +4,8 @@ var mailer = require('nodemailer') , assert = require('assert') - , debug = require('debug') + , debug = require('debug')('loopback:connector:mail') , loopback = require('../loopback') - , STUB = 'STUB'; /** * Export the MailConnector class. From c20ecaf2655b614dcd0720fe168c13f53b03c41c Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Wed, 25 Jun 2014 09:09:14 -0700 Subject: [PATCH 30/30] Bump version and update deps --- package.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index a4f691a6..2f190c88 100644 --- a/package.json +++ b/package.json @@ -26,18 +26,18 @@ "mobile", "mBaaS" ], - "version": "1.9.0-pre", + "version": "1.9.0", "scripts": { "test": "mocha -R spec" }, "dependencies": { - "debug": "~0.8.1", + "debug": "~1.0.2", "express": "~3.5.0", "strong-remoting": "~1.5.0", - "inflection": "~1.3.5", - "nodemailer": "~0.6.5", + "inflection": "~1.3.7", + "nodemailer": "~0.7.0", "ejs": "~1.0.0", - "bcryptjs": "~0.7.12", + "bcryptjs": "~1.0.3", "underscore.string": "~2.3.3", "underscore": "~1.6.0", "uid2": "0.0.3", @@ -50,23 +50,23 @@ "loopback-datasource-juggler": ">=1.4.0 <1.7.0", "mocha": "~1.20.1", "strong-task-emitter": "0.0.x", - "supertest": "~0.12.1", + "supertest": "~0.13.0", "chai": "~1.9.1", "loopback-testing": "~0.2.0", - "browserify": "~4.1.5", + "browserify": "~4.1.11", "grunt": "~0.4.5", "grunt-browserify": "~2.1.0", - "grunt-contrib-uglify": "~0.4.0", + "grunt-contrib-uglify": "~0.5.0", "grunt-contrib-jshint": "~0.10.0", "grunt-contrib-watch": "~0.6.1", "karma-script-launcher": "~0.1.0", - "karma-chrome-launcher": "~0.1.3", + "karma-chrome-launcher": "~0.1.4", "karma-firefox-launcher": "~0.1.3", "karma-html2js-preprocessor": "~0.1.0", "karma-phantomjs-launcher": "~0.1.4", "karma": "~0.12.16", - "karma-browserify": "~0.2.0", - "karma-mocha": "~0.1.3", + "karma-browserify": "~0.2.1", + "karma-mocha": "~0.1.4", "grunt-karma": "~0.8.3" }, "repository": {